10 Commits

Author SHA1 Message Date
messypy
6d318af329 Give settings export controls more space
All checks were successful
build-windows-exe / build (push) Successful in 1m46s
2026-07-28 00:31:42 +09:00
messypy
a017170c71 Align settings controls
All checks were successful
build-windows-exe / build (push) Successful in 2m9s
2026-07-28 00:20:57 +09:00
messypy
d38d2edee3 Remove duplicate font size buttons
All checks were successful
build-windows-exe / build (push) Successful in 2m5s
2026-07-28 00:14:04 +09:00
messypy
01d5799914 Fix settings panel control overlap
All checks were successful
build-windows-exe / build (push) Successful in 2m28s
2026-07-28 00:05:41 +09:00
messypy
f9968895ac Allow smaller GUI font sizes
All checks were successful
build-windows-exe / build (push) Successful in 1m31s
2026-07-27 23:57:57 +09:00
messypy
452c7c59b6 Include full day details in history export
All checks were successful
build-windows-exe / build (push) Successful in 1m4s
2026-07-27 23:53:00 +09:00
messypy
b059c91388 Enable custom window control buttons
All checks were successful
build-windows-exe / build (push) Successful in 1m4s
2026-07-27 23:45:09 +09:00
messypy
289337f016 Keep Win32 GUI on one OS thread
All checks were successful
build-windows-exe / build (push) Successful in 1m35s
2026-07-27 23:36:06 +09:00
messypy
dadc65065c Load GUI history asynchronously
All checks were successful
build-windows-exe / build (push) Successful in 1m12s
2026-07-27 23:32:17 +09:00
messypy
4df1a5d71a Limit release workflow tests to internal packages
All checks were successful
build-windows-exe / build (push) Successful in 1m32s
2026-07-27 23:16:14 +09:00
5 changed files with 413 additions and 75 deletions

View File

@@ -25,7 +25,7 @@ jobs:
- name: Test
shell: bash
run: |
GO111MODULE=on go test ./...
GO111MODULE=on go test ./internal/...
- name: Build executables
shell: bash

View File

