133 lines
5.3 KiB
Go
133 lines
5.3 KiB
Go
//go:build cgo
|
||
|
||
package desktop
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/container"
|
||
"fyne.io/fyne/v2/widget"
|
||
|
||
"github.com/kamelion/ratatoskr-go/internal/ui"
|
||
)
|
||
|
||
// maxLogLen — обрезка буфера панели «Логи», чтобы не расти бесконечно.
|
||
const maxLogLen = 200_000
|
||
|
||
// LogPanel — Fyne-реализация ui.LogPanel (спец 12.8, 12.9): поток системных
|
||
// логов с фильтром по уровням (error/warning/info/debug).
|
||
//
|
||
// Записи хранятся в ui.LogBuffer (ограниченный буфер); лента показывает
|
||
// только записи уровней, отмеченных в фильтре (по умолчанию отмечены все).
|
||
// Фильтр — выпадающий список с чекбоксами: кнопка над лентой открывает
|
||
// widget.PopUp с widget.Check по каждому уровню; изменение набора сразу
|
||
// перерисовывает ленту — и по уже загруженным записям, и для новых.
|
||
//
|
||
// Состояние (buf/selected) доступается только с потока Fyne: Append приходит
|
||
// из горутин Hub и заворачивается в fyne.Do, колбэки чекбоксов Fyne вызывает
|
||
// сам на своём потоке.
|
||
type LogPanel struct {
|
||
label *widget.Label
|
||
btn *widget.Button // кнопка-дропдаун фильтра уровней
|
||
|
||
buf *ui.LogBuffer // буфер записей, старые вытесняются
|
||
selected map[string]bool // уровень → показывать (по умолчанию все)
|
||
}
|
||
|
||
// NewLogPanel создаёт LogPanel со всеми включёнными уровнями.
|
||
func NewLogPanel() *LogPanel {
|
||
p := &LogPanel{
|
||
buf: ui.NewLogBuffer(maxLogLen),
|
||
selected: make(map[string]bool, len(ui.LogLevels)),
|
||
}
|
||
for _, lvl := range ui.LogLevels {
|
||
p.selected[lvl] = true
|
||
}
|
||
|
||
p.label = widget.NewLabel("")
|
||
p.label.Wrapping = fyne.TextWrapWord
|
||
p.btn = widget.NewButton(p.filterCaption(), p.showFilterPopUp)
|
||
return p
|
||
}
|
||
|
||
// Widget возвращает контейнер панели для встраивания в окно (таб «Логи»):
|
||
// строка фильтра сверху, лента лога в скролле снизу.
|
||
func (p *LogPanel) Widget() fyne.CanvasObject {
|
||
return container.NewBorder(
|
||
container.NewHBox(p.btn), nil, nil, nil,
|
||
container.NewScroll(p.label),
|
||
)
|
||
}
|
||
|
||
// Append добавляет запись лога (спец 12.9), с ограничением буфера.
|
||
// Вызывается из горутин Hub → вся работа на потоке Fyne через fyne.Do.
|
||
func (p *LogPanel) Append(level, text string) {
|
||
fyne.Do(func() {
|
||
lvl := ui.NormalizeLogLevel(level)
|
||
dropped := p.buf.Append(lvl, text)
|
||
// Перерисовка нужна, если запись видима или вытеснение задело ленту.
|
||
if p.selected[lvl] || dropped {
|
||
p.render()
|
||
}
|
||
})
|
||
}
|
||
|
||
// render перестраивает текст ленты из буфера с учётом выбранного набора
|
||
// уровней. Пустой набор → пустая лента (без ошибок).
|
||
func (p *LogPanel) render() {
|
||
p.label.SetText(p.buf.Render(p.selected))
|
||
}
|
||
|
||
// setLevel включает/выключает показ уровня и сразу обновляет ленту.
|
||
// Вызывается только с потока Fyne (колбэки чекбоксов).
|
||
func (p *LogPanel) setLevel(level string, on bool) {
|
||
if p.selected[level] == on {
|
||
return
|
||
}
|
||
p.selected[level] = on
|
||
p.btn.SetText(p.filterCaption())
|
||
p.render()
|
||
}
|
||
|
||
// filterCaption — подпись кнопки-дропдауна: текущий выбранный набор уровней.
|
||
func (p *LogPanel) filterCaption() string {
|
||
var chosen []string
|
||
for _, lvl := range ui.LogLevels {
|
||
if p.selected[lvl] {
|
||
chosen = append(chosen, lvl)
|
||
}
|
||
}
|
||
switch {
|
||
case len(chosen) == len(ui.LogLevels):
|
||
return "Уровни: все"
|
||
case len(chosen) == 0:
|
||
return "Уровни: нет"
|
||
default:
|
||
return "Уровни: " + strings.Join(chosen, "+")
|
||
}
|
||
}
|
||
|
||
// showFilterPopUp открывает выпадающий список с чекбоксами уровней под
|
||
// кнопкой фильтра. Список пересобирается при каждом открытии — чекбоксы
|
||
// всегда отражают актуальное состояние фильтра.
|
||
func (p *LogPanel) showFilterPopUp() {
|
||
c := fyne.CurrentApp().Driver().CanvasForObject(p.btn)
|
||
if c == nil {
|
||
return // виджет ещё не встроен в окно
|
||
}
|
||
rows := make([]fyne.CanvasObject, 0, len(ui.LogLevels))
|
||
for _, lvl := range ui.LogLevels {
|
||
lvl := lvl
|
||
cb := widget.NewCheck(lvl, func(on bool) { p.setLevel(lvl, on) })
|
||
cb.SetChecked(p.selected[lvl])
|
||
rows = append(rows, cb)
|
||
}
|
||
pop := widget.NewPopUp(container.NewPadded(container.NewVBox(rows...)), c)
|
||
pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(p.btn)
|
||
pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+p.btn.Size().Height))
|
||
}
|
||
|
||
// compile-time проверка реализации контракта.
|
||
var _ ui.LogPanel = (*LogPanel)(nil)
|