Skip to content

Commit f9c59d9

Browse files
committed
win32: opacity function and remove WU
1 parent b4f805b commit f9c59d9

17 files changed

Lines changed: 226 additions & 43 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ A compact weather widget for your desktop.
5656
],
5757
"refreshInterval": 30,
5858
"cornerPosition": "top-right",
59+
"customX": -819,
60+
"customY": 99,
5961
"apiConfig": {
6062
"provider": "openweathermap",
6163
"apiKey": "YOUR_API_KEY"

cmd/weatherwidget/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"runtime"
99

1010
"fyne.io/fyne/v2/app"
11+
"fyne.io/fyne/v2/theme"
1112

1213
appmanager "weatherwidget/internal/app"
14+
"weatherwidget/internal/ui"
1315
)
1416

1517
func main() {
1618
fyneApp := app.NewWithID("com.weatherwidget")
19+
fyneApp.Settings().SetTheme(ui.NewWidgetTheme(theme.DefaultTheme()))
1720

1821
appDataDir := appDataDirectory()
1922

internal/app/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ func (a *AppManager) applyPosition(cfg *config.Config) {
156156
} else {
157157
a.ui.SetCorner(cfg.CornerPosition)
158158
}
159+
opacity := cfg.Opacity
160+
if opacity == 0 {
161+
opacity = 100
162+
}
163+
a.ui.SetOpacity(opacity)
159164
}
160165

161166
// onSettingsSave is the callback invoked when the user saves settings.

internal/config/config_property_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ var cornerPositions = []string{
3939
// providers is the set of allowed API providers.
4040
var providers = []string{
4141
"openweathermap",
42-
"weatherunderground",
4342
}
4443

4544
// genCityConfig generates a random valid CityConfig.

internal/config/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Config struct {
1616
CornerPosition string `json:"cornerPosition"` // "top-left"|"top-right"|"bottom-left"|"bottom-right"
1717
CustomX *int `json:"customX,omitempty"`
1818
CustomY *int `json:"customY,omitempty"`
19+
Opacity int `json:"opacity"` // 25, 50, 75, or 100 (percent)
1920
APIConfig *APIConfig `json:"apiConfig,omitempty"`
2021
DatabaseConfig *DatabaseConfig `json:"databaseConfig,omitempty"`
2122
}
@@ -70,5 +71,6 @@ func DefaultConfig() *Config {
7071
},
7172
RefreshInterval: 10,
7273
CornerPosition: "bottom-right",
74+
Opacity: 100,
7375
}
7476
}