@@ -89,6 +89,22 @@ type historyExportResult struct {
SelectedCount int
}
type historyRefreshResult struct {
Rows []guiWorldVisitRow
CalendarCells []historyCalendarCell
DetailRows []historyDetailRow
DetailHeightPx int32
SelectedDate time.Time
Month time.Time
SelectedKeys map[string]bool
Status string
Regex string
FromDate string
ToDate string
Err error
Elapsed time.Duration
}
var historyEventsCacheMu sync.Mutex
var historyEventsCache struct {
jsonMod time.Time
@@ -106,12 +122,8 @@ func ensureHistoryState(app *guiApp) {
app.historySelectedKeys = map[string]bool{}
}
if app.historySelectedDate.IsZero() {
if latest := latestHistoryDate(); !latest.IsZero() {
app.historySelectedDate = latest
} else {
app.historySelectedDate = time.Now()
}
}
if app.historyMonth.IsZero() {
app.historyMonth = time.Date(app.historySelectedDate.Year(), app.historySelectedDate.Month(), 1, 0, 0, 0, 0, app.historySelectedDate.Location())
}
@@ -579,9 +591,11 @@ func exportHistoryDay(app *guiApp, day time.Time, selected map[string]bool) (str
}
func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
events := historyEventsFromSnapshot()
visits := historyVisitSource()
rows := append([]historyDetailRow(nil), req.Rows...)
if len(rows) == 0 {
rows = historyVisitsForDayFromData(nil, req.Day, historyVisitSource(), historyEventsFromSnapshot(), req.Query)
rows = historyVisitsForDayFromData(nil, req.Day, visits, events, req.Query)
}
if len(req.Selected) > 0 {
filtered := rows[:0]
@@ -667,6 +681,9 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
}
}
}
appendExportDayVisits(&b, req.Day, visits, req.Query)
appendExportDayEvents(&b, req.Day, events, req.Query)
appendExportGuestSnapshot(&b, req.Day, req.Query)
exportDir := runtimeDir()
if strings.TrimSpace(req.ExportDir) != "" {
exportDir = normalizeHistoryExportDir(req.ExportDir)
@@ -684,6 +701,153 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
return path, nil
}
func appendExportDayVisits(b *strings.Builder, day time.Time, visits []historyVisitRecord, q historyQuery) {
if b == nil {
return
}
dayStart, dayEnd := historyDayBounds(day)
matched := make([]historyVisitRecord, 0, len(visits))
for _, visit := range visits {
start := visit.Start
if start.IsZero() {
continue
}
end := visit.End
if end.IsZero() {
end = time.Now()
}
if !start.Before(dayEnd) || !end.After(dayStart) {
continue
}
if q.Pattern != nil && !q.Pattern.MatchString(visit.WorldLabel) && !q.Pattern.MatchString(visit.WorldID) && !q.Pattern.MatchString(visit.InstanceID) {
continue
}
matched = append(matched, visit)
}
sort.SliceStable(matched, func(i, j int) bool {
return matched[i].Start.Before(matched[j].Start)
})
b.WriteString("\nAll visits:\n")
if len(matched) == 0 {
b.WriteString("(no visits)\n")
return
}
for _, visit := range matched {
b.WriteString("- ")
b.WriteString(historyTimeRangeLabel(visit.Start, visit.End, visit.Current))
b.WriteString(" ")
b.WriteString(strings.TrimSpace(visit.WorldLabel))
if strings.TrimSpace(visit.WorldID) != "" {
b.WriteString(" world_id=")
b.WriteString(strings.TrimSpace(visit.WorldID))
}
if strings.TrimSpace(visit.InstanceID) != "" {
b.WriteString(" instance_id=")
b.WriteString(strings.TrimSpace(visit.InstanceID))
}
b.WriteString("\n")
}
}
func appendExportDayEvents(b *strings.Builder, day time.Time, events []historyEventRecord, q historyQuery) {
if b == nil {
return
}
dayStart, dayEnd := historyDayBounds(day)
matched := make([]historyEventRecord, 0, len(events))
for _, ev := range events {
if ev.At.IsZero() || ev.At.Before(dayStart) || !ev.At.Before(dayEnd) {
continue
}
raw := fmt.Sprintf("[%s] %s %s", ev.At.Format("15:04"), ev.Kind, ev.Name)
if q.Pattern != nil && !q.Pattern.MatchString(ev.Name) && !q.Pattern.MatchString(raw) {
continue
}
matched = append(matched, ev)
}
sort.SliceStable(matched, func(i, j int) bool {
if matched[i].At.Equal(matched[j].At) {
return strings.ToLower(matched[i].Name) < strings.ToLower(matched[j].Name)
}
return matched[i].At.Before(matched[j].At)
})
b.WriteString("\nAll join/leave events:\n")
if len(matched) == 0 {
b.WriteString("(no events)\n")
return
}
for _, ev := range matched {
b.WriteString("- ")
b.WriteString(ev.At.Format("15:04"))
b.WriteString(" ")
b.WriteString(ev.Kind)
b.WriteString(" ")
b.WriteString(strings.TrimSpace(ev.Name))
if ev.Raw != "" {
b.WriteString(" | ")
b.WriteString(strings.TrimSpace(ev.Raw))
}
b.WriteString("\n")
}
}
func appendExportGuestSnapshot(b *strings.Builder, day time.Time, q historyQuery) {
if b == nil {
return
}
guests := readGuestSnapshot()
dayStart, dayEnd := historyDayBounds(day)
type guestLine struct {
name string
present bool
join time.Time
leave time.Time
}
lines := make([]guestLine, 0, len(guests))
for _, guest := range guests {
name := strings.TrimSpace(guest.Name)
if name == "" {
continue
}
inDay := guest.Present ||
(!guest.LastJoin.IsZero() && !guest.LastJoin.Before(dayStart) && guest.LastJoin.Before(dayEnd)) ||
(!guest.LastLeave.IsZero() && !guest.LastLeave.Before(dayStart) && guest.LastLeave.Before(dayEnd))
if !inDay {
continue
}
if q.Pattern != nil && !q.Pattern.MatchString(name) {
continue
}
lines = append(lines, guestLine{name: name, present: guest.Present, join: guest.LastJoin, leave: guest.LastLeave})
}
sort.SliceStable(lines, func(i, j int) bool {
return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name)
})
b.WriteString("\nGuest snapshot:\n")
if len(lines) == 0 {
b.WriteString("(no guests)\n")
return
}
for _, line := range lines {
b.WriteString("- ")
if line.present {
b.WriteString("present ")
} else {
b.WriteString("left ")
}
b.WriteString(line.name)
if !line.join.IsZero() {
b.WriteString(" join=")
b.WriteString(line.join.Format("15:04"))
}
if !line.leave.IsZero() {
b.WriteString(" leave=")
b.WriteString(line.leave.Format("15:04"))
}
b.WriteString("\n")
}
}
func cloneHistorySelection(selected map[string]bool) map[string]bool {
if len(selected) == 0 {
return nil
@@ -757,6 +921,124 @@ func refreshHistoryCache(app *guiApp, force bool) bool {
return true
}
func startHistoryRefresh(app *guiApp, force bool) bool {
if app == nil || app.historyRefreshRunning {
return false
}
if !force && app.historyCacheLoaded && !app.historyReloadPending {
return false
}
ensureHistoryState(app)
if app.historyRefreshResultCh == nil {
app.historyRefreshResultCh = make(chan historyRefreshResult, 1)
}
work := *app
work.historySelectedKeys = cloneHistorySelection(app.historySelectedKeys)
work.historyRefreshResultCh = nil
work.historyExportResultCh = nil
app.historyRefreshRunning = true
app.historyReloadPending = true
if strings.TrimSpace(app.historyStatus) == "" {
app.historyStatus = "reloading..."
}
hwnd := app.hwnd
go func(ch chan<- historyRefreshResult, work guiApp, hwnd uintptr) {
start := time.Now()
result := historyRefreshResult{
SelectedDate: work.historySelectedDate,
Month: work.historyMonth,
SelectedKeys: cloneHistorySelection(work.historySelectedKeys),
Regex: work.historyRegex,
FromDate: work.historyFromDate,
ToDate: work.historyToDate,
}
defer func() {
result.Elapsed = time.Since(start)
if r := recover(); r != nil {
result.Err = fmt.Errorf("refresh panic: %v", r)
}
select {
case ch <- result:
default:
}
if postMessageProc != nil && hwnd != 0 {
postMessageProc.Call(hwnd, uintptr(wmHistoryRefreshDone), 0, 0)
}
}()
refreshHistoryCache(&work, true)
result.Rows = append([]guiWorldVisitRow(nil), work.historyRows...)
result.CalendarCells = append([]historyCalendarCell(nil), work.historyCalendarCells...)
result.DetailRows = cloneHistoryDetailRows(work.historyDetailRows)
result.DetailHeightPx = work.historyDetailContentHeightPx
result.SelectedDate = work.historySelectedDate
result.Month = work.historyMonth
result.SelectedKeys = cloneHistorySelection(work.historySelectedKeys)
result.Status = work.historyStatus
}(app.historyRefreshResultCh, work, hwnd)
guiLog("history refresh start")
return true
}
func drainHistoryRefreshResult(app *guiApp) bool {
if app == nil || app.historyRefreshResultCh == nil || !app.historyRefreshRunning {
return false
}
handled := false
for {
select {
case res := <-app.historyRefreshResultCh:
handled = true
app.historyRefreshRunning = false
if res.Err != nil {
app.historyStatus = "reload failed: " + res.Err.Error()
app.historyReloadPending = true
guiLog(fmt.Sprintf("history refresh failed elapsed=%s err=%v", res.Elapsed, res.Err))
invalidateHistoryPanes(app)
continue
}
stale := !sameDay(app.historySelectedDate, res.SelectedDate) ||
!sameHistoryMonth(app.historyMonth, res.Month) ||
app.historyRegex != res.Regex ||
app.historyFromDate != res.FromDate ||
app.historyToDate != res.ToDate
if stale {
app.historyReloadPending = true
guiLog("history refresh stale; scheduling another refresh")
invalidateHistoryPanes(app)
continue
}
app.historyRows = res.Rows
app.historyCalendarCells = res.CalendarCells
app.historyDetailRows = res.DetailRows
app.historyDetailContentHeightPx = res.DetailHeightPx
app.historySelectedDate = res.SelectedDate
app.historyMonth = res.Month
app.historySelectedKeys = res.SelectedKeys
if app.historySelectedKeys == nil {
app.historySelectedKeys = map[string]bool{}
}
app.historyReloadPending = false
app.historyCacheLoaded = true
if strings.TrimSpace(res.Status) != "" {
app.historyStatus = res.Status
} else {
app.historyStatus = "reloaded: " + time.Now().Format("15:04:05")
}
guiLog(fmt.Sprintf("history refresh ok elapsed=%s rows=%d details=%d", res.Elapsed, len(res.Rows), len(res.DetailRows)))
invalidateHistoryPanes(app)
default:
return handled
}
}
}
func sameHistoryMonth(a, b time.Time) bool {
if a.IsZero() || b.IsZero() {
return a.IsZero() && b.IsZero()
}
return a.Year() == b.Year() && a.Month() == b.Month()
}
func requestHistoryRefresh(app *guiApp, reason string) {
if app == nil {
return

View File

@@ -56,6 +56,8 @@ func normalizeInitialTab(tab string) string {
switch strings.ToLower(strings.TrimSpace(tab)) {
case "join", "log", "logs":
return "join"
case "history", "hist":
return "history"
case "translate", "translation":
return "join"
case "settings", "setting", "config":

View File

@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"sort"
"strconv"
@@ -52,6 +53,7 @@ const (
wmCommandGUI = 0x0111
wmSetFont = 0x0030
wmHistoryExportDone = 0x0401
wmHistoryRefreshDone = 0x0402
enChange = 0x0300
enKillFocus = 0x0200
bnClicked = 0x0000
@@ -100,7 +102,7 @@ const (
leftPaneHeight = 560
rightPaneWidth = windowWidth - leftPaneWidth
settingsPaneWidth = rightPaneWidth
settingsPaneHeight = leftPaneHeight
settingsPaneHeight = 700
clrMainBg = 0x00311c0b
clrHeaderBg = 0x004d2c08
clrNavBg = 0x0024150b
@@ -119,6 +121,12 @@ const (
const translateTabEnabled = false
const (
defaultGUIFontSize = 14
minGUIFontSize = 8
maxGUIFontSize = 30
)
var joinLeaveLinePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`)
var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]`)
var worldIdPattern = regexp.MustCompile(`worldId=(wrld_[0-9a-fA-F-]+)`)
@@ -138,6 +146,7 @@ var hwndNoZOrder uintptr = 0x0004
var hwndNoSize uintptr = 0x0001
var hwndNoActivate uintptr = 0x0010
var hwndShowWindow uintptr = 0x0040
var swMinimize uintptr = 6
var swRestore uintptr = 9
var swHideControl uintptr = 0
var swShowControl uintptr = 5
@@ -219,8 +228,6 @@ type guiApp struct {
tabSettings uintptr
btnTopMost uintptr
btnAutoUnmute uintptr
btnFontDown uintptr
btnFontUp uintptr
activeTab string
topMost bool
autoUnmuteOnLeave bool
@@ -251,6 +258,8 @@ type guiApp struct {
historyReloadRect winRect
historyExportRect winRect
historyReloadPending bool
historyRefreshRunning bool
historyRefreshResultCh chan historyRefreshResult
historyCacheLoaded bool
historyDetailContentHeightPx int32
settingsExportDirEdit uintptr
@@ -325,6 +334,9 @@ func guiLog(text string) {
}
func runNativeGUI(initialTab string) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
user32 := syscall.NewLazyDLL("user32.dll")
kernel32 := syscall.NewLazyDLL("kernel32.dll")
gdi32 := syscall.NewLazyDLL("gdi32.dll")
@@ -442,7 +454,7 @@ func runNativeGUI(initialTab string) error {
if guiCfg.FontSize > 0 {
app.fontSize = guiCfg.FontSize
} else {
app.fontSize = 18
app.fontSize = defaultGUIFontSize
}
app.refreshIntervalValue = guiCfg.RefreshIntervalValue
if app.refreshIntervalValue <= 0 {
@@ -466,7 +478,6 @@ func runNativeGUI(initialTab string) error {
app.historyExportCustom = strings.TrimSpace(guiCfg.HistoryExportCustom)
app.historyReloadPending = true
app.hFont = createAppFont(app.fontSize)
refreshHistoryCache(&app, true)
paneWndProc := syscall.NewCallback(func(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr {
defer func() {
if r := recover(); r != nil {
@@ -573,8 +584,6 @@ func runNativeGUI(initialTab string) error {
rightTitle, _ := syscall.UTF16PtrFromString("")
topMostTitle, _ := syscall.UTF16PtrFromString("Top most")
autoUnmuteTitle, _ := syscall.UTF16PtrFromString("Leave unmute")
fontDownTitle, _ := syscall.UTF16PtrFromString("A-")
fontUpTitle, _ := syscall.UTF16PtrFromString("A+")
browseTitle, _ := syscall.UTF16PtrFromString("参照")
saveTitle, _ := syscall.UTF16PtrFromString("保存")
toolbarTitle, _ := syscall.UTF16PtrFromString("")
@@ -622,41 +631,43 @@ func runNativeGUI(initialTab string) error {
app.settingsPaneHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(paneClassName)), uintptr(unsafe.Pointer(settingsPaneTitle)), paneStyle, 0, contentTop, windowWidth, settingsPaneHeight, hwnd, idLog+1, hInstance, 0)
app.btnTopMost, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(topMostTitle)), buttonStyle, leftPaneWidth+32, contentTop+86, 140, 30, hwnd, 3005, hInstance, 0)
app.btnAutoUnmute, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(autoUnmuteTitle)), buttonStyle, leftPaneWidth+32, contentTop+132, 180, 30, hwnd, 3008, hInstance, 0)
app.btnFontDown, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontDownTitle)), buttonStyle, leftPaneWidth+32, contentTop+178, 52, 30, hwnd, 3006, hInstance, 0)
app.btnFontUp, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontUpTitle)), buttonStyle, leftPaneWidth+92, contentTop+178, 52, 30, hwnd, 3007, hInstance, 0)
exportDirTitle, _ := syscall.UTF16PtrFromString("")
exportDirRect := settingsHistoryExportDirRect()
app.settingsExportDirEdit, _, _ = createWindowEx.Call(
0,
uintptr(unsafe.Pointer(editClassTitle)),
uintptr(unsafe.Pointer(exportDirTitle)),
uintptr(wsChild|wsVisible|wsBorder|wsTabstop|esAutohscroll),
116, 410, 520, 24,
uintptr(exportDirRect.Left), uintptr(exportDirRect.Top), uintptr(exportDirRect.Right-exportDirRect.Left), uintptr(exportDirRect.Bottom-exportDirRect.Top),
app.settingsPaneHwnd, idHistoryExportDir, hInstance, 0,
)
browseStyle := uintptr(wsChild | wsTabstop)
browseRect := settingsHistoryExportBrowseRect()
app.settingsExportBrowseBtn, _, _ = createWindowEx.Call(
0,
uintptr(unsafe.Pointer(buttonClass)),
uintptr(unsafe.Pointer(browseTitle)),
browseStyle,
644, 410, 92, 24,
uintptr(browseRect.Left), uintptr(browseRect.Top), uintptr(browseRect.Right-browseRect.Left), uintptr(browseRect.Bottom-browseRect.Top),
app.settingsPaneHwnd, idHistoryExportBrowse, hInstance, 0,
)
exportCustomTitle, _ := syscall.UTF16PtrFromString("")
customRect := settingsHistoryExportCustomRect()
app.settingsExportCustomEdit, _, _ = createWindowEx.Call(
0,
uintptr(unsafe.Pointer(editClassTitle)),
uintptr(unsafe.Pointer(exportCustomTitle)),
uintptr(wsChild|wsVisible|wsBorder|wsTabstop|esAutohscroll),
116, 520, 520, 24,
uintptr(customRect.Left), uintptr(customRect.Top), uintptr(customRect.Right-customRect.Left), uintptr(customRect.Bottom-customRect.Top),
app.settingsPaneHwnd, idHistoryExportCustom, hInstance, 0,
)
saveRect := settingsHistoryExportCustomSaveRect()
app.settingsExportCustomSaveBtn, _, _ = createWindowEx.Call(
0,
uintptr(unsafe.Pointer(buttonClass)),
uintptr(unsafe.Pointer(saveTitle)),
browseStyle,
644, 520, 92, 24,
uintptr(saveRect.Left), uintptr(saveRect.Top), uintptr(saveRect.Right-saveRect.Left), uintptr(saveRect.Bottom-saveRect.Top),
app.settingsPaneHwnd, idHistoryExportSave, hInstance, 0,
)
applyRefreshTimer(hwnd, &app)
@@ -742,8 +753,11 @@ func runNativeGUI(initialTab string) error {
if !allowRapidSettingsAction(&app) {
return 0
}
if app.fontSize > 10 {
if app.fontSize > minGUIFontSize {
app.fontSize -= 2
if app.fontSize < minGUIFontSize {
app.fontSize = minGUIFontSize
}
oldFont := app.hFont
app.hFont = createAppFont(app.fontSize)
applyFont(&app)
@@ -757,8 +771,11 @@ func runNativeGUI(initialTab string) error {
if !allowRapidSettingsAction(&app) {
return 0
}
if app.fontSize < 30 {
if app.fontSize < maxGUIFontSize {
app.fontSize += 2
if app.fontSize > maxGUIFontSize {
app.fontSize = maxGUIFontSize
}
oldFont := app.hFont
app.hFont = createAppFont(app.fontSize)
applyFont(&app)
@@ -788,8 +805,25 @@ func runNativeGUI(initialTab string) error {
width = rc.Right
}
}
if x >= width-42 && x <= width-22 && y >= 12 && y <= 32 {
guiLog("close button ignored to keep GUI visible")
switch {
case pointInHeaderDot(x, y, width-92, 22):
guiLog("window control minimize")
if showWindowProc != nil {
showWindowProc.Call(hwnd, swMinimize)
}
return 0
case pointInHeaderDot(x, y, width-62, 22):
guiLog("window control topmost toggle")
app.topMost = !app.topMost
saveGUISettings(&app)
applyTopMost(hwnd, &app)
if invalidateRectProc != nil {
invalidateRectProc.Call(hwnd, 0, 1)
}
return 0
case pointInHeaderDot(x, y, width-32, 22):
guiLog("window control close")
postQuitMessage.Call(0)
return 0
}
releaseCapture.Call()
@@ -851,17 +885,31 @@ func runNativeGUI(initialTab string) error {
}
case wmTimerGUI:
drainHistoryExportResult(&app)
drainHistoryRefreshResult(&app)
reloadData := app.activeTab == "join" || app.activeTab == "translate"
if app.activeTab == "history" && app.historyReloadPending {
if refreshHistoryCache(&app, true) {
startHistoryRefresh(&app, true)
refreshGUI(setWindowText, &app, false)
}
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
return 0
}
refreshGUI(setWindowText, &app, reloadData)
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
return 0
case wmHistoryRefreshDone:
drainHistoryRefreshResult(&app)
if invalidateRectProc != nil {
if app.leftHwnd != 0 {
invalidateRectProc.Call(app.leftHwnd, 0, 1)
}
if app.rightHwnd != 0 {
invalidateRectProc.Call(app.rightHwnd, 0, 1)
}
if app.hwnd != 0 {
invalidateRectProc.Call(app.hwnd, 0, 1)
}
}
return 0
case wmHistoryExportDone:
drainHistoryExportResult(&app)
if invalidateRectProc != nil {
@@ -878,7 +926,8 @@ func runNativeGUI(initialTab string) error {
postQuitMessage.Call(0)
return 0
case wmCloseGUI:
guiLog("wmClose ignored to keep GUI visible")
guiLog("wmClose received")
postQuitMessage.Call(0)
return 0
}
ret, _, _ := defWindowProc.Call(hwnd, uintptr(message), wParam, lParam)
@@ -974,6 +1023,10 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) {
if app == nil {
return
}
if app.activeTab == "history" && !reloadData {
syncPaneVisibility(app)
return
}
start := time.Now()
defer func() {
if d := time.Since(start); d > 100*time.Millisecond {
@@ -984,11 +1037,7 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) {
instanceCount := app.currentUserCount
worldLabel := app.currentWorld
state := readRuntimeSnapshot()
historyDirty := false
historyNeedsRefresh := app.activeTab == "history" && (app.historyReloadPending || !app.historyCacheLoaded)
if historyNeedsRefresh {
historyDirty = refreshHistoryCache(app, false)
}
historyDirty := app.activeTab == "history" && app.historyReloadPending
if reloadData {
if app.activeTab == "translate" {
guiLog("refreshGUI activeTab=translate light refresh")
@@ -1161,7 +1210,7 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) {
if app.settingsPaneHwnd != 0 {
showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
}
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.btnFontDown, app.btnFontUp} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl)
}
@@ -1202,7 +1251,7 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
return
}
app.lastSettingsVisible = true
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.btnFontDown, app.btnFontUp} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl)
}
@@ -1226,7 +1275,7 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
if app.settingsPaneHwnd != 0 {
showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
}
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.btnFontDown, app.btnFontUp, app.settingsExportDirEdit, app.settingsExportCustomEdit, app.settingsExportBrowseBtn, app.settingsExportCustomSaveBtn} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.settingsExportDirEdit, app.settingsExportCustomEdit, app.settingsExportBrowseBtn, app.settingsExportCustomSaveBtn} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl)
}
@@ -1394,7 +1443,7 @@ func applyJoinLogFont(app *guiApp, presentCount int) {
size = app.fontSize
}
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinHeadlineHFont != 0 &&
app.joinFontSize == size && app.joinBoldFontSize == size && app.joinHeadlineSize == size+2 {
@@ -1469,7 +1518,7 @@ func applyJoinLogFontSize(app *guiApp, size int) {
return
}
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinFontSize == size && app.joinBoldFontSize == size {
return
@@ -1499,7 +1548,7 @@ func applyJoinLogFontSize(app *guiApp, size int) {
func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int {
size := baseSize
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if lineCount <= 0 {
lineCount = 1
@@ -1507,14 +1556,14 @@ func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int {
if availableHeight <= 0 {
availableHeight = 300
}
for size > 8 {
for size > minGUIFontSize {
if joinPaneContentHeight(size, lineCount) <= availableHeight {
break
}
size -= 2
}
if size < 8 {
size = 8
if size < minGUIFontSize {
size = minGUIFontSize
}
return size
}
@@ -1584,10 +1633,10 @@ func resizeGUIForSettings(app *guiApp) {
return
}
resizeFlags := hwndNoMove | hwndNoZOrder | hwndNoActivate
windowHeight := contentTop + leftPaneHeight + bottomBarHeight
windowHeight := contentTop + settingsPaneHeight + bottomBarHeight
setWindowPosProc.Call(app.hwnd, 0, 0, 0, windowWidth, uintptr(windowHeight), resizeFlags)
if app.settingsPaneHwnd != 0 {
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 0, uintptr(contentTop), windowWidth, uintptr(leftPaneHeight), resizeFlags)
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 0, uintptr(contentTop), windowWidth, uintptr(settingsPaneHeight), resizeFlags)
}
}
@@ -1712,6 +1761,10 @@ func paintMainWindow(hwnd uintptr, app *guiApp) uintptr {
return 0
}
func pointInHeaderDot(x, y, cx, cy int32) bool {
return x >= cx-11 && x <= cx+11 && y >= cy-11 && y <= cy+11
}
func paintFooter(hdc uintptr, rc winRect, app *guiApp) {
if app == nil {
return
@@ -1814,22 +1867,17 @@ func paintSettingsPane(hwnd uintptr, app *guiApp) uintptr {
drawPaneText(hdc, 24, 68, "表示", app.hFont, clrMutedText)
drawSettingsWideButton(hdc, settingsTopMostRect(), "Top most", onOffBool(app.topMost), app.hFont, app.topMost)
drawSettingsWideButton(hdc, settingsAutoUnmuteRect(), "Leave unmute", onOffBool(app.autoUnmuteOnLeave), app.hFont, app.autoUnmuteOnLeave)
drawPaneText(hdc, 24, 190, "文字サイズ", app.hFont, clrMutedText)
drawPaneText(hdc, 24, 190, "更新間隔", app.hFont, clrMutedText)
drawSettingsRefreshControl(hdc, app)
drawPaneText(hdc, 24, 314, "文字サイズ", app.hFont, clrMutedText)
drawSettingsFontControl(hdc, app)
drawPaneText(hdc, 24, 390, "履歴 Export", app.hFont, clrMutedText)
drawPaneText(hdc, 24, 414, "出力先フォルダ", app.hFont, clrMutedText)
drawSettingsBox(hdc, settingsHistoryExportDirRect(), 0x0038281a, clrPaneBorder)
drawPaneText(hdc, 120, 414, exportDirDisplayText(app.historyExportDir), app.hFont, clrText)
drawSettingsWideButton(hdc, settingsHistoryExportBrowseRect(), "参照", "", app.hFont, true)
drawPaneText(hdc, 24, 400, "履歴 Export", app.hFont, clrMutedText)
drawPaneText(hdc, 24, 430, "出力先フォルダ", app.hFont, clrMutedText)
drawSettingsWideButton(hdc, settingsHistoryExportTimeRect(), "Time", onOffBool(app.historyExportIncludeTime), app.hFont, app.historyExportIncludeTime)
drawSettingsWideButton(hdc, settingsHistoryExportWorldRect(), "World", onOffBool(app.historyExportIncludeWorld), app.hFont, app.historyExportIncludeWorld)
drawSettingsWideButton(hdc, settingsHistoryExportJoinLeaveRect(), "Join/Leave", onOffBool(app.historyExportIncludeJoinLeave), app.hFont, app.historyExportIncludeJoinLeave)
drawSettingsWideButton(hdc, settingsHistoryExportCustomToggleRect(), "Custom", onOffBool(app.historyExportCustomEnabled), app.hFont, app.historyExportCustomEnabled)
drawPaneText(hdc, 24, 522, "Custom 条件", app.hFont, clrMutedText)
drawSettingsBox(hdc, settingsHistoryExportCustomRect(), 0x0038281a, clrPaneBorder)
drawPaneText(hdc, 120, 522, previewLine(app.historyExportCustom, 60), app.hFont, clrText)
drawSettingsWideButton(hdc, settingsHistoryExportCustomSaveRect(), "保存", "", app.hFont, true)
drawPaneText(hdc, 24, 618, "Custom 条件", app.hFont, clrMutedText)
return 0
}
@@ -1870,35 +1918,35 @@ func settingsFontUpRect() winRect {
}
func settingsHistoryExportDirRect() winRect {
return winRect{Left: 116, Top: 410, Right: 636, Bottom: 434}
return winRect{Left: 24, Top: 458, Right: 636, Bottom: 490}
}
func settingsHistoryExportBrowseRect() winRect {
return winRect{Left: 644, Top: 410, Right: 736, Bottom: 434}
return winRect{Left: 644, Top: 458, Right: 736, Bottom: 490}
}
func settingsHistoryExportTimeRect() winRect {
return winRect{Left: 24, Top: 442, Right: 360, Bottom: 476}
return winRect{Left: 24, Top: 510, Right: 360, Bottom: 546}
}
func settingsHistoryExportWorldRect() winRect {
return winRect{Left: 376, Top: 442, Right: 736, Bottom: 476}
return winRect{Left: 376, Top: 510, Right: 736, Bottom: 546}
}
func settingsHistoryExportJoinLeaveRect() winRect {
return winRect{Left: 24, Top: 480, Right: 360, Bottom: 514}
return winRect{Left: 24, Top: 558, Right: 360, Bottom: 594}
}
func settingsHistoryExportCustomToggleRect() winRect {
return winRect{Left: 376, Top: 480, Right: 736, Bottom: 514}
return winRect{Left: 376, Top: 558, Right: 736, Bottom: 594}
}
func settingsHistoryExportCustomRect() winRect {
return winRect{Left: 116, Top: 520, Right: 636, Bottom: 544}
return winRect{Left: 24, Top: 646, Right: 636, Bottom: 678}
}
func settingsHistoryExportCustomSaveRect() winRect {
return winRect{Left: 644, Top: 520, Right: 736, Bottom: 544}
return winRect{Left: 644, Top: 646, Right: 736, Bottom: 678}
}
func handleSettingsPaneClick(app *guiApp, x, y int32, getWindowText, setWindowText *syscall.LazyProc) bool {
@@ -1929,15 +1977,21 @@ func handleSettingsPaneClick(app *guiApp, x, y int32, getWindowText, setWindowTe
applyRefreshTimer(app.hwnd, app)
}
case pointInRect(x, y, settingsFontDownRect()):
if app.fontSize > 10 {
if app.fontSize > minGUIFontSize {
app.fontSize -= 2
if app.fontSize < minGUIFontSize {
app.fontSize = minGUIFontSize
}
app.hFont = createAppFont(app.fontSize)
applyFont(app)
saveGUISettings(app)
}
case pointInRect(x, y, settingsFontUpRect()):
if app.fontSize < 30 {
if app.fontSize < maxGUIFontSize {
app.fontSize += 2
if app.fontSize > maxGUIFontSize {
app.fontSize = maxGUIFontSize
}
app.hFont = createAppFont(app.fontSize)
applyFont(app)
saveGUISettings(app)
@@ -1982,9 +2036,9 @@ func drawSettingsRefreshControl(hdc uintptr, app *guiApp) {
drawSettingsBox(hdc, settingsRefreshValueRect(), uint32(0x0038281a), border)
drawSettingsBox(hdc, settingsRefreshUpRect(), fill, border)
drawSettingsWideButton(hdc, settingsRefreshUnitRect(), "\u5358\u4f4d", refreshIntervalUnitLabel(app.refreshIntervalUnit), font, true)
drawCenteredPaneText(hdc, settingsRefreshDownRect(), "A-", font, clrText)
drawCenteredPaneText(hdc, settingsRefreshDownRect(), "-", font, clrText)
drawCenteredPaneText(hdc, settingsRefreshValueRect(), refreshIntervalValueLabel(app.refreshIntervalValue, app.refreshIntervalUnit), font, clrMutedText)
drawCenteredPaneText(hdc, settingsRefreshUpRect(), "A+", font, clrText)
drawCenteredPaneText(hdc, settingsRefreshUpRect(), "+", font, clrText)
}
func refreshIntervalValueLabel(value int, unit string) string {
@@ -2609,7 +2663,7 @@ func drawSettingsFontControl(hdc uintptr, app *guiApp) {
drawSettingsBox(hdc, settingsFontValueRect(), uint32(0x0038281a), border)
drawSettingsBox(hdc, settingsFontUpRect(), fill, border)
drawCenteredPaneText(hdc, settingsFontDownRect(), "A-", font, clrText)
drawCenteredPaneText(hdc, settingsFontValueRect(), strconv.Itoa(app.fontSize), font, clrMutedText)
drawCenteredPaneText(hdc, settingsFontValueRect(), strconv.Itoa(app.fontSize)+" pt", font, clrMutedText)
drawCenteredPaneText(hdc, settingsFontUpRect(), "A+", font, clrText)
}
@@ -3267,7 +3321,7 @@ func formatDurationShort(d time.Duration) string {
func joinLogFontSize(baseSize, presentCount int) int {
size := baseSize
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if presentCount <= 32 {
return size
@@ -3280,8 +3334,8 @@ func joinLogFontSize(baseSize, presentCount int) int {
case presentCount >= 33:
size -= 2
}
if size < 8 {
size = 8
if size < minGUIFontSize {
size = minGUIFontSize
}
return size
}
@@ -3736,7 +3790,7 @@ func loadGUISettings() config.GUIConfig {
if err != nil || cfg == nil {
return config.GUIConfig{
TopMost: false,
FontSize: 18,
FontSize: defaultGUIFontSize,
RefreshIntervalValue: 2,
RefreshIntervalUnit: "sec",
HistoryExportIncludeTime: true,
@@ -3745,7 +3799,7 @@ func loadGUISettings() config.GUIConfig {
}
}
if cfg.GUI.FontSize <= 0 {
cfg.GUI.FontSize = 18
cfg.GUI.FontSize = defaultGUIFontSize
}
cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit = normalizeRefreshIntervalSettings(cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit)
cfg.GUI.HistoryRegex = strings.TrimSpace(cfg.GUI.HistoryRegex)
@@ -4096,7 +4150,7 @@ func formatRightPane(tab string, state map[string]any, worldLabel string, curren
b.WriteString("\r\nFont size: ")
fontSize, _ := state["font_size"].(int)
if fontSize == 0 {
fontSize = 18
fontSize = defaultGUIFontSize
}
b.WriteString(strconv.Itoa(fontSize))
b.WriteString("\r\nLeave unmute: ")
@@ -4134,7 +4188,7 @@ func formatSettingsPane(state map[string]any) string {
b.WriteString("\r\nFont size: ")
fontSize, _ := state["font_size"].(int)
if fontSize == 0 {
fontSize = 18
fontSize = defaultGUIFontSize
}
b.WriteString(strconv.Itoa(fontSize))
b.WriteString("\r\nLeave unmute: ")

View File

@@ -53,7 +53,7 @@ func Load(path string) (*Config, error) {
cfg := &Config{
OSC: OSCConfig{Host: "127.0.0.1", Port: 9001},
GUI: GUIConfig{
FontSize: 18,
FontSize: 14,
RefreshIntervalValue: 2,
RefreshIntervalUnit: "sec",
HistoryExportIncludeTime: true,