diff --git a/core/logbook/qsolist.go b/core/logbook/qsolist.go index 631c6fe..a8ad269 100644 --- a/core/logbook/qsolist.go +++ b/core/logbook/qsolist.go @@ -338,12 +338,10 @@ func (l *QSOList) GetQSOs(numbers []core.QSONumber) []core.QSO { func (l *QSOList) getQSOs(numbers []core.QSONumber) []core.QSO { result := make([]core.QSO, 0, len(numbers)) for _, n := range numbers { - listIndex, found := l.findIndex(n) + qso, found := l.getQSO(n) if !found { - log.Printf("QSO number %d not found", n) continue } - qso := l.list[listIndex] if len(result) > 0 && n > result[len(result)-1].MyNumber { result = append(result, qso) } else { @@ -357,6 +355,22 @@ func (l *QSOList) getQSOs(numbers []core.QSONumber) []core.QSO { return result } +func (l *QSOList) GetQSO(number core.QSONumber) (core.QSO, bool) { + l.dataLock.RLock() + defer l.dataLock.RUnlock() + + return l.getQSO(number) +} + +func (l *QSOList) getQSO(number core.QSONumber) (core.QSO, bool) { + listIndex, found := l.findIndex(number) + if !found { + log.Printf("QSO number %d not found", number) + return core.QSO{}, false + } + return l.list[listIndex], true +} + func (l *QSOList) FindWorkedQSOs(callsign callsign.Callsign, band core.Band, mode core.Mode) ([]core.QSO, bool) { l.dataLock.RLock() diff --git a/fyneui/app.go b/fyneui/app.go index 87cac82..42d381b 100644 --- a/fyneui/app.go +++ b/fyneui/app.go @@ -35,6 +35,7 @@ type application struct { shortcuts *Shortcuts mainWindow *mainWindow mainMenu *mainMenu + qsoList *qsoList statusBar *statusBar controller *app.Controller @@ -49,17 +50,23 @@ func (a *application) activate() { a.controller.Startup() a.shortcuts = setupShortcuts(a.controller) + a.qsoList = setupQSOList() a.statusBar = setupStatusBar() mainWindow := a.app.NewWindow("Hello Contest") - a.mainWindow = setupMainWindow(mainWindow, a.statusBar) + a.mainWindow = setupMainWindow(mainWindow, a.qsoList, a.statusBar) a.shortcuts.AddTo(mainWindow.Canvas()) a.mainMenu = setupMainMenu(a.mainWindow.window, a.controller, a.shortcuts) + a.qsoList.SetLogbookController(a.controller.QSOList) + a.controller.SetView(a.mainWindow) + a.controller.QSOList.Notify(a.qsoList) a.controller.ServiceStatus.Notify(a.statusBar) + a.controller.Refresh() + a.mainWindow.UseDefaultWindowGeometry() // TODO: store/restore the window geometry a.mainWindow.Show() } diff --git a/fyneui/mainWindow.go b/fyneui/mainWindow.go index 28423b2..034520e 100644 --- a/fyneui/mainWindow.go +++ b/fyneui/mainWindow.go @@ -9,25 +9,24 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/widget" ) type mainWindow struct { window fyne.Window } -func setupMainWindow(window fyne.Window, statusBar *statusBar) *mainWindow { +func setupMainWindow(window fyne.Window, qsoList *qsoList, statusBar *statusBar) *mainWindow { result := &mainWindow{ window: window, } window.SetMaster() root := container.NewBorder( - nil, // top - statusBar.container, // bottom - nil, // left - nil, // right - widget.NewLabel("Hello Contest"), // center + nil, // top + statusBar.container, // bottom + nil, // left + nil, // right + qsoList.container, // center ) window.SetContent(root) diff --git a/fyneui/qsoList.go b/fyneui/qsoList.go new file mode 100644 index 0000000..ee2df37 --- /dev/null +++ b/fyneui/qsoList.go @@ -0,0 +1,204 @@ +package fyneui + +import ( + "fmt" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" + + "github.com/ftl/hellocontest/core" +) + +const ( + exchangeAt = 4 + myExchangeTemplate = "My %s" + theirExchangeTemplate = "Th %s" + exchangeColumnWidth = 100 +) + +var columnHeaders = []string{"UTC", "Callsign", "Band", "Mode", "Pts", "Mult", "D"} +var columnWidth = []float32{55, 100, 55, 50, 35, 40, 10} + +type LogbookController interface { + GetExchangeFields() ([]core.ExchangeField, []core.ExchangeField) + SelectRow(int) +} + +type qsoList struct { + container *fyne.Container + table *widget.Table + controller LogbookController + + myExchangeFields []core.ExchangeField + theirExchangeFields []core.ExchangeField + + headerRow []string + entryRows [][]string +} + +func setupQSOList() *qsoList { + result := &qsoList{ + headerRow: columnHeaders, + entryRows: [][]string{}, + } + + result.table = widget.NewTable(result.tableSize, result.createTableCell, result.updateValueCell) + result.table.ShowHeaderRow = true + result.table.CreateHeader = result.createTableCell + result.table.UpdateHeader = result.updateHeaderCell + + result.container = container.New(layout.NewStackLayout(), result.table) + + return result +} + +func (l *qsoList) SetLogbookController(controller LogbookController) { + l.controller = controller + l.ExchangeFieldsChanged(l.controller.GetExchangeFields()) +} + +func (l *qsoList) updateHeaderRow() { + exchangeLength := len(l.myExchangeFields) + len(l.theirExchangeFields) + firstTheirExchange := exchangeAt + len(l.myExchangeFields) + length := len(columnHeaders) + exchangeLength + + headerRow := make([]string, length) + for i := range headerRow { + if i < exchangeAt { + headerRow[i] = columnHeaders[i] + } else if i >= exchangeAt+exchangeLength { + headerRow[i] = columnHeaders[i-exchangeLength] + } else if i < firstTheirExchange { + headerRow[i] = fmt.Sprintf(myExchangeTemplate, exchangeColumnName(l.myExchangeFields[i-exchangeAt])) + } else { + headerRow[i] = fmt.Sprintf(theirExchangeTemplate, exchangeColumnName(l.theirExchangeFields[i-firstTheirExchange])) + } + } + + l.headerRow = headerRow +} + +func exchangeColumnName(field core.ExchangeField) string { + if len(field.Properties) == 1 { + return field.Short + } + return "Exch" +} + +func (l *qsoList) tableSize() (int, int) { + return len(l.entryRows), len(l.headerRow) +} + +func (l *qsoList) createTableCell() fyne.CanvasObject { + return widget.NewLabel("") +} + +func (l *qsoList) updateHeaderCell(id widget.TableCellID, cell fyne.CanvasObject) { + label := cell.(*widget.Label) + label.TextStyle.Bold = true + label.SetText(l.columnHeaderText(id.Col)) + + l.table.SetColumnWidth(id.Col, l.columnWidth(id.Col)) +} + +func (l *qsoList) columnHeaderText(column int) string { + if column < 0 || column >= len(l.headerRow) { + return "" + } + + return l.headerRow[column] +} + +func (l *qsoList) columnWidth(column int) float32 { + if column < exchangeAt { + return columnWidth[column] + } else if column >= len(l.headerRow)-exchangeAt { + return columnWidth[column-exchangeAt] + } else { + return exchangeColumnWidth + } +} + +func (l *qsoList) updateValueCell(id widget.TableCellID, cell fyne.CanvasObject) { + label := cell.(*widget.Label) + label.TextStyle.Bold = false + label.SetText(l.valueCellText(id.Row, id.Col)) +} + +func (l *qsoList) valueCellText(row int, column int) string { + if row < 0 || row >= len(l.entryRows) { + return "" + } + entryRow := l.entryRows[row] + if column < 0 || column >= len(entryRow) { + return "" + } + + return entryRow[column] +} + +func (l *qsoList) QSOsCleared() { + l.entryRows = [][]string{} +} + +func (l *qsoList) QSOAdded(qso core.QSO) { + entryRow := l.qsoToRow(qso) + l.entryRows = append(l.entryRows, entryRow) +} + +func (l *qsoList) qsoToRow(qso core.QSO) []string { + length := len(l.headerRow) + firstTheirExchange := exchangeAt + len(l.myExchangeFields) + result := make([]string, length) + result[0] = qso.Time.In(time.UTC).Format("15:04") + result[1] = qso.Callsign.String() + result[2] = qso.Band.String() + result[3] = qso.Mode.String() + result[length-3] = pointsToString(qso.Points, qso.Duplicate) + result[length-2] = pointsToString(qso.Multis, qso.Duplicate) + result[length-1] = boolToCheckmark(qso.Duplicate) + + for i, value := range qso.MyExchange { + result[i+exchangeAt] = value + } + + for i, value := range qso.TheirExchange { + result[i+firstTheirExchange] = value + } + + return result +} + +func pointsToString(points int, duplicate bool) string { + if duplicate { + return fmt.Sprintf("(%d)", points) + } + return fmt.Sprintf("%d", points) +} + +func boolToCheckmark(value bool) string { + if value { + return "✓" + } + return "" +} + +func (l *qsoList) forRow(row int, f func(widget.TableCellID)) { + for col := range l.headerRow { + id := widget.TableCellID{Row: row, Col: col} + f(id) + } +} + +func (l *qsoList) RowSelected(row int) { + l.table.Select(widget.TableCellID{Row: row, Col: 0}) +} + +func (l *qsoList) ExchangeFieldsChanged(myExchangeFields []core.ExchangeField, theirExchangeFields []core.ExchangeField) { + l.myExchangeFields = myExchangeFields + l.theirExchangeFields = theirExchangeFields + l.updateHeaderRow() +} diff --git a/ui/logbookView.go b/ui/logbookView.go index d6114d8..563f2d7 100644 --- a/ui/logbookView.go +++ b/ui/logbookView.go @@ -220,11 +220,6 @@ func boolToCheckmark(value bool) string { return "" } -func (v *logbookView) QSOInserted(index int, qso core.QSO) { - // insertion is currently not supported as it does not happen in practice - log.Printf("qso %d inserted at %d", qso.MyNumber, index) -} - func (v *logbookView) QSOUpdated(index int, _, qso core.QSO) { row, err := v.list.GetIterFromString(fmt.Sprintf("%d", index)) if err != nil {