The first version filled the whole row at 24%, and in a real log that meant every row was painted: nearly every contact has SOME QSL state, so colour was present everywhere and stopped being information — a striped background with the data behind it. The default is now a 3px stripe down the left edge. Same signal, nothing lost to read it. A filled row is still offered, with a strength slider, for operators who want the block — and the rule cards in Settings preview whichever is chosen, so the decision is made by looking rather than by imagining. Drawn as an inset shadow rather than a border: a border would shift the cells three pixels on coloured rows only, and the columns would stop lining up. Style and strength are normalised rather than rejected — a value out of range is a slider that got away, not a reason to reset the operator's colours.
113 lines
3.8 KiB
Go
113 lines
3.8 KiB
Go
package main
|
|
|
|
// Row colouring for the log grid, by QSL / LoTW status — the thing Logger32 does
|
|
// and the reason an operator can tell at a glance what still needs sending.
|
|
//
|
|
// Rules are ORDERED and the first match wins, because a contact is usually
|
|
// several things at once: one confirmed on LoTW and by card is confirmed, not
|
|
// "sent, awaiting reply". Putting the order in the data rather than in a chain
|
|
// of ifs is what lets the settings panel show it in the same order it applies.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// RowColorRule is one status and the colour it paints.
|
|
type RowColorRule struct {
|
|
ID string `json:"id"`
|
|
Color string `json:"color"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// RowColorSettings is the whole appearance block.
|
|
type RowColorSettings struct {
|
|
Enabled bool `json:"enabled"`
|
|
// Style is how the colour is shown: "bar" paints a stripe down the left edge,
|
|
// "tint" washes the row, "both" does each.
|
|
//
|
|
// A bar is the default because a filled row is a poor signal in a log where
|
|
// nearly every contact has SOME QSL state: colour that is always present
|
|
// stops being information and becomes a striped background, with the data
|
|
// behind it. The stripe says the same thing and costs nothing to read.
|
|
Style string `json:"style"`
|
|
// Intensity is the tint strength in percent. Only used by "tint"/"both".
|
|
Intensity int `json:"intensity"`
|
|
Rules []RowColorRule `json:"rules"`
|
|
}
|
|
|
|
// The rule ids, in priority order. The frontend matches on these and holds the
|
|
// labels, so a translated name never has to travel through the settings.
|
|
var rowColorOrder = []string{
|
|
"confirmed_lotw", // LoTW confirmation received
|
|
"confirmed_paper", // card or eQSL received
|
|
"sent_waiting", // sent by some route, nothing back yet
|
|
"to_send", // a card is requested / queued and has not gone out
|
|
}
|
|
|
|
// Defaults: green for done, amber for waiting, blue for owed. Deliberately
|
|
// muted — they are composited at low opacity over a dark grid, and a saturated
|
|
// value there reads as an error state rather than a status.
|
|
var rowColorDefaults = map[string]string{
|
|
"confirmed_lotw": "#16a34a",
|
|
"confirmed_paper": "#0ea5e9",
|
|
"sent_waiting": "#f59e0b",
|
|
"to_send": "#a855f7",
|
|
}
|
|
|
|
// hexColor guards what reaches the stylesheet. The value is interpolated into a
|
|
// CSS color-mix() by the grid, so anything that is not plainly a hex colour is
|
|
// refused rather than passed through.
|
|
var hexColor = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
|
|
|
func normRowColors(s RowColorSettings) RowColorSettings {
|
|
byID := map[string]RowColorRule{}
|
|
for _, r := range s.Rules {
|
|
byID[r.ID] = r
|
|
}
|
|
out := RowColorSettings{Enabled: s.Enabled, Style: s.Style, Intensity: s.Intensity}
|
|
switch out.Style {
|
|
case "bar", "tint", "both":
|
|
default:
|
|
out.Style = "bar"
|
|
}
|
|
// Clamped rather than rejected: the value only shapes a colour-mix, and a
|
|
// number outside the range is a slider that got away, not a fault worth
|
|
// resetting the operator's whole choice for.
|
|
if out.Intensity < 5 {
|
|
out.Intensity = 12
|
|
} else if out.Intensity > 45 {
|
|
out.Intensity = 45
|
|
}
|
|
for _, id := range rowColorOrder {
|
|
r := byID[id]
|
|
r.ID = id
|
|
if !hexColor.MatchString(strings.TrimSpace(r.Color)) {
|
|
r.Color = rowColorDefaults[id]
|
|
}
|
|
out.Rules = append(out.Rules, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetRowColors returns the row-colouring configuration, defaults included so the
|
|
// panel never has to invent one.
|
|
func (a *App) GetRowColors() RowColorSettings {
|
|
var s RowColorSettings
|
|
if raw := a.settingOr(keyRowColors, ""); raw != "" {
|
|
_ = json.Unmarshal([]byte(raw), &s)
|
|
}
|
|
return normRowColors(s)
|
|
}
|
|
|
|
// SaveRowColors persists it.
|
|
func (a *App) SaveRowColors(s RowColorSettings) error {
|
|
b, err := json.Marshal(normRowColors(s))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.setSetting(keyRowColors, string(b))
|
|
return nil
|
|
}
|