mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-25 09:48:09 +02:00
Implement mouse interaction with result list
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
@@ -211,6 +213,71 @@ func (s *SortedResults) AddSortedSlice(sl []*ResultItem) {
|
|||||||
s.slices = append(s.slices, sl)
|
s.slices = append(s.slices, sl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SortedResults) Apply(first, last CollectionIndex, action func(*ResultItem) (keep_going bool)) {
|
||||||
|
s.lock()
|
||||||
|
defer s.unlock()
|
||||||
|
if first.Slice >= len(s.slices) || first.Pos >= len(s.slices[first.Slice]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
amt := utils.IfElse(first.Less(last), 1, -1)
|
||||||
|
var did_wrap bool
|
||||||
|
for {
|
||||||
|
if !action(s.slices[first.Slice][first.Pos]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if first.Compare(last) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
first, did_wrap = s.increment_with_wrap_around(first, amt)
|
||||||
|
if did_wrap {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SortedResults) Closest(idx CollectionIndex, matches func(*ResultItem) bool) *CollectionIndex {
|
||||||
|
s.lock()
|
||||||
|
defer s.unlock()
|
||||||
|
if idx.Slice >= len(s.slices) || idx.Pos >= len(s.slices[idx.Slice]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
idx CollectionIndex
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
var a, b result
|
||||||
|
iterate := func(idx CollectionIndex, amt int, result *result) {
|
||||||
|
var did_wrap bool
|
||||||
|
var count int
|
||||||
|
result.count = -1
|
||||||
|
for {
|
||||||
|
idx, did_wrap = s.increment_with_wrap_around(idx, amt)
|
||||||
|
if did_wrap {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if matches(s.slices[idx.Slice][idx.Pos]) {
|
||||||
|
result.idx = idx
|
||||||
|
result.count = count
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go func() { iterate(idx, 1, &a) }()
|
||||||
|
go func() { iterate(idx, -1, &a) }()
|
||||||
|
if a.count < 0 && b.count < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return utils.IfElse(a.count < b.count, &b.idx, &a.idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SortedResults) IncrementIndexWithWrapAroundAndCheck(idx CollectionIndex, amt int) (ans CollectionIndex, did_wrap bool) {
|
||||||
|
s.lock()
|
||||||
|
defer s.unlock()
|
||||||
|
return s.increment_with_wrap_around(idx, amt)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SortedResults) IncrementIndexWithWrapAround(idx CollectionIndex, amt int) CollectionIndex {
|
func (s *SortedResults) IncrementIndexWithWrapAround(idx CollectionIndex, amt int) CollectionIndex {
|
||||||
s.lock()
|
s.lock()
|
||||||
defer s.unlock()
|
defer s.unlock()
|
||||||
@@ -259,6 +326,27 @@ func (s *SortedResults) increment_with_wrap_around(idx CollectionIndex, amt int)
|
|||||||
return idx, did_wrap
|
return idx, did_wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return a - b
|
||||||
|
func (s *SortedResults) SignedDistance(a, b CollectionIndex) (ans int) {
|
||||||
|
s.lock()
|
||||||
|
defer s.unlock()
|
||||||
|
return s.signed_distance(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a - b
|
||||||
|
func (s *SortedResults) signed_distance(a, b CollectionIndex) (ans int) {
|
||||||
|
mult := -1
|
||||||
|
if b.Less(a) {
|
||||||
|
a, b = b, a
|
||||||
|
mult = 1
|
||||||
|
}
|
||||||
|
limit := min(b.Slice, len(s.slices))
|
||||||
|
for ; a.Slice < limit; a.NextSlice() {
|
||||||
|
ans += len(s.slices[a.Slice]) - a.Pos
|
||||||
|
}
|
||||||
|
return mult * (ans + (b.Pos - a.Pos))
|
||||||
|
}
|
||||||
|
|
||||||
// Return |a - b|
|
// Return |a - b|
|
||||||
func (s *SortedResults) distance(a, b CollectionIndex) (ans int) {
|
func (s *SortedResults) distance(a, b CollectionIndex) (ans int) {
|
||||||
if b.Less(a) {
|
if b.Less(a) {
|
||||||
@@ -271,13 +359,13 @@ func (s *SortedResults) distance(a, b CollectionIndex) (ans int) {
|
|||||||
return ans + (b.Pos - a.Pos)
|
return ans + (b.Pos - a.Pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_column, num_before_current int, current CollectionIndex) (ans [][]*ResultItem, num_before int) {
|
func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_column, num_before_current int, current CollectionIndex) (ans [][]*ResultItem, num_before int, first_idx CollectionIndex) {
|
||||||
s.lock()
|
s.lock()
|
||||||
defer s.unlock()
|
defer s.unlock()
|
||||||
num_cols := calc_num_cols(s.len)
|
num_cols := calc_num_cols(s.len)
|
||||||
total := num_cols * num_per_column
|
total := num_cols * num_per_column
|
||||||
if total < 1 {
|
if total < 1 {
|
||||||
return nil, 0
|
return nil, 0, CollectionIndex{}
|
||||||
}
|
}
|
||||||
num_before = min(total-1, num_before_current)
|
num_before = min(total-1, num_before_current)
|
||||||
idx, did_wrap := s.increment_with_wrap_around(current, -num_before)
|
idx, did_wrap := s.increment_with_wrap_around(current, -num_before)
|
||||||
@@ -290,6 +378,7 @@ func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_co
|
|||||||
idx = CollectionIndex{}
|
idx = CollectionIndex{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
first_idx = idx
|
||||||
num_before = s.distance(idx, current)
|
num_before = s.distance(idx, current)
|
||||||
// fmt.Printf("111111 idx: %v current: %v num_before: %d\n", idx, current, num_before)
|
// fmt.Printf("111111 idx: %v current: %v num_before: %d\n", idx, current, num_before)
|
||||||
ans = make([][]*ResultItem, num_cols)
|
ans = make([][]*ResultItem, num_cols)
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func (m Mode) WindowTitle() string {
|
|||||||
|
|
||||||
type render_state struct {
|
type render_state struct {
|
||||||
num_matches, num_of_slots, num_before, num_per_column, num_columns int
|
num_matches, num_of_slots, num_before, num_per_column, num_columns int
|
||||||
|
first_idx CollectionIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
@@ -175,12 +176,14 @@ func (s *State) AddSelection(abspath string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) ToggleSelection(abspath string) {
|
func (s *State) ToggleSelection(abspath string) (added bool) {
|
||||||
before := len(s.selections)
|
before := len(s.selections)
|
||||||
s.selections = slices.DeleteFunc(s.selections, func(x string) bool { return x == abspath })
|
s.selections = slices.DeleteFunc(s.selections, func(x string) bool { return x == abspath })
|
||||||
if len(s.selections) == before {
|
if len(s.selections) == before {
|
||||||
s.selections = append(s.selections, abspath)
|
s.selections = append(s.selections, abspath)
|
||||||
|
added = true
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) IsSelected(x *ResultItem) bool {
|
func (s *State) IsSelected(x *ResultItem) bool {
|
||||||
@@ -279,15 +282,33 @@ func (h *Handler) current_abspath() string {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) toggle_selection() bool {
|
func (s *State) CanSelect(r *ResultItem) bool {
|
||||||
m := h.current_abspath()
|
return utils.IfElse(s.OnlyDirs(), r.ftype.IsDir(), !r.ftype.IsDir())
|
||||||
if m != "" {
|
}
|
||||||
h.state.ToggleSelection(m)
|
|
||||||
|
func (h *Handler) toggle_selection_at(idx CollectionIndex) bool {
|
||||||
|
matches, _ := h.get_results()
|
||||||
|
if r := matches.At(idx); r != nil && h.state.CanSelect(r) {
|
||||||
|
m := filepath.Join(h.state.CurrentDir(), r.text)
|
||||||
|
if added := h.state.ToggleSelection(m); added {
|
||||||
|
h.result_manager.last_click_anchor = &idx
|
||||||
|
} else {
|
||||||
|
h.result_manager.last_click_anchor = nil
|
||||||
|
if len(h.state.selections) > 0 {
|
||||||
|
x := utils.NewSetWithItems(h.state.selections...)
|
||||||
|
cdir := h.state.CurrentDir()
|
||||||
|
h.result_manager.last_click_anchor = matches.Closest(idx, func(q *ResultItem) bool { return x.Has(filepath.Join(cdir, q.text)) })
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) toggle_selection() bool {
|
||||||
|
return h.toggle_selection_at(h.state.CurrentIndex())
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) change_current_dir(dir string) {
|
func (h *Handler) change_current_dir(dir string) {
|
||||||
if dir != h.state.CurrentDir() {
|
if dir != h.state.CurrentDir() {
|
||||||
h.state.SetCurrentDir(dir)
|
h.state.SetCurrentDir(dir)
|
||||||
@@ -346,6 +367,31 @@ func (h *Handler) change_filter(delta int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) accept_idx(idx CollectionIndex) (accepted bool, err error) {
|
||||||
|
matches, _ := h.get_results()
|
||||||
|
if r := matches.At(idx); r != nil {
|
||||||
|
m := filepath.Join(h.state.CurrentDir(), r.text)
|
||||||
|
|
||||||
|
if h.state.mode.SelectFiles() {
|
||||||
|
var s os.FileInfo
|
||||||
|
if s, err = os.Stat(m); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if s.IsDir() {
|
||||||
|
return true, h.change_to_current_dir_if_possible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.state.AddSelection(m)
|
||||||
|
h.result_manager.last_click_anchor = &idx
|
||||||
|
if len(h.state.selections) > 0 {
|
||||||
|
return true, h.finish_selection()
|
||||||
|
}
|
||||||
|
return true, h.draw_screen()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) dispatch_action(name, args string) (err error) {
|
func (h *Handler) dispatch_action(name, args string) (err error) {
|
||||||
switch name {
|
switch name {
|
||||||
case "quit":
|
case "quit":
|
||||||
@@ -380,26 +426,13 @@ func (h *Handler) dispatch_action(name, args string) (err error) {
|
|||||||
return h.draw_screen()
|
return h.draw_screen()
|
||||||
}
|
}
|
||||||
case "accept":
|
case "accept":
|
||||||
m := h.current_abspath()
|
accepted, aerr := h.accept_idx(h.state.CurrentIndex())
|
||||||
if m == "" {
|
if aerr != nil {
|
||||||
|
return aerr
|
||||||
|
}
|
||||||
|
if !accepted {
|
||||||
h.lp.Beep()
|
h.lp.Beep()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if h.state.mode.SelectFiles() {
|
|
||||||
var s os.FileInfo
|
|
||||||
if s, err = os.Stat(m); err != nil {
|
|
||||||
h.lp.Beep()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if s.IsDir() {
|
|
||||||
return h.change_to_current_dir_if_possible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.state.AddSelection(m)
|
|
||||||
if len(h.state.selections) > 0 {
|
|
||||||
return h.finish_selection()
|
|
||||||
}
|
|
||||||
return h.draw_screen()
|
|
||||||
case "toggle":
|
case "toggle":
|
||||||
switch args {
|
switch args {
|
||||||
case "dotfiles":
|
case "dotfiles":
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ map('Quit', 'quit esc quit')
|
|||||||
map('Quit', 'quit ctrl+c quit')
|
map('Quit', 'quit ctrl+c quit')
|
||||||
|
|
||||||
map('Accept current result', 'accept enter accept')
|
map('Accept current result', 'accept enter accept')
|
||||||
map('Select current result', 'select shift+enter select')
|
map('Select current result', 'select shift+enter select', long_text='''
|
||||||
|
When selecting multiple files, this will add the current file to the list of selected files.
|
||||||
|
You can also toggle the selected status of a file by holding down the :kbd:`Ctrl` key and clicking on
|
||||||
|
it. Similarly, the :kbd:`Alt` key can be held to click and extend the range of selected files.
|
||||||
|
''')
|
||||||
|
|
||||||
map('Next result', 'next_result down next 1')
|
map('Next result', 'next_result down next 1')
|
||||||
map('Previous result', 'prev_result up next -1')
|
map('Previous result', 'prev_result up next -1')
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty/tools/icons"
|
"github.com/kovidgoyal/kitty/tools/icons"
|
||||||
|
"github.com/kovidgoyal/kitty/tools/tui"
|
||||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||||
@@ -123,7 +125,7 @@ func icon_for(path string, x os.FileMode) string {
|
|||||||
return ans
|
return ans
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x, available_width int) {
|
func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x, y, available_width, colnum int) {
|
||||||
root_dir := h.state.CurrentDir()
|
root_dir := h.state.CurrentDir()
|
||||||
for i, m := range matches {
|
for i, m := range matches {
|
||||||
h.lp.QueueWriteString("\r")
|
h.lp.QueueWriteString("\r")
|
||||||
@@ -137,9 +139,11 @@ func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x
|
|||||||
}
|
}
|
||||||
text := m.text
|
text := m.text
|
||||||
add_ellipsis := false
|
add_ellipsis := false
|
||||||
if wcswidth.Stringwidth(text) > available_width-3 {
|
width := wcswidth.Stringwidth(text)
|
||||||
|
if width > available_width-3 {
|
||||||
text = wcswidth.TruncateToVisualLength(text, available_width-4)
|
text = wcswidth.TruncateToVisualLength(text, available_width-4)
|
||||||
add_ellipsis = true
|
add_ellipsis = true
|
||||||
|
width = available_width - 3
|
||||||
}
|
}
|
||||||
is_current := i == current_idx
|
is_current := i == current_idx
|
||||||
if is_current {
|
if is_current {
|
||||||
@@ -153,6 +157,54 @@ func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x
|
|||||||
}
|
}
|
||||||
h.render_match_with_positions(text, add_ellipsis, m.sorted_positions(), is_current)
|
h.render_match_with_positions(text, add_ellipsis, m.sorted_positions(), is_current)
|
||||||
h.lp.MoveCursorVertically(1)
|
h.lp.MoveCursorVertically(1)
|
||||||
|
cr := h.state.mouse_state.AddCellRegion(fmt.Sprintf("result-%d-%d", colnum, i), x, y-1+i, x+width+2, y-1+i)
|
||||||
|
cr.HoverStyle = HOVER_STYLE
|
||||||
|
var data struct {
|
||||||
|
colnum, i int
|
||||||
|
}
|
||||||
|
data.colnum, data.i = colnum, i
|
||||||
|
cr.OnClickEvent = func(id string, ev *loop.MouseEvent, cell_offset tui.Point) error {
|
||||||
|
if ev.Buttons&loop.LEFT_MOUSE_BUTTON == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctrl_mod := utils.IfElse(runtime.GOOS == "darwin", loop.SUPER, loop.CTRL)
|
||||||
|
mods := ev.Mods & (ctrl_mod | loop.ALT) // shift alone and ctrl+shift are used for kitty bindings
|
||||||
|
matches, _ := h.get_results()
|
||||||
|
num_before := h.state.last_render.num_of_slots*data.colnum + data.i
|
||||||
|
idx, did_wrap := matches.IncrementIndexWithWrapAroundAndCheck(h.state.last_render.first_idx, num_before)
|
||||||
|
if did_wrap {
|
||||||
|
h.lp.Beep()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d := matches.SignedDistance(idx, h.state.current_idx)
|
||||||
|
h.state.SetCurrentIndex(idx)
|
||||||
|
h.state.last_render.num_before = max(0, h.state.last_render.num_before+d)
|
||||||
|
switch mods {
|
||||||
|
case 0:
|
||||||
|
h.dispatch_action("accept", "")
|
||||||
|
case ctrl_mod, ctrl_mod | loop.ALT:
|
||||||
|
h.dispatch_action("select", "")
|
||||||
|
case loop.ALT:
|
||||||
|
r := matches.At(idx)
|
||||||
|
if (r != nil && h.state.IsSelected(r)) || h.result_manager.last_click_anchor == nil {
|
||||||
|
h.dispatch_action("select", "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
already_selected := utils.NewSetWithItems(h.state.selections...)
|
||||||
|
cdir := h.state.CurrentDir()
|
||||||
|
matches.Apply(idx, *h.result_manager.last_click_anchor, func(r *ResultItem) bool {
|
||||||
|
m := filepath.Join(cdir, r.text)
|
||||||
|
if !already_selected.Has(m) && h.state.CanSelect(r) {
|
||||||
|
already_selected.Add(m)
|
||||||
|
h.state.selections = append(h.state.selections, m)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return h.draw_screen()
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,14 +226,15 @@ func (h *Handler) draw_list_of_results(matches *SortedResults, y, height int) in
|
|||||||
}
|
}
|
||||||
return num_cols
|
return num_cols
|
||||||
}
|
}
|
||||||
columns, num_before := matches.SplitIntoColumns(calc_num_cols, height, h.state.last_render.num_before, h.state.CurrentIndex())
|
columns, num_before, first_idx := matches.SplitIntoColumns(calc_num_cols, height, h.state.last_render.num_before, h.state.CurrentIndex())
|
||||||
h.state.last_render.num_before = num_before
|
h.state.last_render.num_before = num_before
|
||||||
h.state.last_render.num_per_column = height
|
h.state.last_render.num_per_column = height
|
||||||
h.state.last_render.num_columns = num_cols
|
h.state.last_render.num_columns = num_cols
|
||||||
|
h.state.last_render.first_idx = first_idx
|
||||||
x := 1
|
x := 1
|
||||||
for _, col := range columns {
|
for i, col := range columns {
|
||||||
h.lp.MoveCursorTo(x, y)
|
h.lp.MoveCursorTo(x, y)
|
||||||
h.draw_column_of_matches(col, num_before, x, col_width-1)
|
h.draw_column_of_matches(col, num_before, x, y, col_width-1, i)
|
||||||
num_before -= height
|
num_before -= height
|
||||||
x += col_width
|
x += col_width
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -640,6 +640,8 @@ type ResultManager struct {
|
|||||||
scorer *FileSystemScorer
|
scorer *FileSystemScorer
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
last_wakeup_at time.Time
|
last_wakeup_at time.Time
|
||||||
|
|
||||||
|
last_click_anchor *CollectionIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResultManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *ResultManager {
|
func NewResultManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *ResultManager {
|
||||||
@@ -658,6 +660,7 @@ func (m *ResultManager) new_scorer() {
|
|||||||
m.scorer.respect_ignores = m.settings.RespectIgnores()
|
m.scorer.respect_ignores = m.settings.RespectIgnores()
|
||||||
m.scorer.show_hidden = m.settings.ShowHidden()
|
m.scorer.show_hidden = m.settings.ShowHidden()
|
||||||
m.scorer.global_ignore = m.settings.GlobalIgnores()
|
m.scorer.global_ignore = m.settings.GlobalIgnores()
|
||||||
|
m.last_click_anchor = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ResultManager) on_results(err error, is_finished bool) {
|
func (m *ResultManager) on_results(err error, is_finished bool) {
|
||||||
@@ -709,6 +712,7 @@ func (m *ResultManager) set_something(callback func()) {
|
|||||||
m.new_scorer()
|
m.new_scorer()
|
||||||
m.scorer.Start()
|
m.scorer.Start()
|
||||||
} else {
|
} else {
|
||||||
|
m.last_click_anchor = nil
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user