173 lines
4.2 KiB
Go
173 lines
4.2 KiB
Go
package ui
|
|
|
|
import (
|
|
"os/exec"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
"viewsagent/internal/logging"
|
|
)
|
|
|
|
// logWindow 是独立的「运行日志」窗口。
|
|
//
|
|
// 程序是打包成无控制台窗口的桌面应用,日志不再往 cmd 输出,
|
|
// 想看的时候点工具栏的日志按钮,这个窗口就会单独弹出来。
|
|
type logWindow struct {
|
|
buf *logging.Buffer
|
|
win fyne.Window
|
|
list *widget.List
|
|
lines []string
|
|
|
|
// shown 是界面上已经显示的日志版本号,用原子变量跨 goroutine 传递。
|
|
shown atomic.Uint64
|
|
// stop 用来结束自动刷新的后台协程。
|
|
stop chan struct{}
|
|
}
|
|
|
|
// newLogWindow 创建运行日志窗口。logDir 是日志文件所在目录。
|
|
func newLogWindow(a fyne.App, buf *logging.Buffer, logDir string) *logWindow {
|
|
w := &logWindow{buf: buf}
|
|
|
|
w.win = a.NewWindow("运行日志")
|
|
w.win.Resize(fyne.NewSize(900, 560))
|
|
// 点关闭只是隐藏窗口,方便下次再打开。
|
|
w.win.SetCloseIntercept(func() {
|
|
w.stopTicker()
|
|
w.win.Hide()
|
|
})
|
|
w.win.SetContent(w.buildContent(logDir))
|
|
return w
|
|
}
|
|
|
|
// buildContent 组装窗口内容:上面一排按钮,下面是日志列表。
|
|
func (w *logWindow) buildContent(logDir string) fyne.CanvasObject {
|
|
w.list = widget.NewList(
|
|
func() int { return len(w.lines) },
|
|
func() fyne.CanvasObject {
|
|
l := widget.NewLabel("")
|
|
l.TextStyle = fyne.TextStyle{Monospace: true}
|
|
l.Truncation = fyne.TextTruncateOff
|
|
return l
|
|
},
|
|
func(id widget.ListItemID, obj fyne.CanvasObject) {
|
|
l, ok := obj.(*widget.Label)
|
|
if !ok {
|
|
return
|
|
}
|
|
if id < 0 || id >= len(w.lines) {
|
|
l.SetText("")
|
|
return
|
|
}
|
|
l.SetText(w.lines[id])
|
|
},
|
|
)
|
|
|
|
refreshBtn := widget.NewButtonWithIcon("刷新", theme.ViewRefreshIcon(), func() { w.refresh() })
|
|
copyBtn := widget.NewButtonWithIcon("复制全部", theme.ContentCopyIcon(), func() { w.copyAll() })
|
|
clearBtn := widget.NewButtonWithIcon("清空", theme.DeleteIcon(), func() { w.clear() })
|
|
dirBtn := widget.NewButtonWithIcon("打开日志目录", theme.FolderOpenIcon(), func() { openDir(logDir) })
|
|
|
|
bar := container.NewHBox(refreshBtn, copyBtn, clearBtn, layout.NewSpacer(), dirBtn)
|
|
hint := widget.NewLabel("日志同时写入该目录下的 app.log 文件,重启程序后仍可查看。")
|
|
hint.Importance = widget.LowImportance
|
|
hint.Wrapping = fyne.TextWrapWord
|
|
|
|
return container.NewBorder(bar, hint, nil, nil, w.list)
|
|
}
|
|
|
|
// show 显示日志窗口(已经显示时只刷新内容并置前)。
|
|
func (w *logWindow) show() {
|
|
if w.win == nil {
|
|
return
|
|
}
|
|
w.refresh()
|
|
w.win.Show()
|
|
w.win.RequestFocus()
|
|
w.startTicker()
|
|
}
|
|
|
|
// refresh 把最新日志刷到列表里,并滚动到底部。
|
|
// 只能在 UI 线程调用。
|
|
func (w *logWindow) refresh() {
|
|
if w.list == nil {
|
|
return
|
|
}
|
|
w.lines = w.buf.Lines()
|
|
w.shown.Store(w.buf.Seq())
|
|
w.list.Refresh()
|
|
w.list.ScrollToBottom()
|
|
}
|
|
|
|
// copyAll 把全部日志复制到剪贴板。
|
|
func (w *logWindow) copyAll() {
|
|
text := w.buf.Text()
|
|
if text == "" {
|
|
return
|
|
}
|
|
fyne.CurrentApp().Clipboard().SetContent(text)
|
|
}
|
|
|
|
// clear 清空内存里的日志显示(磁盘上的文件不受影响)。
|
|
func (w *logWindow) clear() {
|
|
w.buf.Clear()
|
|
w.refresh()
|
|
}
|
|
|
|
// startTicker 启动自动刷新:每秒检查一次有没有新日志。
|
|
func (w *logWindow) startTicker() {
|
|
if w.stop != nil {
|
|
return
|
|
}
|
|
stop := make(chan struct{})
|
|
w.stop = stop
|
|
|
|
go func() {
|
|
t := time.NewTicker(time.Second)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-t.C:
|
|
if w.buf.Seq() == w.shown.Load() {
|
|
continue
|
|
}
|
|
fyne.Do(w.refresh)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// stopTicker 停止自动刷新,窗口隐藏时调用。
|
|
func (w *logWindow) stopTicker() {
|
|
if w.stop == nil {
|
|
return
|
|
}
|
|
close(w.stop)
|
|
w.stop = nil
|
|
}
|
|
|
|
// close 停掉后台刷新,程序退出时调用。
|
|
// 退出后 fyne.Do 不再走主线程队列,回调会直接执行,
|
|
// 这时再去刷界面已经没有意义了。
|
|
func (w *logWindow) close() {
|
|
if w == nil {
|
|
return
|
|
}
|
|
w.stopTicker()
|
|
}
|
|
|
|
// openDir 用资源管理器打开目录。
|
|
func openDir(dir string) {
|
|
if dir == "" {
|
|
return
|
|
}
|
|
_ = exec.Command("explorer", dir).Start()
|
|
}
|