feat(ui): фильтр уровней логов в панели «Логи»

This commit is contained in:
ki.sagidullin
2026-08-21 21:35:37 +05:00
parent ee76f9d89f
commit a762a1df80
7 changed files with 509 additions and 45 deletions

View File

@@ -3,6 +3,8 @@
package desktop
import (
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
@@ -10,40 +12,121 @@ import (
"github.com/kamelion/ratatoskr-go/internal/ui"
)
// LogPanel — Fyne-реализация ui.LogPanel (спец 12.8, 12.9): поток системных логов.
//
// Внутри — widget.Label с ограниченным буфером (maxLogLen); Append обрезает
// строку до лимита. Вызовы Append приходят из горутин Hub, поэтому обновление
// виджета выполняется через fyne.Do (на потоке Fyne).
type LogPanel struct {
label *widget.Label
}
// maxLogLen — обрезка буфера панели «Логи», чтобы не расти бесконечно.
const maxLogLen = 200_000
// NewLogPanel создаёт LogPanel.
// 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 {
l := widget.NewLabel("")
l.Wrapping = fyne.TextWrapWord
return &LogPanel{label: l}
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 возвращает контейнер панели для встраивания в окно (таб «Логи»).
// Widget возвращает контейнер панели для встраивания в окно (таб «Логи»):
// строка фильтра сверху, лента лога в скролле снизу.
func (p *LogPanel) Widget() fyne.CanvasObject {
return container.NewScroll(p.label)
return container.NewBorder(
container.NewHBox(p.btn), nil, nil, nil,
container.NewScroll(p.label),
)
}
// Append добавляет строку лога (спец 12.9), с ограничением буфера.
func (p *LogPanel) Append(text string) {
// Append добавляет запись лога (спец 12.9), с ограничением буфера.
// Вызывается из горутин Hub → вся работа на потоке Fyne через fyne.Do.
func (p *LogPanel) Append(level, text string) {
fyne.Do(func() {
s := p.label.Text + text + "\n"
if len(s) > maxLogLen {
s = s[len(s)-maxLogLen:]
lvl := ui.NormalizeLogLevel(level)
dropped := p.buf.Append(lvl, text)
// Перерисовка нужна, если запись видима или вытеснение задело ленту.
if p.selected[lvl] || dropped {
p.render()
}
p.label.SetText(s)
})
}
// 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)
var _ ui.LogPanel = (*LogPanel)(nil)