internal/config/validation.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ var allowedCornerPositions = map[string]bool{
1212

1313
// allowedProviders defines the valid remote API providers.
1414
var allowedProviders = map[string]bool{
15-
"openweathermap": true,
16-
"weatherunderground": true,
15+
"openweathermap": true,
1716
}
1817

1918
// Validate checks the given Config and returns a slice of ValidationError
@@ -80,7 +79,7 @@ func validateAPIConfig(api *APIConfig) []ValidationError {
8079
if !allowedProviders[api.Provider] {
8180
errs = append(errs, ValidationError{
8281
Field: "apiConfig.provider",
83-
Message: fmt.Sprintf("must be openweathermap or weatherunderground, got %q", api.Provider),
82+
Message: fmt.Sprintf("must be openweathermap, got %q", api.Provider),
8483
})
8584
}
8685
return errs

internal/config/validation_property_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func genMaybeInvalidAPIConfig(t *rapid.T, errs map[string]bool) (*APIConfig, map
233233
var provider string
234234
if invalidProvider {
235235
provider = rapid.StringMatching(`[a-z]{3,15}`).Draw(t, "badProvider")
236-
if provider == "openweathermap" || provider == "weatherunderground" {
236+
if provider == "openweathermap" {
237237
provider = "invalidprovider"
238238
}
239239
errs["apiConfig.provider"] = true

internal/ui/manager.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,12 @@ func (u *UIManager) SetPosition(x, y int) {
146146
func (u *UIManager) GetPosition() (int, int) {
147147
return getWindowPosition()
148148
}
149+
150+
// SetOpacity applies background-only transparency to the widget window.
151+
// opacityPercent should be 25, 50, 75, or 100.
152+
// At < 100% the background color becomes transparent via Win32 color-key;
153+
// all text, icons and other content remain fully opaque.
154+
func (u *UIManager) SetOpacity(opacityPercent int) {
155+
setWindowOpacity(opacityPercent)
156+
u.widget.Canvas().Refresh(u.widget.Content())
157+
}

internal/ui/settings.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,8 @@ func (u *UIManager) ShowSettings(cfg *config.Config, onSave func(*config.Config)
5353
}
5454

5555
// ── API config ───────────────────────────────────────────────────────
56-
providerSelect := widget.NewSelect([]string{"OpenWeatherMap", "Weather Underground"}, nil)
57-
if cfg.APIConfig != nil && cfg.APIConfig.Provider == "weatherunderground" {
58-
providerSelect.SetSelected("Weather Underground")
59-
} else {
60-
providerSelect.SetSelected("OpenWeatherMap")
61-
}
56+
providerSelect := widget.NewSelect([]string{"OpenWeatherMap"}, nil)
57+
providerSelect.SetSelected("OpenWeatherMap")
6258
apiKeyEntry := widget.NewEntry()
6359
apiKeyEntry.SetPlaceHolder("API Key")
6460
if cfg.APIConfig != nil {
@@ -146,6 +142,21 @@ func (u *UIManager) ShowSettings(cfg *config.Config, onSave func(*config.Config)
146142
}
147143
positionRadio.OnChanged = func(_ string) { customPosLabel.SetText("") }
148144

145+
// ── Transparency ─────────────────────────────────────────────────────
146+
opacityRadio := widget.NewRadioGroup([]string{"25%", "50%", "75%", "100%"}, nil)
147+
opacityRadio.Horizontal = true
148+
opacityMap := map[string]int{"25%": 25, "50%": 50, "75%": 75, "100%": 100}
149+
opacityLabelMap := map[int]string{25: "25%", 50: "50%", 75: "75%", 100: "100%"}
150+
currentOpacity := cfg.Opacity
151+
if currentOpacity == 0 {
152+
currentOpacity = 100
153+
}
154+
if label, ok := opacityLabelMap[currentOpacity]; ok {
155+
opacityRadio.SetSelected(label)
156+
} else {
157+
opacityRadio.SetSelected("100%")
158+
}
159+
149160
// ── Refresh interval ─────────────────────────────────────────────────
150161
intervalSlider := widget.NewSlider(1, 60)
151162
intervalSlider.Step = 1
@@ -168,6 +179,9 @@ func (u *UIManager) ShowSettings(cfg *config.Config, onSave func(*config.Config)
168179
positionRadio,
169180
customPosLabel,
170181
widget.NewSeparator(),
182+
widget.NewLabel("Background Transparency"),
183+
opacityRadio,
184+
widget.NewSeparator(),
171185
widget.NewLabel("Refresh Interval"),
172186
container.NewHBox(intervalSlider, intervalLabel),
173187
)
@@ -302,7 +316,7 @@ func (u *UIManager) ShowSettings(cfg *config.Config, onSave func(*config.Config)
302316
newCfg := buildConfigFromUI(
303317
dataSourceRadio, providerSelect, apiKeyEntry,
304318
dbHostEntry, dbPortEntry, dbNameEntry, dbUserEntry, dbPassEntry, dbQueryEntry,
305-
intervalSlider, state, positionValueMap, positionRadio,
319+
intervalSlider, state, positionValueMap, positionRadio, opacityRadio, opacityMap, cfg,
306320
)
307321
errs := config.Validate(newCfg)
308322
if len(errs) > 0 {
@@ -347,15 +361,25 @@ func buildConfigFromUI(
347361
state *settingsState,
348362
positionValueMap map[string]string,
349363
positionRadio *widget.RadioGroup,
364+
opacityRadio *widget.RadioGroup,
365+
opacityMap map[string]int,
366+
current *config.Config,
350367
) *config.Config {
351368
cornerPosition := positionValueMap[positionRadio.Selected]
352369
if cornerPosition == "" {
353370
cornerPosition = "bottom-right"
354371
}
372+
opacity := opacityMap[opacityRadio.Selected]
373+
if opacity == 0 {
374+
opacity = 100
375+
}
355376
cfg := &config.Config{
356377
Cities: copyCities(state.cities),
357378
RefreshInterval: int(intervalSlider.Value),
358379
CornerPosition: cornerPosition,
380+
CustomX: current.CustomX,
381+
CustomY: current.CustomY,
382+
Opacity: opacity,
359383
}
360384
if dataSourceRadio.Selected == "Local Database" {
361385
cfg.DataSource = config.DataSourceLocalDatabase
@@ -370,12 +394,8 @@ func buildConfigFromUI(
370394
}
371395
} else {
372396
cfg.DataSource = config.DataSourceRemoteAPI
373-
provider := "openweathermap"
374-
if providerSelect.Selected == "Weather Underground" {
375-
provider = "weatherunderground"
376-
}
377397
cfg.APIConfig = &config.APIConfig{
378-
Provider: provider,
398+
Provider: "openweathermap",
379399
APIKey: strings.TrimSpace(apiKeyEntry.Text),
380400
}
381401
}

internal/ui/theme.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package ui
2+
3+
import (
4+
"image/color"
5+
"sync/atomic"
6+
7+
"fyne.io/fyne/v2"
8+
"fyne.io/fyne/v2/theme"
9+
)
10+
11+
// transparencyKey is the color Windows will make invisible via LWA_COLORKEY.
12+
// Chosen to be near-black but distinct from pure black so it doesn't clash
13+
// with any real UI element color.
14+
var transparencyKey = color.NRGBA{R: 1, G: 1, B: 1, A: 255}
15+
16+
// transparencyActive is 1 when a color-key background should be used.
17+
var transparencyActive atomic.Int32
18+
19+
// SetTransparencyActive switches the background color key on or off.
20+
func SetTransparencyActive(active bool) {
21+
if active {
22+
transparencyActive.Store(1)
23+
} else {
24+
transparencyActive.Store(0)
25+
}
26+
}
27+
28+
// widgetTheme is a Fyne theme that replaces the window background with the
29+
// color key when transparency is active, leaving all other colors unchanged.
30+
type widgetTheme struct {
31+
base fyne.Theme
32+
}
33+
34+
// NewWidgetTheme wraps the given base theme.
35+
func NewWidgetTheme(base fyne.Theme) fyne.Theme {
36+
return &widgetTheme{base: base}
37+
}
38+
39+
func (t *widgetTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
40+
if transparencyActive.Load() == 1 {
41+
switch name {
42+
case theme.ColorNameBackground, theme.ColorNameOverlayBackground:
43+
return transparencyKey
44+
}
45+
}
46+
return t.base.Color(name, variant)
47+
}
48+
49+
func (t *widgetTheme) Font(style fyne.TextStyle) fyne.Resource {
50+
return t.base.Font(style)
51+
}
52+
53+
func (t *widgetTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
54+
return t.base.Icon(name)
55+
}
56+
57+
func (t *widgetTheme) Size(name fyne.ThemeSizeName) float32 {
58+
return t.base.Size(name)
59+
}

0 commit comments

Comments
 (0)