feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"nuxt.isNuxtApp": false
|
||||
}
|
||||
131
README.md
Normal file → Executable file
131
README.md
Normal file → Executable file
@ -1,12 +1,23 @@
|
||||
# fckeuspy-go
|
||||
|
||||
## Co to je
|
||||
|
||||
Mini Go/HTMX aplikace pro šifrování zpráv cizím veřejným klíčem.
|
||||
Nástroj v Go pro asymetrické šifrování zpráv cizím veřejným klíčem.
|
||||
|
||||
* Při startu vygeneruje nebo načte RSA identitu (2048 bitů).
|
||||
* Umožní sdílet veřejný klíč (`public.pem`) a self-signed cert (`identity.crt`).
|
||||
* Šifruje hybridně: **RSA-OAEP (SHA-256) + AES-256-GCM**.
|
||||
* Rozhraní přes **HTMX**, moderní responzivní layout (2 sloupce na desktopu, 1 sloupec na mobilu).
|
||||
* Všude jsou tlačítka **Copy** a **Clear** pro snadnou práci s texty.
|
||||
Má dvě rozhraní:
|
||||
|
||||
1. Web (HTMX) – jednoduché formuláře (encrypt / decrypt)
|
||||
2. Desktop (Fyne v2) – 3 záložky: Identita, Šifrování, Dešifrování (dark UI)
|
||||
|
||||
Vlastnosti:
|
||||
|
||||
* Při startu vygeneruje nebo načte RSA identitu (2048 bitů)
|
||||
* Sdílí veřejný klíč (`public.pem`) a self‑signed cert (`identity.crt`)
|
||||
* Hybridní šifra: **RSA-OAEP (SHA-256) + AES-256-GCM**
|
||||
* JSON payload: `{ek,n,ct}` (base64 komponenty)
|
||||
* V GUI nad každým polem sada tlačítek (Paste, Clear, Encrypt/Decrypt, Copy)
|
||||
* Output pole jsou read‑only
|
||||
* Pamatuje poslední záložku a velikost okna
|
||||
|
||||
---
|
||||
|
||||
@ -17,22 +28,18 @@ Mini Go/HTMX aplikace pro šifrování zpráv cizím veřejným klíčem.
|
||||
|
||||
---
|
||||
|
||||
## Rychlý start
|
||||
## Rychlý start (gui)
|
||||
|
||||
```bash
|
||||
# spustí server
|
||||
go run .
|
||||
|
||||
# otevři v prohlížeči
|
||||
http://localhost:8080/
|
||||
go run . gui
|
||||
```
|
||||
|
||||
Na prvním startu se vytvoří:
|
||||
|
||||
```
|
||||
```text
|
||||
identity_key.pem # RSA private key (PKCS#1)
|
||||
public.pem # veřejný klíč PEM (PKIX)
|
||||
identity.crt # self-signed cert (vizitka pro sdílení)
|
||||
identity.crt # self-signed cert (vizitka)
|
||||
```
|
||||
|
||||
---
|
||||
@ -51,26 +58,29 @@ REGEN_KEYS=1 go run .
|
||||
|
||||
---
|
||||
|
||||
## Jak to používat (UI)
|
||||
## Web UI použití
|
||||
|
||||
1. Otevři `http://localhost:8080/`.
|
||||
2. V sekci **Můj veřejný klíč** stáhni/kopíruj `public.pem` nebo `identity.crt` a pošli je kontaktům.
|
||||
3. V sekci **Šifrovat pro cizí klíč**:
|
||||
```bash
|
||||
# přes flag
|
||||
go run . web
|
||||
```
|
||||
|
||||
* napiš zprávu,
|
||||
* vlož **cizí** public key nebo cert (PEM blok),
|
||||
* klikni **Encrypt**.
|
||||
* Výsledek (**Zašifrovaný payload**) se zobrazí hned pod formulářem jako JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"ek": "base64(RSA-OAEP(aesKey))",
|
||||
"n": "base64(nonce)",
|
||||
"ct": "base64(aes-gcm-ciphertext)"
|
||||
}
|
||||
```
|
||||
* tlačítkem **Copy** zkopíruješ, **Clear** vymaže obsah.
|
||||
4. Příjemce vloží payload do sekce **Dešifrovat** a klikne **Decrypt** → objeví se plaintext.
|
||||
1. Otevři `http://localhost:8080/`
|
||||
2. Zkopíruj / stáhni svůj veřejný klíč nebo cert
|
||||
3. Do Encrypt formuláře vlož cizí public key/cert + zprávu → Encrypt
|
||||
4. JSON payload odešli příjemci
|
||||
5. Příjemce vloží payload do Decrypt → plaintext
|
||||
|
||||
Formát payloadu:
|
||||
|
||||
```json
|
||||
{
|
||||
"ek": "base64(RSA-OAEP(aesKey))",
|
||||
"n": "base64(nonce)",
|
||||
"ct": "base64(aes-gcm-ciphertext)"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -90,17 +100,18 @@ REGEN_KEYS=1 go run .
|
||||
|
||||
---
|
||||
|
||||
## Struktura projektu
|
||||
## Struktura projektu (zkráceně)
|
||||
|
||||
```
|
||||
```text
|
||||
.
|
||||
├─ main.go
|
||||
├─ templates/
|
||||
│ ├─ index.html # UI s Copy/Clear tlačítky
|
||||
│ ├─ encrypt.html # výstup šifrování
|
||||
│ └─ decrypt.html # výstup dešifrování
|
||||
└─ static/
|
||||
└─ style.css # responzivní layout (1 sloupec mobile, 2 desktop)
|
||||
├─ main.go # cobra entry
|
||||
├─ cmd.go # CLI příkazy (gui, web)
|
||||
├─ ui.go # Fyne komponenty
|
||||
├─ fyne_ui.go # init okna + prefs
|
||||
├─ server.go # HTTP handlery
|
||||
├─ lib/crypto.go # šifrovací logika
|
||||
├─ templates/ # HTMX šablony
|
||||
└─ static/ # CSS
|
||||
```
|
||||
|
||||
---
|
||||
@ -122,15 +133,41 @@ REGEN_KEYS=1 go run .
|
||||
|
||||
---
|
||||
|
||||
## Fyne GUI režim
|
||||
|
||||
Spuštění desktopu:
|
||||
|
||||
```bash
|
||||
go run . gui
|
||||
```
|
||||
|
||||
Záložky:
|
||||
|
||||
1. Identita – kopírování `public.pem` / `identity.crt`
|
||||
2. Šifrování – peer key, zpráva, tlačítka: Paste, Clear, Encrypt, Copy
|
||||
3. Dešifrování – payload: Paste+Decrypt, Clear (čistí i výsledek), Copy
|
||||
|
||||
GUI je trvale v dark stylu. Pamatuje poslední tab a velikost okna.
|
||||
|
||||
HTTP režim zůstává:
|
||||
|
||||
```bash
|
||||
go run . web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Jak to funguje podle chatGPT
|
||||
|
||||
## Jak to funguje podle chatGPT
|
||||
|
||||
Ok, pojďme úplně jednoduše, krok za krokem – bez odborných keců 👇
|
||||
|
||||
---
|
||||
|
||||
### Jak to celé funguje
|
||||
## Jak to celé funguje
|
||||
|
||||
1. **Máš dva klíče**:
|
||||
|
||||
@ -200,9 +237,9 @@ Ok, pojďme úplně jednoduše, krok za krokem – bez odborných keců 👇
|
||||
|
||||
|
||||
|
||||
# Jak je to s certifikaty podle chatGPT
|
||||
## Jak je to s certifikaty podle chatGPT
|
||||
|
||||
### `identity_key.pem`
|
||||
## `identity_key.pem`
|
||||
|
||||
* **Soukromý klíč** (private key)
|
||||
* Tenhle soubor je jen pro tebe.
|
||||
@ -215,7 +252,7 @@ Ok, pojďme úplně jednoduše, krok za krokem – bez odborných keců 👇
|
||||
|
||||
---
|
||||
|
||||
### `public.pem`
|
||||
## `public.pem`
|
||||
|
||||
* **Veřejný klíč** (public key)
|
||||
* To je „vizitka“, kterou pošleš ostatním.
|
||||
@ -225,7 +262,7 @@ Ok, pojďme úplně jednoduše, krok za krokem – bez odborných keců 👇
|
||||
|
||||
---
|
||||
|
||||
### `identity.crt`
|
||||
## `identity.crt`
|
||||
|
||||
* **Certifikát** (self-signed = podepsaný sám sebou)
|
||||
* Je to vlastně balíček, který obsahuje:
|
||||
@ -238,7 +275,7 @@ Ok, pojďme úplně jednoduše, krok za krokem – bez odborných keců 👇
|
||||
|
||||
---
|
||||
|
||||
### Shrnutí v řeči pro normální lidi
|
||||
## Shrnutí v řeči pro normální lidi
|
||||
|
||||
* `identity_key.pem` → **tajný klíč** = schovej do trezoru, nikomu nedávej.
|
||||
* `public.pem` → **veřejný klíč** = pošli komukoli, aby ti mohl šifrovat zprávy.
|
||||
|
||||
35
cmd.go
Executable file
35
cmd.go
Executable file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "fckeuspy",
|
||||
Short: "Hybrid RSA+AES šifrovací nástroj (GUI / HTTP)",
|
||||
Long: "Fckeuspy: nástroj pro šifrování a dešifrování pomocí hybridního RSA-OAEP + AES-GCM, s Fyne GUI a HTTP serverem.",
|
||||
}
|
||||
|
||||
var guiCmd = &cobra.Command{
|
||||
Use: "gui",
|
||||
Short: "Spustí Fyne GUI",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runFyne()
|
||||
},
|
||||
}
|
||||
|
||||
var webCmd = &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "Spustí HTTP server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Spouštím HTTP server na :8080 ...")
|
||||
RunWebApp()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(guiCmd)
|
||||
rootCmd.AddCommand(webCmd)
|
||||
}
|
||||
378
fyne_ui.go
Executable file
378
fyne_ui.go
Executable file
@ -0,0 +1,378 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// strengthRowLayout rozdělí dostupnou šířku v poměru ~70:30 (bar+label : tlačítko)
|
||||
type strengthRowLayout struct{}
|
||||
|
||||
func (l *strengthRowLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
|
||||
if len(objs) != 2 {
|
||||
return
|
||||
}
|
||||
left := objs[0]
|
||||
right := objs[1]
|
||||
leftW := float32(float64(size.Width) * 0.70)
|
||||
if leftW < 10 {
|
||||
leftW = size.Width / 2
|
||||
}
|
||||
left.Resize(fyne.NewSize(leftW, size.Height))
|
||||
left.Move(fyne.NewPos(0, 0))
|
||||
right.Resize(fyne.NewSize(size.Width-leftW, size.Height))
|
||||
right.Move(fyne.NewPos(leftW, 0))
|
||||
}
|
||||
|
||||
func (l *strengthRowLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
|
||||
var w, h float32
|
||||
if len(objs) == 2 {
|
||||
m1 := objs[0].MinSize()
|
||||
m2 := objs[1].MinSize()
|
||||
w = m1.Width + m2.Width
|
||||
if m1.Height > m2.Height {
|
||||
h = m1.Height
|
||||
} else {
|
||||
h = m2.Height
|
||||
}
|
||||
} else {
|
||||
for _, o := range objs {
|
||||
ms := o.MinSize()
|
||||
w += ms.Width
|
||||
if ms.Height > h {
|
||||
h = ms.Height
|
||||
}
|
||||
}
|
||||
}
|
||||
return fyne.NewSize(w, h)
|
||||
}
|
||||
|
||||
func NewUI() (stprageDir string, window fyne.Window) {
|
||||
// App + storage dir
|
||||
a := app.NewWithID("fckeuspy")
|
||||
w := a.NewWindow("Encryptor (Fyne)")
|
||||
prefs := a.Preferences()
|
||||
width := prefs.IntWithFallback("winW", 1100)
|
||||
height := prefs.IntWithFallback("winH", 720)
|
||||
w.Resize(fyne.NewSize(float32(width), float32(height)))
|
||||
w.SetOnClosed(func() {
|
||||
sz := w.Canvas().Size()
|
||||
prefs.SetInt("winW", int(sz.Width))
|
||||
prefs.SetInt("winH", int(sz.Height))
|
||||
})
|
||||
|
||||
// docasny bypass
|
||||
/*
|
||||
base, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
base, _ = os.UserHomeDir()
|
||||
}
|
||||
*/
|
||||
base := "./"
|
||||
|
||||
//return filepath.Join(base, "fckeuspy-go"), w
|
||||
return filepath.Join(base, ""), w
|
||||
}
|
||||
|
||||
// ShowPasswordVaultDialog zobrazí moderní odemykací / registrační dialog s countdown animací při špatném hesle.
|
||||
// Callback je zavolán pouze při úspěchu (ne při neplatném hesle). Zrušení vrací password="".
|
||||
func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string, commonName string)) {
|
||||
modeCreate := false
|
||||
if _, err := os.Stat(vaultPath); err != nil {
|
||||
modeCreate = true
|
||||
}
|
||||
statusLabel := widget.NewLabel("")
|
||||
statusLabel.Wrapping = fyne.TextWrapWord
|
||||
statusLabel.Hide()
|
||||
// Unlock
|
||||
unlockPw := widget.NewPasswordEntry()
|
||||
unlockPw.SetPlaceHolder("Heslo…")
|
||||
unlockBtn := widget.NewButton("Odemknout", nil)
|
||||
unlockForm := container.NewVBox(widget.NewForm(widget.NewFormItem("Heslo", unlockPw)), unlockBtn)
|
||||
// Create
|
||||
createPw1 := widget.NewPasswordEntry()
|
||||
createPw1.SetPlaceHolder("Heslo…")
|
||||
createPw2 := widget.NewPasswordEntry()
|
||||
createPw2.SetPlaceHolder("Znovu heslo…")
|
||||
commonNameEntry := widget.NewEntry()
|
||||
commonNameEntry.SetPlaceHolder("Jméno identity (např. Alice)…")
|
||||
strengthBar := widget.NewProgressBar()
|
||||
strengthBar.Min = 0
|
||||
strengthBar.Max = 100
|
||||
// Skryj defaultní procenta uvnitř progress baru, protože máme vlastní overlay label
|
||||
strengthBar.TextFormatter = func() string { return "" }
|
||||
strengthLabel := widget.NewLabel("")
|
||||
updateStrength := func(pw string) {
|
||||
s, d := passwordStrengthScore(pw)
|
||||
strengthBar.SetValue(float64(s))
|
||||
strengthLabel.SetText(fmt.Sprintf("%s: %d%%", d, s))
|
||||
}
|
||||
createPw1.OnChanged = updateStrength
|
||||
genBtn := widget.NewButton("Generovat", func() {
|
||||
if v, err := encrypt.GenerateRandomPassword(24); err == nil {
|
||||
createPw1.SetText(v)
|
||||
createPw2.SetText(v)
|
||||
updateStrength(v)
|
||||
}
|
||||
})
|
||||
createBtn := widget.NewButton("Vytvořit trezor", nil)
|
||||
|
||||
// Obě pole stejné šířky – žádné tlačítko uvnitř prvního řádku
|
||||
form := widget.NewForm(
|
||||
widget.NewFormItem("Jméno", commonNameEntry),
|
||||
widget.NewFormItem("Heslo", createPw1),
|
||||
widget.NewFormItem("Potvrzení", createPw2),
|
||||
)
|
||||
|
||||
// Řádek: progress bar s textem uvnitř (overlay) : tlačítko (70:30)
|
||||
strengthLabel.Alignment = fyne.TextAlignCenter
|
||||
leftStack := container.NewStack(
|
||||
strengthBar,
|
||||
container.NewCenter(strengthLabel),
|
||||
)
|
||||
strengthRow := container.New(&strengthRowLayout{}, leftStack, genBtn)
|
||||
createForm := container.NewVBox(
|
||||
form,
|
||||
strengthRow,
|
||||
createBtn,
|
||||
)
|
||||
stack := container.NewStack(unlockForm, createForm)
|
||||
showMode := func(c bool) {
|
||||
modeCreate = c
|
||||
if c {
|
||||
unlockForm.Hide()
|
||||
createForm.Show()
|
||||
} else {
|
||||
createForm.Hide()
|
||||
unlockForm.Show()
|
||||
}
|
||||
statusLabel.Hide()
|
||||
}
|
||||
showMode(modeCreate)
|
||||
segment := widget.NewRadioGroup([]string{"Odemknout", "Vytvořit"}, func(val string) { showMode(val == "Vytvořit") })
|
||||
segment.Horizontal = true
|
||||
segment.Refresh()
|
||||
if modeCreate {
|
||||
segment.SetSelected("Vytvořit")
|
||||
} else {
|
||||
segment.SetSelected("Odemknout")
|
||||
}
|
||||
header := container.NewVBox(
|
||||
widget.NewLabelWithStyle("🔐 Trezor", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||
segment,
|
||||
widget.NewSeparator(),
|
||||
)
|
||||
var d dialog.Dialog
|
||||
completed := false
|
||||
var countdownCancel chan struct{}
|
||||
startCountdown := func(sec int) {
|
||||
if countdownCancel != nil {
|
||||
close(countdownCancel)
|
||||
}
|
||||
countdownCancel = make(chan struct{})
|
||||
statusLabel.Show()
|
||||
unlockBtn.Disable()
|
||||
unlockPw.Disable()
|
||||
go func() {
|
||||
for i := sec; i > 0; i-- {
|
||||
select {
|
||||
case <-countdownCancel:
|
||||
return
|
||||
default:
|
||||
}
|
||||
iLocal := i
|
||||
fyne.Do(func() { statusLabel.SetText(fmt.Sprintf("Neplatné heslo. Nový pokus za %d s", iLocal)) })
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
fyne.Do(func() { unlockBtn.Enable(); unlockPw.Enable(); unlockPw.SetText(""); statusLabel.Hide() })
|
||||
}()
|
||||
}
|
||||
unlockBtn.OnTapped = func() {
|
||||
pw := unlockPw.Text
|
||||
if pw == "" {
|
||||
statusLabel.SetText("Zadejte heslo")
|
||||
statusLabel.Show()
|
||||
return
|
||||
}
|
||||
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil {
|
||||
statusLabel.SetText("Neplatné heslo")
|
||||
statusLabel.Show()
|
||||
startCountdown(3)
|
||||
return
|
||||
}
|
||||
completed = true
|
||||
onResult(false, pw, "")
|
||||
d.Hide()
|
||||
}
|
||||
createBtn.OnTapped = func() {
|
||||
pw1, pw2 := createPw1.Text, createPw2.Text
|
||||
if pw1 != pw2 {
|
||||
statusLabel.SetText("Hesla se neshodují")
|
||||
statusLabel.Show()
|
||||
return
|
||||
}
|
||||
if err := encrypt.ValidatePasswordForUI(pw1); err != nil {
|
||||
statusLabel.SetText(err.Error())
|
||||
statusLabel.Show()
|
||||
return
|
||||
}
|
||||
completed = true
|
||||
cn := strings.TrimSpace(commonNameEntry.Text)
|
||||
onResult(true, pw1, cn)
|
||||
d.Hide()
|
||||
}
|
||||
body := container.NewVBox(header, container.NewPadded(stack), statusLabel)
|
||||
d = dialog.NewCustom("", "Zrušit", body, w)
|
||||
d.SetOnClosed(func() {
|
||||
if countdownCancel != nil {
|
||||
close(countdownCancel)
|
||||
}
|
||||
if !completed {
|
||||
onResult(false, "", "")
|
||||
}
|
||||
})
|
||||
d.Resize(fyne.NewSize(480, 320))
|
||||
d.Show()
|
||||
if modeCreate {
|
||||
w.Canvas().Focus(createPw1)
|
||||
} else {
|
||||
w.Canvas().Focus(unlockPw)
|
||||
}
|
||||
}
|
||||
|
||||
// passwordStrengthScore: heuristický odhad síly (0-100) s ohledem na entropii, třídy znaků a penalizace.
|
||||
func passwordStrengthScore(pw string) (int, string) {
|
||||
if pw == "" {
|
||||
return 0, "Prázdné"
|
||||
}
|
||||
length := len(pw)
|
||||
// Charakteristiky
|
||||
hasUpper, hasLower, hasDigit, hasSymbol := false, false, false, false
|
||||
symbols := "!@#$%^&*()_-+=[]{}/?:.,<>|~`'\""
|
||||
freq := make(map[rune]int)
|
||||
for _, r := range pw {
|
||||
freq[r]++
|
||||
switch {
|
||||
case r >= 'A' && r <= 'Z':
|
||||
hasUpper = true
|
||||
case r >= 'a' && r <= 'z':
|
||||
hasLower = true
|
||||
case r >= '0' && r <= '9':
|
||||
hasDigit = true
|
||||
default:
|
||||
if strings.ContainsRune(symbols, r) {
|
||||
hasSymbol = true
|
||||
}
|
||||
}
|
||||
}
|
||||
classes := 0
|
||||
if hasUpper {
|
||||
classes++
|
||||
}
|
||||
if hasLower {
|
||||
classes++
|
||||
}
|
||||
if hasDigit {
|
||||
classes++
|
||||
}
|
||||
if hasSymbol {
|
||||
classes++
|
||||
}
|
||||
|
||||
// Shannon entropie per char
|
||||
var shannon float64
|
||||
for _, c := range freq {
|
||||
p := float64(c) / float64(length)
|
||||
shannon += -p * (log2(p))
|
||||
}
|
||||
// log2 hrubě implementujeme konverzí přes Ln, zde jednoduchá aproximace: log2(p) ~ (ln p)/0.693
|
||||
// aby nebyla složitost, definujeme pomocnou funkci nahoře - zjednodušeno níže (inline hack):
|
||||
// Použijeme předpočítané p^ využitím math (neimportujeme math -> krátký lookup) => nahradíme vlastní small table -> zvolíme fallback.
|
||||
// Kvůli jednoduchosti: pokud shannon > 4.5 považuj za 4.5 (limit pro běžná hesla)
|
||||
if shannon > 4.5 {
|
||||
shannon = 4.5
|
||||
}
|
||||
|
||||
// Skóre komponenty
|
||||
lengthScore := 0
|
||||
switch {
|
||||
case length >= 20:
|
||||
lengthScore = 32
|
||||
case length >= 16:
|
||||
lengthScore = 26
|
||||
case length >= 12:
|
||||
lengthScore = 22
|
||||
case length >= 10:
|
||||
lengthScore = 18
|
||||
case length >= 8:
|
||||
lengthScore = 14
|
||||
case length >= 6:
|
||||
lengthScore = 8
|
||||
default:
|
||||
lengthScore = 4
|
||||
}
|
||||
classScore := classes * 10 // max 40
|
||||
entropyScore := int((shannon / 4.5) * 28) // max ~28
|
||||
|
||||
// Penalizace monotónnosti & nízké diverzity
|
||||
uniqueChars := len(freq)
|
||||
diversityPenalty := 0
|
||||
if uniqueChars <= 2 {
|
||||
diversityPenalty = 25
|
||||
} else if uniqueChars <= 4 {
|
||||
diversityPenalty = 15
|
||||
} else if uniqueChars <= 6 {
|
||||
diversityPenalty = 5
|
||||
}
|
||||
// Sekvenční vzory
|
||||
lower := strings.ToLower(pw)
|
||||
for _, pat := range []string{"abc", "abcd", "qwert", "1234", "password", "heslo"} {
|
||||
if strings.Contains(lower, pat) {
|
||||
diversityPenalty += 10
|
||||
}
|
||||
}
|
||||
|
||||
raw := lengthScore + classScore + entropyScore - diversityPenalty
|
||||
if raw < 0 {
|
||||
raw = 0
|
||||
}
|
||||
if raw > 100 {
|
||||
raw = 100
|
||||
}
|
||||
|
||||
desc := "Slabé"
|
||||
switch {
|
||||
case raw >= 85:
|
||||
desc = "Silné"
|
||||
case raw >= 65:
|
||||
desc = "Dobré"
|
||||
case raw >= 45:
|
||||
desc = "Střední"
|
||||
case raw >= 25:
|
||||
desc = "Nízké"
|
||||
}
|
||||
return raw, desc
|
||||
}
|
||||
|
||||
// log2 pomocná aproximace (rychlá, bez importu math)
|
||||
func log2(x float64) float64 {
|
||||
// racionální aproximace: log2(x) ≈ ln(x)/ln(2); použijeme jednoduchý Newton pro ln
|
||||
if x <= 0 {
|
||||
return 0
|
||||
}
|
||||
// Polynomická aproximace pro ln v okolí 1: ln(x) ~ 2*( (y) + y^3/3 ), y=(x-1)/(x+1) (rychlý log)
|
||||
y := (x - 1) / (x + 1)
|
||||
y2 := y * y
|
||||
ln := 2 * (y + y2*y/3)
|
||||
return ln / 0.69314718056
|
||||
}
|
||||
49
go.mod
Normal file → Executable file
49
go.mod
Normal file → Executable file
@ -1,3 +1,50 @@
|
||||
module fckeusp-go
|
||||
module fckeuspy-go
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.3
|
||||
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea
|
||||
github.com/makiuchi-d/gozxing v0.1.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/spf13/cobra v1.10.1
|
||||
golang.org/x/crypto v0.42.0
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.11.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fredbi/uri v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rymdport/portal v0.4.1 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
102
go.sum
Executable file
102
go.sum
Executable file
@ -0,0 +1,102 @@
|
||||
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
|
||||
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
|
||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
|
||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
|
||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4=
|
||||
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE=
|
||||
github.com/makiuchi-d/gozxing v0.1.0 h1:bLJdKoi5G2wGQnFirTQI9aOSCwNm5N2e0P8ov04Hltk=
|
||||
github.com/makiuchi-d/gozxing v0.1.0/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
74
lib/crypto.go
Executable file
74
lib/crypto.go
Executable file
@ -0,0 +1,74 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// No envelope here; server/UI define their own payload structs.
|
||||
|
||||
func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemOrCert))
|
||||
if block == nil {
|
||||
return nil, errors.New("no PEM block found")
|
||||
}
|
||||
switch block.Type {
|
||||
case "PUBLIC KEY":
|
||||
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsaPub, ok := k.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errors.New("expecting RSA PUBLIC KEY")
|
||||
}
|
||||
return rsaPub, nil
|
||||
case "RSA PUBLIC KEY":
|
||||
return x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
case "CERTIFICATE":
|
||||
c, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsaPub, ok := c.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errors.New("certificate does not contain RSA key")
|
||||
}
|
||||
return rsaPub, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported PEM type: %s", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
|
||||
return parsePeerPublicKey(pemOrCert)
|
||||
}
|
||||
|
||||
func generateSelfSignedCert(priv *rsa.PrivateKey, commonName string) ([]byte, error) {
|
||||
if commonName == "" {
|
||||
commonName = "Encryptor Local Identity"
|
||||
}
|
||||
tpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: commonName},
|
||||
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
_ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
486
lib/crypto_storage.go
Executable file
486
lib/crypto_storage.go
Executable file
@ -0,0 +1,486 @@
|
||||
package encrypt
|
||||
|
||||
// Nový návrh: Password-based šifrované úložiště s derivací klíče (scrypt) +
|
||||
// vytvoření uživatelských RSA klíčů při prvotní inicializaci. Private/Public
|
||||
// (a volitelně self-signed cert) se uloží jako položky uvnitř zašifrovaného
|
||||
// JSON. Soubor není možno otevřít bez hesla.
|
||||
//
|
||||
// Formát na disku (JSON):
|
||||
// {
|
||||
// "v":1,
|
||||
// "kdf":"scrypt",
|
||||
// "N":32768, "r":8, "p":1,
|
||||
// "salt":"base64(...)",
|
||||
// "n":"base64(nonce)",
|
||||
// "c":"base64(ciphertext)"
|
||||
// }
|
||||
// Plaintext (po dešifrování):
|
||||
// {
|
||||
// "updated":"RFC3339",
|
||||
// "data":{ key: <json>, ... }
|
||||
// }
|
||||
// Povinné interní klíče:
|
||||
// _identity_private_pem
|
||||
// _identity_public_pem
|
||||
// _identity_cert_pem (volitelně)
|
||||
//
|
||||
// API: CreateEncryptedStore (nový), OpenEncryptedStore, ChangePassword.
|
||||
// Původní NewSecureJSONStore (s KeyProvider) je odstraněn – přechod na heslo.
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
// SecureJSONStore definuje metody úložiště.
|
||||
type SecureJSONStore interface {
|
||||
Put(key string, value any) error
|
||||
Get(key string, dst any) error
|
||||
Delete(key string) error
|
||||
ListKeys() []string
|
||||
Flush() error
|
||||
Close() error
|
||||
ChangePassword(newPassword string) error
|
||||
Has(key string) bool
|
||||
// Identity getters (pem). Vrací prázdný string, pokud neexistují.
|
||||
IdentityPrivatePEM() string
|
||||
IdentityPublicPEM() string
|
||||
IdentityCertPEM() string
|
||||
}
|
||||
|
||||
type internalPlain struct {
|
||||
Updated time.Time `json:"updated"`
|
||||
Data map[string]json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type fileEnvelope struct {
|
||||
Version int `json:"v"`
|
||||
KDF string `json:"kdf"`
|
||||
N int `json:"N"`
|
||||
R int `json:"r"`
|
||||
P int `json:"p"`
|
||||
Salt string `json:"salt"`
|
||||
Nonce string `json:"n"`
|
||||
Cipher string `json:"c"`
|
||||
}
|
||||
|
||||
type secureJSONStore struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
kdfN int
|
||||
kdfR int
|
||||
kdfP int
|
||||
salt []byte
|
||||
key []byte // odvozený AES-256 klíč
|
||||
dirty bool
|
||||
plain internalPlain
|
||||
hasIdent bool
|
||||
}
|
||||
|
||||
const (
|
||||
defaultKDFN = 32768
|
||||
defaultKDFR = 8
|
||||
defaultKDFP = 1
|
||||
)
|
||||
|
||||
// CreateEncryptedStore vytvoří nový soubor; error pokud již existuje.
|
||||
func CreateEncryptedStore(path, password string, generateIdentity bool, commonName string) (SecureJSONStore, error) {
|
||||
if err := validatePasswordStrength(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil, fmt.Errorf("store already exists: %s", path)
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
s := &secureJSONStore{
|
||||
path: filepath.Clean(path),
|
||||
kdfN: defaultKDFN,
|
||||
kdfR: defaultKDFR,
|
||||
kdfP: defaultKDFP,
|
||||
plain: internalPlain{Updated: time.Now().UTC(), Data: make(map[string]json.RawMessage)},
|
||||
}
|
||||
if err := s.initNew(password, generateIdentity, commonName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// OpenEncryptedStore načte existující soubor a ověří heslo.
|
||||
func OpenEncryptedStore(path, password string) (SecureJSONStore, error) {
|
||||
if password == "" {
|
||||
return nil, errors.New("empty password")
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &secureJSONStore{path: filepath.Clean(path)}
|
||||
if err := s.load(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// initNew vytvoří salt, odvodí klíč a vytvoří identitu pokud je třeba.
|
||||
func (s *secureJSONStore) initNew(password string, generateIdentity bool, commonName string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := deriveKey(password, salt, s.kdfN, s.kdfR, s.kdfP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.salt = salt
|
||||
s.key = key
|
||||
if generateIdentity {
|
||||
if err := s.generateIdentityLocked(commonName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secureJSONStore) generateIdentityLocked(commonName string) error {
|
||||
// RSA 2048
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
|
||||
pubASN1, _ := x509.MarshalPKIXPublicKey(&pk.PublicKey)
|
||||
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
|
||||
// Optional self-signed cert s CommonName uživatele (fallback pokud prázdný)
|
||||
if strings.TrimSpace(commonName) == "" {
|
||||
commonName = "Local User"
|
||||
}
|
||||
certPEM, _ := generateSelfSignedCert(pk, commonName)
|
||||
_ = s.putLocked("_identity_private_pem", string(privPEM))
|
||||
_ = s.putLocked("_identity_public_pem", string(pubPEM))
|
||||
_ = s.putLocked("_identity_cert_pem", string(certPEM))
|
||||
s.hasIdent = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// deriveKey scrypt.
|
||||
func deriveKey(password string, salt []byte, N, r, p int) ([]byte, error) {
|
||||
return scrypt.Key([]byte(password), salt, N, r, p, 32)
|
||||
}
|
||||
|
||||
// load načte soubor, přečte parametry, odvodí klíč a dešifruje.
|
||||
func (s *secureJSONStore) load(password string) error {
|
||||
b, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var env fileEnvelope
|
||||
if err := json.Unmarshal(b, &env); err != nil {
|
||||
return fmt.Errorf("invalid envelope: %w", err)
|
||||
}
|
||||
if env.Version != 1 || env.KDF != "scrypt" {
|
||||
return errors.New("unsupported version/kdf")
|
||||
}
|
||||
salt, err := base64.StdEncoding.DecodeString(env.Salt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("salt b64: %w", err)
|
||||
}
|
||||
key, err := deriveKey(password, salt, env.N, env.R, env.P)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonce, err := base64.StdEncoding.DecodeString(env.Nonce)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nonce b64: %w", err)
|
||||
}
|
||||
cBytes, err := base64.StdEncoding.DecodeString(env.Cipher)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cipher b64: %w", err)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainBytes, err := gcm.Open(nil, nonce, cBytes, nil)
|
||||
if err != nil {
|
||||
return errors.New("invalid password or corrupted store")
|
||||
}
|
||||
var pl internalPlain
|
||||
if err := json.Unmarshal(plainBytes, &pl); err != nil {
|
||||
return fmt.Errorf("plaintext json: %w", err)
|
||||
}
|
||||
if pl.Data == nil {
|
||||
pl.Data = make(map[string]json.RawMessage)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.kdfN, s.kdfR, s.kdfP = env.N, env.R, env.P
|
||||
s.salt = salt
|
||||
s.key = key
|
||||
s.plain = pl
|
||||
s.dirty = false
|
||||
s.hasIdent = s.hasIdentityLocked()
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secureJSONStore) hasIdentityLocked() bool {
|
||||
_, ok1 := s.plain.Data["_identity_private_pem"]
|
||||
_, ok2 := s.plain.Data["_identity_public_pem"]
|
||||
return ok1 && ok2
|
||||
}
|
||||
|
||||
// Put
|
||||
func (s *secureJSONStore) Put(key string, value any) error {
|
||||
if strings.HasPrefix(key, "_identity_") {
|
||||
return errors.New("reserved identity key prefix")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.putLocked(key, value)
|
||||
}
|
||||
|
||||
func (s *secureJSONStore) putLocked(key string, value any) error {
|
||||
if key == "" {
|
||||
return errors.New("empty key")
|
||||
}
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.plain.Data == nil {
|
||||
s.plain.Data = make(map[string]json.RawMessage)
|
||||
}
|
||||
s.plain.Data[key] = json.RawMessage(b)
|
||||
s.plain.Updated = time.Now().UTC()
|
||||
s.dirty = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get
|
||||
func (s *secureJSONStore) Get(key string, dst any) error {
|
||||
s.mu.RLock()
|
||||
raw, ok := s.plain.Data[key]
|
||||
s.mu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("key not found: %s", key)
|
||||
}
|
||||
return json.Unmarshal(raw, dst)
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (s *secureJSONStore) Delete(key string) error {
|
||||
if strings.HasPrefix(key, "_identity_") {
|
||||
return errors.New("cannot delete identity key")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, ok := s.plain.Data[key]; !ok {
|
||||
return fmt.Errorf("key not found: %s", key)
|
||||
}
|
||||
delete(s.plain.Data, key)
|
||||
s.plain.Updated = time.Now().UTC()
|
||||
s.dirty = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secureJSONStore) Has(key string) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
_, ok := s.plain.Data[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ListKeys (bez identity interních klíčů)
|
||||
func (s *secureJSONStore) ListKeys() []string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]string, 0, len(s.plain.Data))
|
||||
for k := range s.plain.Data {
|
||||
if !strings.HasPrefix(k, "_identity_") {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Flush uloží na disk.
|
||||
func (s *secureJSONStore) Flush() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.dirty {
|
||||
return nil
|
||||
}
|
||||
if len(s.key) == 0 {
|
||||
return errors.New("store not initialized")
|
||||
}
|
||||
plainBytes, err := json.Marshal(s.plain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, err := aes.NewCipher(s.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
cipherBytes := gcm.Seal(nil, nonce, plainBytes, nil)
|
||||
env := fileEnvelope{
|
||||
Version: 1,
|
||||
KDF: "scrypt",
|
||||
N: s.kdfN,
|
||||
R: s.kdfR,
|
||||
P: s.kdfP,
|
||||
Salt: base64.StdEncoding.EncodeToString(s.salt),
|
||||
Nonce: base64.StdEncoding.EncodeToString(nonce),
|
||||
Cipher: base64.StdEncoding.EncodeToString(cipherBytes),
|
||||
}
|
||||
out, err := json.MarshalIndent(env, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := s.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, out, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, s.path); err != nil {
|
||||
return err
|
||||
}
|
||||
s.dirty = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secureJSONStore) Close() error { return s.Flush() }
|
||||
|
||||
// ChangePassword re-derivuje klíč a re-encryptne celý soubor atomicky.
|
||||
func (s *secureJSONStore) ChangePassword(newPassword string) error {
|
||||
if err := validatePasswordStrength(newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
newSalt := make([]byte, 16)
|
||||
if _, err := rand.Read(newSalt); err != nil {
|
||||
return err
|
||||
}
|
||||
newKey, err := deriveKey(newPassword, newSalt, s.kdfN, s.kdfR, s.kdfP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Dočasně si zapamatuj starý klíč pro rollback, i když tady rollback neimplementujeme – chyba se projeví před přepsáním.
|
||||
oldKey, oldSalt := s.key, s.salt
|
||||
s.key, s.salt = newKey, newSalt
|
||||
s.dirty = true
|
||||
if err := s.Flush(); err != nil {
|
||||
// rollback (best effort)
|
||||
s.key, s.salt = oldKey, oldSalt
|
||||
s.dirty = true
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Identity helpers
|
||||
func (s *secureJSONStore) IdentityPrivatePEM() string { return s.getString("_identity_private_pem") }
|
||||
func (s *secureJSONStore) IdentityPublicPEM() string { return s.getString("_identity_public_pem") }
|
||||
func (s *secureJSONStore) IdentityCertPEM() string { return s.getString("_identity_cert_pem") }
|
||||
|
||||
func (s *secureJSONStore) getString(key string) string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if raw, ok := s.plain.Data[key]; ok {
|
||||
var v string
|
||||
if err := json.Unmarshal(raw, &v); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GenerateRandomPassword vytvoří kryptograficky silné heslo ze sady znaků.
|
||||
func GenerateRandomPassword(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", errors.New("invalid length")
|
||||
}
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#-_+="
|
||||
buf := make([]byte, length)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range buf {
|
||||
buf[i] = alphabet[int(b)%len(alphabet)]
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// validatePasswordStrength provede základní kontrolu síly hesla.
|
||||
// Požadavky: min délka 10, alespoň 3 z kategorií (upper, lower, digit, special !@#-_+=)
|
||||
func validatePasswordStrength(pw string) error {
|
||||
if len(pw) < 10 {
|
||||
return errors.New("password too short (min 10)")
|
||||
}
|
||||
var hasU, hasL, hasD, hasS bool
|
||||
for _, r := range pw {
|
||||
switch {
|
||||
case r >= 'A' && r <= 'Z':
|
||||
hasU = true
|
||||
case r >= 'a' && r <= 'z':
|
||||
hasL = true
|
||||
case r >= '0' && r <= '9':
|
||||
hasD = true
|
||||
default:
|
||||
if strings.ContainsRune("!@#-_+=", r) {
|
||||
hasS = true
|
||||
}
|
||||
}
|
||||
}
|
||||
cats := 0
|
||||
if hasU {
|
||||
cats++
|
||||
}
|
||||
if hasL {
|
||||
cats++
|
||||
}
|
||||
if hasD {
|
||||
cats++
|
||||
}
|
||||
if hasS {
|
||||
cats++
|
||||
}
|
||||
if cats < 3 {
|
||||
return errors.New("password too weak (need 3 types: upper, lower, digit, special)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePasswordForUI exportuje validaci pro použití ve Fyne UI.
|
||||
func ValidatePasswordForUI(pw string) error { return validatePasswordStrength(pw) }
|
||||
394
main.go
Normal file → Executable file
394
main.go
Normal file → Executable file
@ -1,25 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -27,10 +22,6 @@ var (
|
||||
pubPEM []byte
|
||||
certPEM []byte // self-signed cert jen pro sdílení identity (volitelné)
|
||||
tmpl *template.Template
|
||||
|
||||
privPath = "identity_key.pem"
|
||||
pubPath = "public.pem"
|
||||
certPath = "identity.crt"
|
||||
)
|
||||
|
||||
type envelope struct {
|
||||
@ -41,292 +32,101 @@ type envelope struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 1) načti nebo vygeneruj klíče
|
||||
if err := loadOrGenerateKeys(); err != nil {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Printf("Chyba: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Malé zpoždění pro případné async logy
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
func runFyne() {
|
||||
storageDir, w := NewUI()
|
||||
vaultPath := filepath.Join(storageDir, "vault.enc")
|
||||
placeholder := widget.NewLabel("Inicializace trezoru…")
|
||||
w.SetContent(placeholder)
|
||||
|
||||
showDialog := func() {
|
||||
ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string, commonName string) {
|
||||
if password == "" { // Cancel nebo zavření dialogu => ukonči app
|
||||
fyne.CurrentApp().Quit()
|
||||
return
|
||||
}
|
||||
var store encrypt.SecureJSONStore
|
||||
var err error
|
||||
if create {
|
||||
store, err = encrypt.CreateEncryptedStore(vaultPath, password, true, commonName)
|
||||
} else {
|
||||
store, err = encrypt.OpenEncryptedStore(vaultPath, password)
|
||||
if err != nil {
|
||||
dialog.NewError(err, w).Show()
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
dialog.NewError(err, w).Show()
|
||||
return
|
||||
}
|
||||
vs, err := NewVaultService(store)
|
||||
if err != nil {
|
||||
dialog.NewError(err, w).Show()
|
||||
return
|
||||
}
|
||||
parts := buildEntries()
|
||||
fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)")
|
||||
w.SetContent(buildTabbedUI(parts, vs, vaultPath))
|
||||
})
|
||||
}
|
||||
|
||||
// Pokud soubor neexistuje, dialog v režimu vytvořit (default). Pokud existuje, uživatel může přepnout.
|
||||
showDialog()
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
func RunWebApp() {
|
||||
// Otevři nebo vytvoř šifrovaný trezor a načti identitu pouze z něj
|
||||
vaultPath := os.Getenv("VAULT_PATH")
|
||||
if vaultPath == "" {
|
||||
vaultPath = "./vault.enc"
|
||||
}
|
||||
pw := os.Getenv("VAULT_PASSWORD")
|
||||
if pw == "" {
|
||||
log.Fatal("VAULT_PASSWORD must be set for web mode")
|
||||
}
|
||||
var store encrypt.SecureJSONStore
|
||||
if _, statErr := os.Stat(vaultPath); os.IsNotExist(statErr) {
|
||||
s, err := encrypt.CreateEncryptedStore(vaultPath, pw, true, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
store = s
|
||||
} else {
|
||||
s, err := encrypt.OpenEncryptedStore(vaultPath, pw)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
store = s
|
||||
}
|
||||
// Načti privátní klíč a veřejné materiály
|
||||
privPEM := store.IdentityPrivatePEM()
|
||||
if privPEM == "" {
|
||||
log.Fatal("missing private key in vault")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(privPEM))
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
log.Fatal("invalid private key PEM in vault")
|
||||
}
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
priv = key
|
||||
pubPEM = []byte(store.IdentityPublicPEM())
|
||||
certPEM = []byte(store.IdentityCertPEM())
|
||||
|
||||
// 2) šablony
|
||||
tmpl = template.Must(template.ParseGlob("templates/*.html"))
|
||||
|
||||
// 3) routing
|
||||
http.HandleFunc("/", indexHandler)
|
||||
http.HandleFunc("/public.pem", publicKeyHandler)
|
||||
http.HandleFunc("/public.crt", publicCertHandler)
|
||||
http.HandleFunc("/encrypt", encryptHandler)
|
||||
http.HandleFunc("/decrypt", decryptHandler)
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
log.Println("Server běží na http://localhost:8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = tmpl.ExecuteTemplate(w, "index.html", nil)
|
||||
}
|
||||
|
||||
func publicKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
w.Write(pubPEM)
|
||||
}
|
||||
|
||||
func publicCertHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
w.Write(certPEM)
|
||||
}
|
||||
|
||||
func encryptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
msg := r.Form.Get("message")
|
||||
peer := r.Form.Get("pubkey") // může to být PEM PUBLIC KEY nebo CERT
|
||||
|
||||
pubKey, err := parsePeerPublicKey(peer)
|
||||
if err != nil {
|
||||
http.Error(w, "Neplatný public key/cert: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// --- hybrid: AES-GCM + RSA-OAEP(SHA-256) ---
|
||||
// 1) vygeneruj náhodný sym. klíč
|
||||
aesKey := make([]byte, 32) // AES-256
|
||||
if _, err := rand.Read(aesKey); err != nil {
|
||||
http.Error(w, "Rand fail", 500)
|
||||
return
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
http.Error(w, "AES fail", 500)
|
||||
return
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
http.Error(w, "GCM fail", 500)
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
http.Error(w, "Nonce fail", 500)
|
||||
return
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, []byte(msg), nil)
|
||||
|
||||
// 2) zašifruj AES klíč cizím RSA klíčem (OAEP SHA-256)
|
||||
label := []byte{} // prázdný label
|
||||
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label)
|
||||
if err != nil {
|
||||
http.Error(w, "RSA-OAEP fail: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
env := envelope{
|
||||
EK: base64.StdEncoding.EncodeToString(ek),
|
||||
N: base64.StdEncoding.EncodeToString(nonce),
|
||||
CT: base64.StdEncoding.EncodeToString(ciphertext),
|
||||
}
|
||||
|
||||
payload, _ := json.MarshalIndent(env, "", " ")
|
||||
_ = tmpl.ExecuteTemplate(w, "encrypt.html", string(payload))
|
||||
}
|
||||
|
||||
func decryptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
in := r.Form.Get("payload")
|
||||
|
||||
var env envelope
|
||||
if err := json.Unmarshal([]byte(in), &env); err != nil {
|
||||
http.Error(w, "Neplatné JSON", 400)
|
||||
return
|
||||
}
|
||||
|
||||
ek, err := base64.StdEncoding.DecodeString(env.EK)
|
||||
if err != nil {
|
||||
http.Error(w, "ek base64", 400)
|
||||
return
|
||||
}
|
||||
nonce, err := base64.StdEncoding.DecodeString(env.N)
|
||||
if err != nil {
|
||||
http.Error(w, "nonce base64", 400)
|
||||
return
|
||||
}
|
||||
ct, err := base64.StdEncoding.DecodeString(env.CT)
|
||||
if err != nil {
|
||||
http.Error(w, "ct base64", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 1) rozšifruj AES klíč naším RSA
|
||||
label := []byte{}
|
||||
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, label)
|
||||
if err != nil {
|
||||
http.Error(w, "RSA-OAEP decrypt fail: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
http.Error(w, "AES fail", 500)
|
||||
return
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
http.Error(w, "GCM fail", 500)
|
||||
return
|
||||
}
|
||||
plain, err := gcm.Open(nil, nonce, ct, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "GCM open fail: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
_ = tmpl.ExecuteTemplate(w, "decrypt.html", string(plain))
|
||||
}
|
||||
|
||||
// parsePeerPublicKey umí vzít buď PEM PUBLIC KEY, nebo PEM CERT a vrátí *rsa.PublicKey
|
||||
func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(pemOrCert))
|
||||
if block == nil {
|
||||
return nil, errf("nenašel jsem PEM blok")
|
||||
}
|
||||
switch block.Type {
|
||||
case "PUBLIC KEY":
|
||||
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsaPub, ok := k.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errf("očekávám RSA PUBLIC KEY")
|
||||
}
|
||||
return rsaPub, nil
|
||||
case "CERTIFICATE":
|
||||
c, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsaPub, ok := c.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errf("cert neobsahuje RSA klíč")
|
||||
}
|
||||
return rsaPub, nil
|
||||
default:
|
||||
return nil, errf("nepodporovaný PEM typ: %s", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func generateSelfSignedCert(pub *rsa.PublicKey) []byte {
|
||||
tpl := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Encryptor Local Identity",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
// Tenhle cert je jen „vizitka“ (není pro TLS).
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
der, _ := x509.CreateCertificate(rand.Reader, &tpl, &tpl, pub, priv)
|
||||
buf := &bytes.Buffer{}
|
||||
_ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// malá helper chyba
|
||||
type strErr string
|
||||
|
||||
func (e strErr) Error() string { return string(e) }
|
||||
func errf(s string, a ...any) error { return strErr(fmtS(s, a...)) }
|
||||
func fmtS(format string, a ...any) string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(format)
|
||||
if len(a) > 0 {
|
||||
b.WriteString(": ")
|
||||
}
|
||||
for i, v := range a {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(anyToString(v))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
func anyToString(v any) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
j, _ := json.Marshal(t)
|
||||
return string(j)
|
||||
}
|
||||
}
|
||||
|
||||
func loadOrGenerateKeys() error {
|
||||
// existuje private key?
|
||||
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
|
||||
// načti privátní klíč
|
||||
pkBytes, err := os.ReadFile(privPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, _ := pem.Decode(pkBytes)
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
return fmt.Errorf("invalid private key PEM")
|
||||
}
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
priv = key
|
||||
|
||||
// načti public & cert
|
||||
pubPEM, _ = os.ReadFile(pubPath)
|
||||
certPEM, _ = os.ReadFile(certPath)
|
||||
|
||||
log.Println("Načtena existující identita z disku.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// jinak vygeneruj nové
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
priv = key
|
||||
|
||||
// public
|
||||
pubASN1, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
|
||||
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
|
||||
|
||||
// cert
|
||||
certPEM = generateSelfSignedCert(&priv.PublicKey)
|
||||
|
||||
// ulož
|
||||
if err := os.WriteFile(privPath,
|
||||
pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}),
|
||||
fs.FileMode(0600)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(pubPath, pubPEM, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Vygenerována nová identita a uložena na disk.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
info, err := os.Stat(p)
|
||||
return err == nil && !info.IsDir()
|
||||
muxServer := NewServer()
|
||||
log.Fatal(http.ListenAndServe(":8080", muxServer))
|
||||
}
|
||||
|
||||
31
qr_manual_test.go
Normal file
31
qr_manual_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/png"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// go test -run TestDecodeQR -v
|
||||
func TestDecodeQR(t *testing.T) {
|
||||
path := os.Getenv("QR_TEST_FILE")
|
||||
if path == "" {
|
||||
t.Skip("set QR_TEST_FILE to a PNG path to run")
|
||||
return
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
txt, err := DecodeQR(img)
|
||||
if err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
t.Logf("decoded: %d bytes", len(txt))
|
||||
}
|
||||
|
||||
520
qr_support.go
Executable file
520
qr_support.go
Executable file
@ -0,0 +1,520 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"log"
|
||||
"math"
|
||||
"os/exec"
|
||||
|
||||
"github.com/liyue201/goqr"
|
||||
"github.com/makiuchi-d/gozxing"
|
||||
qrx "github.com/makiuchi-d/gozxing/qrcode"
|
||||
qrgen "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// GenerateQRPNG returns PNG bytes for the given text.
|
||||
func GenerateQRPNG(text string, size int) ([]byte, error) {
|
||||
if size <= 0 {
|
||||
size = 256
|
||||
}
|
||||
// Use Low EC for larger modules; improves decode reliability for long payloads
|
||||
png, err := qrgen.Encode(text, qrgen.Low, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return png, nil
|
||||
}
|
||||
|
||||
// DecodeQR decodes first QR code text from an image.
|
||||
func DecodeQR(img image.Image) (string, error) {
|
||||
log.Printf("[qr] start decode: %dx%d %T", img.Bounds().Dx(), img.Bounds().Dy(), img)
|
||||
// Try basic decode first
|
||||
codes, err := goqr.Recognize(img)
|
||||
if err == nil && len(codes) > 0 {
|
||||
log.Printf("[qr] basic success length=%d", len(codes))
|
||||
return string(codes[0].Payload), nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[qr] basic error: %v", err)
|
||||
} else {
|
||||
log.Printf("[qr] basic no codes")
|
||||
}
|
||||
|
||||
// Convert palette/indexed images to RGBA first to avoid issues with goqr
|
||||
if _, ok := img.ColorModel().(color.Palette); ok {
|
||||
bounds := img.Bounds()
|
||||
rgba := image.NewRGBA(bounds)
|
||||
draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
|
||||
img = rgba
|
||||
|
||||
// Try again after conversion
|
||||
codes, err := goqr.Recognize(img)
|
||||
if err == nil && len(codes) > 0 {
|
||||
log.Printf("[qr] palette-convert success")
|
||||
return string(codes[0].Payload), nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[qr] palette-convert err: %v", err)
|
||||
} else {
|
||||
log.Printf("[qr] palette-convert no codes")
|
||||
}
|
||||
}
|
||||
|
||||
// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
|
||||
attempts := []image.Image{}
|
||||
add := func(im image.Image) {
|
||||
if im != nil {
|
||||
attempts = append(attempts, im)
|
||||
}
|
||||
}
|
||||
add(img)
|
||||
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
|
||||
// Helper: nearest-neighbour scale
|
||||
scale := func(src image.Image, fw, fh int) image.Image {
|
||||
if fw <= 0 || fh <= 0 {
|
||||
return src
|
||||
}
|
||||
dst := image.NewNRGBA(image.Rect(0, 0, fw, fh))
|
||||
sb := src.Bounds()
|
||||
sw, sh := float64(sb.Dx()), float64(sb.Dy())
|
||||
for y := 0; y < fh; y++ {
|
||||
sy := int(float64(y)/float64(fh)*sh + 0.5)
|
||||
if sy >= sb.Dy() {
|
||||
sy = sb.Dy() - 1
|
||||
}
|
||||
for x := 0; x < fw; x++ {
|
||||
sx := int(float64(x)/float64(fw)*sw + 0.5)
|
||||
if sx >= sb.Dx() {
|
||||
sx = sb.Dx() - 1
|
||||
}
|
||||
dst.Set(x, y, src.At(sb.Min.X+sx, sb.Min.Y+sy))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Helper: binarize with adaptive threshold (simple mean + variance fudge)
|
||||
binarize := func(src image.Image) image.Image {
|
||||
b := src.Bounds()
|
||||
gray := image.NewGray(b)
|
||||
var sum, sum2 float64
|
||||
total := float64(b.Dx() * b.Dy())
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
r, g, bl, _ := src.At(x, y).RGBA()
|
||||
lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8) // 0-255 approx
|
||||
sum += lum
|
||||
sum2 += lum * lum
|
||||
}
|
||||
}
|
||||
mean := sum / total
|
||||
variance := (sum2 / total) - mean*mean
|
||||
stddev := math.Sqrt(math.Max(variance, 0))
|
||||
// threshold tweaks: lower slightly for dark backgrounds
|
||||
thr := mean - stddev*0.15
|
||||
if thr < 50 {
|
||||
thr = 50
|
||||
}
|
||||
if thr > 200 {
|
||||
thr = 200
|
||||
}
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
r, g, bl, _ := src.At(x, y).RGBA()
|
||||
lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8)
|
||||
var c color.Gray
|
||||
if lum < thr {
|
||||
c = color.Gray{Y: 0}
|
||||
} else {
|
||||
c = color.Gray{Y: 255}
|
||||
}
|
||||
gray.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
return gray
|
||||
}
|
||||
|
||||
// Scale strategy: even if image is not tiny, scaled variants sometimes help goqr lock onto module grid
|
||||
// 1) If very small, aggressive scale
|
||||
if w < 280 || h < 280 {
|
||||
factor := 2
|
||||
if w < 160 {
|
||||
factor = 3
|
||||
}
|
||||
if w < 100 {
|
||||
factor = 4
|
||||
}
|
||||
add(scale(img, w*factor, h*factor))
|
||||
}
|
||||
// 2) Always try moderate upscale variants for mid-sized screenshots (helps when original was downscaled with interpolation)
|
||||
if w >= 180 && w <= 900 { // avoid exploding huge images
|
||||
add(scale(img, w*2, h*2))
|
||||
if w*3 < 2000 && h*3 < 2000 { // guard upper bound
|
||||
add(scale(img, w*3, h*3))
|
||||
}
|
||||
}
|
||||
|
||||
// Add binarized versions (original + scaled)
|
||||
add(binarize(img))
|
||||
if w < 320 || h < 320 {
|
||||
factor := 2
|
||||
add(binarize(scale(img, w*factor, h*factor)))
|
||||
}
|
||||
// Also binarize any newly added large upscales (skip if too big)
|
||||
for _, cand := range attempts { // safe: attempts already contains originals
|
||||
cw := cand.Bounds().Dx()
|
||||
ch := cand.Bounds().Dy()
|
||||
if (cw <= 1400 && ch <= 1400) && (cw > w || ch > h) { // only for upscaled variants within limit
|
||||
add(binarize(cand))
|
||||
}
|
||||
}
|
||||
|
||||
// Add contrast-enhanced alpha-flattened variant
|
||||
flatten := func(src image.Image) image.Image {
|
||||
b := src.Bounds()
|
||||
dst := image.NewRGBA(b)
|
||||
draw.Draw(dst, b, image.Black, image.Point{}, draw.Src)
|
||||
draw.Draw(dst, b, src, b.Min, draw.Over)
|
||||
return dst
|
||||
}
|
||||
add(flatten(img))
|
||||
|
||||
// Rotations (90,180,270) in case of orientation issues
|
||||
rotate := func(src image.Image) []image.Image {
|
||||
b := src.Bounds()
|
||||
w := b.Dx()
|
||||
h := b.Dy()
|
||||
r90 := image.NewRGBA(image.Rect(0, 0, h, w))
|
||||
r180 := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
r270 := image.NewRGBA(image.Rect(0, 0, h, w))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
c := src.At(b.Min.X+x, b.Min.Y+y)
|
||||
// 90
|
||||
r90.Set(h-1-y, x, c)
|
||||
// 180
|
||||
r180.Set(w-1-x, h-1-y, c)
|
||||
// 270
|
||||
r270.Set(y, w-1-x, c)
|
||||
}
|
||||
}
|
||||
return []image.Image{r90, r180, r270}
|
||||
}
|
||||
for _, r := range rotate(img) {
|
||||
add(r)
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for idx, attempt := range attempts {
|
||||
codes, err := goqr.Recognize(attempt)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
log.Printf("[qr] attempt %d error: %v", idx, err)
|
||||
continue
|
||||
}
|
||||
if len(codes) > 0 {
|
||||
log.Printf("[qr] attempt %d success (%d codes)", idx, len(codes))
|
||||
return string(codes[0].Payload), nil
|
||||
}
|
||||
log.Printf("[qr] attempt %d no codes", idx)
|
||||
}
|
||||
|
||||
// ZXing helper used across multiple branches
|
||||
decodeZX := func(src image.Image, hints map[gozxing.DecodeHintType]interface{}) (string, error) {
|
||||
binSrc := gozxing.NewLuminanceSourceFromImage(src)
|
||||
// Try hybrid first (usually better for photos)
|
||||
bmpH, _ := gozxing.NewBinaryBitmap(gozxing.NewHybridBinarizer(binSrc))
|
||||
reader := qrx.NewQRCodeReader()
|
||||
if res, err := reader.Decode(bmpH, hints); err == nil {
|
||||
return res.GetText(), nil
|
||||
}
|
||||
// Fallback to global histogram
|
||||
bmpG, _ := gozxing.NewBinaryBitmap(gozxing.NewGlobalHistgramBinarizer(binSrc))
|
||||
if res, err := reader.Decode(bmpG, hints); err == nil {
|
||||
return res.GetText(), nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
// Also try ZXing on all preprocessed attempts
|
||||
for idx, attempt := range attempts {
|
||||
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
gozxing.DecodeHintType_PURE_BARCODE: true,
|
||||
}); err == nil && txt != "" {
|
||||
log.Printf("[qr] zxing attempt %d success (pure)", idx)
|
||||
return txt, nil
|
||||
}
|
||||
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
}); err == nil && txt != "" {
|
||||
log.Printf("[qr] zxing attempt %d success (try-harder)", idx)
|
||||
return txt, nil
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
log.Printf("[qr] preprocess firstErr: %v", firstErr)
|
||||
}
|
||||
|
||||
// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
|
||||
addQuietZone := func(src image.Image) image.Image {
|
||||
b := src.Bounds()
|
||||
pad := int(math.Max(float64(b.Dx()/32), 8))
|
||||
out := image.NewRGBA(image.Rect(0, 0, b.Dx()+pad*2, b.Dy()+pad*2))
|
||||
// fill white
|
||||
for y := 0; y < out.Bounds().Dy(); y++ {
|
||||
for x := 0; x < out.Bounds().Dx(); x++ {
|
||||
out.Set(x, y, color.White)
|
||||
}
|
||||
}
|
||||
draw.Draw(out, b.Add(image.Point{X: pad, Y: pad}), src, b.Min, draw.Src)
|
||||
return out
|
||||
}
|
||||
invert := func(src image.Image) image.Image {
|
||||
b := src.Bounds()
|
||||
out := image.NewRGBA(b)
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
r, g, bb, a := src.At(x, y).RGBA()
|
||||
out.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - bb/257), uint8(a / 257)})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
// estimate dark pixel ratio
|
||||
b := img.Bounds()
|
||||
var dark, total int
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
r, g, bb, _ := img.At(x, y).RGBA()
|
||||
lum := (r*299 + g*587 + bb*114) / 1000
|
||||
if lum < 128<<8 {
|
||||
dark++
|
||||
}
|
||||
total++
|
||||
}
|
||||
}
|
||||
ratio := float64(dark) / float64(total)
|
||||
needInvert := ratio > 0.85 // almost full dark background (white modules?)
|
||||
var modified []image.Image
|
||||
base := img
|
||||
if needInvert {
|
||||
base = invert(base)
|
||||
}
|
||||
modified = append(modified, addQuietZone(base))
|
||||
modified = append(modified, addQuietZone(invert(base)))
|
||||
for _, m := range modified {
|
||||
if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
|
||||
log.Printf("[qr] quiet-zone/invert success")
|
||||
return string(codes[0].Payload), nil
|
||||
}
|
||||
// Also try ZXing on these variants before moving on
|
||||
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
gozxing.DecodeHintType_PURE_BARCODE: true,
|
||||
}); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing success (quiet-zone+pure)")
|
||||
return txt, nil
|
||||
}
|
||||
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
}); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing success (quiet-zone+try-harder)")
|
||||
return txt, nil
|
||||
}
|
||||
}
|
||||
// ZXing with TRY_HARDER and PURE_BARCODE hints which often fix dense PNGs
|
||||
hints := map[gozxing.DecodeHintType]interface{}{
|
||||
gozxing.DecodeHintType_TRY_HARDER: true,
|
||||
// Many of our images are perfect render PNGs from go-qrcode -> enable PURE_BARCODE
|
||||
gozxing.DecodeHintType_PURE_BARCODE: true,
|
||||
}
|
||||
if txt, err := decodeZX(base, hints); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing success (hints)")
|
||||
return txt, nil
|
||||
} else if err != nil {
|
||||
log.Printf("[qr] gozxing(hints) err: %v", err)
|
||||
}
|
||||
// Try again without PURE_BARCODE in case quiet zone is cropped
|
||||
hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
|
||||
if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing success (try-harder)")
|
||||
return txt, nil
|
||||
} else if err != nil {
|
||||
log.Printf("[qr] gozxing(try-harder) err: %v", err)
|
||||
}
|
||||
// Try ZXing on rotations as well
|
||||
for _, r := range rotate(base) {
|
||||
if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing success (rotated)")
|
||||
return txt, nil
|
||||
}
|
||||
if txt, err := decodeZX(r, hints); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing success (rotated pure)")
|
||||
return txt, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Extra pure-QR recovery: try small insets and simple upscales with PURE_BARCODE.
|
||||
inset := func(src image.Image, px int) image.Image {
|
||||
if px <= 0 {
|
||||
return src
|
||||
}
|
||||
b := src.Bounds()
|
||||
r := image.Rect(b.Min.X+px, b.Min.Y+px, b.Max.X-px, b.Max.Y-px)
|
||||
if r.Dx() <= 10 || r.Dy() <= 10 {
|
||||
return src
|
||||
}
|
||||
out := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
|
||||
draw.Draw(out, out.Bounds(), src, r.Min, draw.Src)
|
||||
return out
|
||||
}
|
||||
cropSides := func(src image.Image, l, t, r, b int) image.Image {
|
||||
bb := src.Bounds()
|
||||
rr := image.Rect(bb.Min.X+l, bb.Min.Y+t, bb.Max.X-r, bb.Max.Y-b)
|
||||
if rr.Dx() <= 10 || rr.Dy() <= 10 || rr.Min.X >= rr.Max.X || rr.Min.Y >= rr.Max.Y {
|
||||
return src
|
||||
}
|
||||
out := image.NewRGBA(image.Rect(0, 0, rr.Dx(), rr.Dy()))
|
||||
draw.Draw(out, out.Bounds(), src, rr.Min, draw.Src)
|
||||
return out
|
||||
}
|
||||
tryPure := func(src image.Image) (string, bool) {
|
||||
for _, px := range []int{1, 2, 3} {
|
||||
if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing pure+inset %d success", px)
|
||||
return txt, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
// Try on base, binarized base and scaled variants
|
||||
if txt, ok := tryPure(base); ok {
|
||||
return txt, nil
|
||||
}
|
||||
if txt, ok := tryPure(binarize(base)); ok {
|
||||
return txt, nil
|
||||
}
|
||||
if w < 800 && h < 800 { // upscale to mitigate rounding of module size
|
||||
if txt, ok := tryPure(scale(base, w*2, h*2)); ok {
|
||||
return txt, nil
|
||||
}
|
||||
if txt, ok := tryPure(scale(base, w*3, h*3)); ok {
|
||||
return txt, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Brute-force small asymmetric crops to satisfy ZXing's 1 (mod 4) dimension constraint.
|
||||
for _, src := range []image.Image{base, binarize(base)} {
|
||||
for l := 0; l <= 2; l++ {
|
||||
for t := 0; t <= 2; t++ {
|
||||
for r := 0; r <= 2; r++ {
|
||||
for btm := 0; btm <= 2; btm++ {
|
||||
cand := cropSides(src, l, t, r, btm)
|
||||
if cand == src {
|
||||
continue
|
||||
}
|
||||
if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing pure+asym inset %d,%d,%d,%d success", l, t, r, btm)
|
||||
return txt, nil
|
||||
}
|
||||
if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing asym inset %d,%d,%d,%d success", l, t, r, btm)
|
||||
return txt, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bounding box crop of dark pixels, then upscale and retry
|
||||
boundsAll := base.Bounds()
|
||||
minX, minY := boundsAll.Max.X, boundsAll.Max.Y
|
||||
maxX, maxY := boundsAll.Min.X, boundsAll.Min.Y
|
||||
for y := boundsAll.Min.Y; y < boundsAll.Max.Y; y++ {
|
||||
for x := boundsAll.Min.X; x < boundsAll.Max.X; x++ {
|
||||
r, g, bb, _ := base.At(x, y).RGBA()
|
||||
lum := (r*299 + g*587 + bb*114) / 1000
|
||||
if lum < 180<<8 { // treat as dark
|
||||
if x < minX {
|
||||
minX = x
|
||||
}
|
||||
if y < minY {
|
||||
minY = y
|
||||
}
|
||||
if x > maxX {
|
||||
maxX = x
|
||||
}
|
||||
if y > maxY {
|
||||
maxY = y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if minX < maxX && minY < maxY {
|
||||
crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
|
||||
draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
|
||||
log.Printf("[qr] crop bbox size=%dx%d", crop.Bounds().Dx(), crop.Bounds().Dy())
|
||||
for _, up := range []int{2, 3, 4} {
|
||||
cw := crop.Bounds().Dx() * up
|
||||
ch := crop.Bounds().Dy() * up
|
||||
if cw > 2400 || ch > 2400 {
|
||||
continue
|
||||
}
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, cw, ch))
|
||||
for y := 0; y < ch; y++ {
|
||||
sy := y / up
|
||||
for x := 0; x < cw; x++ {
|
||||
sx := x / up
|
||||
scaled.Set(x, y, crop.At(sx, sy))
|
||||
}
|
||||
}
|
||||
if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
|
||||
log.Printf("[qr] gozxing crop+scale success up=%d", up)
|
||||
return txt, nil
|
||||
} else if err != nil {
|
||||
log.Printf("[qr] crop up=%d fail: %v", up, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
return "", firstErr
|
||||
}
|
||||
// Final external fallback: try zbarimg if available on the system
|
||||
if cmd, lookErr := exec.LookPath("zbarimg"); lookErr == nil {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := png.Encode(buf, img); err == nil {
|
||||
c := exec.Command(cmd, "-q", "--raw", "-")
|
||||
c.Stdin = bytes.NewReader(buf.Bytes())
|
||||
out, err := c.Output()
|
||||
if err == nil && len(out) > 0 {
|
||||
log.Printf("[qr] zbarimg success")
|
||||
return string(out), nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[qr] zbarimg error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("no qr code found")
|
||||
}
|
||||
|
||||
// LoadPNG decodes raw PNG bytes to image.Image.
|
||||
func LoadPNG(b []byte) (image.Image, error) {
|
||||
im, err := png.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return im, nil
|
||||
}
|
||||
159
server.go
Executable file
159
server.go
Executable file
@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
encrypt "fckeuspy-go/lib"
|
||||
)
|
||||
|
||||
func NewServer() *http.ServeMux {
|
||||
server := &http.ServeMux{}
|
||||
|
||||
// 3) routing
|
||||
server.HandleFunc("/", indexHandler)
|
||||
server.HandleFunc("/public.pem", publicKeyHandler)
|
||||
server.HandleFunc("/public.crt", publicCertHandler)
|
||||
server.HandleFunc("/encrypt", encryptHandler)
|
||||
server.HandleFunc("/decrypt", decryptHandler)
|
||||
server.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
// log.Println("Server běží na http://localhost:8080")
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = tmpl.ExecuteTemplate(w, "index.html", nil)
|
||||
}
|
||||
|
||||
func publicKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
w.Write(pubPEM)
|
||||
}
|
||||
|
||||
func publicCertHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
w.Write(certPEM)
|
||||
}
|
||||
|
||||
func encryptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
msg := r.Form.Get("message")
|
||||
peer := r.Form.Get("pubkey") // může to být PEM PUBLIC KEY nebo CERT
|
||||
|
||||
pubKey, err := encrypt.ParsePeerPublicKey(peer)
|
||||
if err != nil {
|
||||
http.Error(w, "Neplatný public key/cert: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// --- hybrid: AES-GCM + RSA-OAEP(SHA-256) ---
|
||||
// 1) vygeneruj náhodný sym. klíč
|
||||
aesKey := make([]byte, 32) // AES-256
|
||||
if _, err := rand.Read(aesKey); err != nil {
|
||||
http.Error(w, "Rand fail", 500)
|
||||
return
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
http.Error(w, "AES fail", 500)
|
||||
return
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
http.Error(w, "GCM fail", 500)
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
http.Error(w, "Nonce fail", 500)
|
||||
return
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, []byte(msg), nil)
|
||||
|
||||
// 2) zašifruj AES klíč cizím RSA klíčem (OAEP SHA-256)
|
||||
label := []byte{} // prázdný label
|
||||
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label)
|
||||
if err != nil {
|
||||
http.Error(w, "RSA-OAEP fail: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
env := envelope{
|
||||
EK: base64.StdEncoding.EncodeToString(ek),
|
||||
N: base64.StdEncoding.EncodeToString(nonce),
|
||||
CT: base64.StdEncoding.EncodeToString(ciphertext),
|
||||
}
|
||||
|
||||
payload, _ := json.MarshalIndent(env, "", " ")
|
||||
_ = tmpl.ExecuteTemplate(w, "encrypt.html", string(payload))
|
||||
}
|
||||
|
||||
func decryptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
in := r.Form.Get("payload")
|
||||
|
||||
var env envelope
|
||||
if err := json.Unmarshal([]byte(in), &env); err != nil {
|
||||
http.Error(w, "Neplatné JSON", 400)
|
||||
return
|
||||
}
|
||||
|
||||
ek, err := base64.StdEncoding.DecodeString(env.EK)
|
||||
if err != nil {
|
||||
http.Error(w, "ek base64", 400)
|
||||
return
|
||||
}
|
||||
nonce, err := base64.StdEncoding.DecodeString(env.N)
|
||||
if err != nil {
|
||||
http.Error(w, "nonce base64", 400)
|
||||
return
|
||||
}
|
||||
ct, err := base64.StdEncoding.DecodeString(env.CT)
|
||||
if err != nil {
|
||||
http.Error(w, "ct base64", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 1) rozšifruj AES klíč naším RSA
|
||||
label := []byte{}
|
||||
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, label)
|
||||
if err != nil {
|
||||
http.Error(w, "RSA-OAEP decrypt fail: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
http.Error(w, "AES fail", 500)
|
||||
return
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
http.Error(w, "GCM fail", 500)
|
||||
return
|
||||
}
|
||||
plain, err := gcm.Open(nil, nonce, ct, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "GCM open fail: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
_ = tmpl.ExecuteTemplate(w, "decrypt.html", string(plain))
|
||||
}
|
||||
0
static/style.css
Normal file → Executable file
0
static/style.css
Normal file → Executable file
0
templates/decrypt.html
Normal file → Executable file
0
templates/decrypt.html
Normal file → Executable file
0
templates/encrypt.html
Normal file → Executable file
0
templates/encrypt.html
Normal file → Executable file
0
templates/index.html
Normal file → Executable file
0
templates/index.html
Normal file → Executable file
846
ui.go
Executable file
846
ui.go
Executable file
@ -0,0 +1,846 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// --- Core UI model ---
|
||||
type uiParts struct {
|
||||
outKey, msg, peer, cipherOut, payload, plainOut *widget.Entry
|
||||
toastLabel *widget.Label
|
||||
cipherQR, pubQR, crtQR, peerQR, payloadQR *canvas.Image
|
||||
showQR, showPeerQR, showPayloadQR bool
|
||||
}
|
||||
|
||||
func buildEntries() *uiParts {
|
||||
p := &uiParts{
|
||||
outKey: widget.NewMultiLineEntry(),
|
||||
msg: widget.NewMultiLineEntry(),
|
||||
peer: widget.NewMultiLineEntry(),
|
||||
cipherOut: widget.NewMultiLineEntry(),
|
||||
payload: widget.NewMultiLineEntry(),
|
||||
plainOut: widget.NewMultiLineEntry(),
|
||||
toastLabel: widget.NewLabel(""),
|
||||
cipherQR: canvas.NewImageFromImage(nil),
|
||||
pubQR: canvas.NewImageFromImage(nil),
|
||||
crtQR: canvas.NewImageFromImage(nil),
|
||||
peerQR: canvas.NewImageFromImage(nil),
|
||||
payloadQR: canvas.NewImageFromImage(nil),
|
||||
showQR: true, showPeerQR: true, showPayloadQR: true,
|
||||
}
|
||||
p.cipherQR.SetMinSize(fyne.NewSize(220, 220))
|
||||
p.pubQR.SetMinSize(fyne.NewSize(200, 200))
|
||||
p.peerQR.SetMinSize(fyne.NewSize(200, 200))
|
||||
p.payloadQR.SetMinSize(fyne.NewSize(220, 220))
|
||||
p.toastLabel.Hide()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *uiParts) showToast(s string) {
|
||||
fyne.Do(func() { p.toastLabel.SetText(s); p.toastLabel.Show() })
|
||||
time.AfterFunc(1500*time.Millisecond, func() {
|
||||
fyne.Do(func() {
|
||||
if p.toastLabel.Text == s {
|
||||
p.toastLabel.Hide()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Theme
|
||||
type simpleTheme struct{}
|
||||
|
||||
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
|
||||
if n == theme.ColorNameBackground {
|
||||
return color.NRGBA{24, 27, 31, 255}
|
||||
}
|
||||
return theme.DefaultTheme().Color(n, v)
|
||||
}
|
||||
func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(st) }
|
||||
func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) }
|
||||
func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) }
|
||||
|
||||
// Facade interface
|
||||
type ServiceFacade interface {
|
||||
Encrypt(msg, peer string) (string, error)
|
||||
Decrypt(json string) (string, error)
|
||||
PublicPEM() string
|
||||
PublicCert() string
|
||||
ListContacts() ([]Contact, error)
|
||||
SaveContact(c Contact) error
|
||||
DeleteContact(id string) error
|
||||
}
|
||||
|
||||
// Clipboard helpers
|
||||
func copyClip(s string, parts *uiParts) {
|
||||
fyne.CurrentApp().Clipboard().SetContent(s)
|
||||
parts.showToast("Zkopírováno")
|
||||
}
|
||||
|
||||
func copyImageToClipboard(img image.Image, parts *uiParts) {
|
||||
if img == nil {
|
||||
return
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := png.Encode(buf, img); err != nil {
|
||||
parts.showToast("Chyba PNG")
|
||||
return
|
||||
}
|
||||
choose := func() *exec.Cmd {
|
||||
wayland := os.Getenv("WAYLAND_DISPLAY") != ""
|
||||
has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
|
||||
if wayland {
|
||||
if has("wl-copy") {
|
||||
return exec.Command("wl-copy", "--type", "image/png")
|
||||
}
|
||||
if has("xclip") {
|
||||
return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
|
||||
}
|
||||
} else {
|
||||
if has("xclip") {
|
||||
return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
|
||||
}
|
||||
if has("wl-copy") {
|
||||
return exec.Command("wl-copy", "--type", "image/png")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if choose == nil {
|
||||
parts.showToast("Chybí wl-copy/xclip")
|
||||
return
|
||||
}
|
||||
stdin, _ := choose.StdinPipe()
|
||||
if err := choose.Start(); err != nil {
|
||||
parts.showToast("Nelze spustit")
|
||||
return
|
||||
}
|
||||
_, _ = stdin.Write(buf.Bytes())
|
||||
_ = stdin.Close()
|
||||
if err := choose.Wait(); err != nil {
|
||||
parts.showToast("Selhalo")
|
||||
return
|
||||
}
|
||||
parts.showToast("QR obrázek ve schránce")
|
||||
}
|
||||
|
||||
func readImageClipboard() (image.Image, error) {
|
||||
has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
|
||||
tryDecode := func(d []byte) (image.Image, bool) {
|
||||
if len(d) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
if img, _, e := image.Decode(bytes.NewReader(d)); e == nil {
|
||||
return img, true
|
||||
}
|
||||
s := strings.TrimSpace(string(d))
|
||||
if strings.HasPrefix(s, "data:image") {
|
||||
if p := strings.Index(s, ","); p > 0 {
|
||||
s = s[p+1:]
|
||||
}
|
||||
}
|
||||
if raw, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||
if img, _, e2 := image.Decode(bytes.NewReader(raw)); e2 == nil {
|
||||
return img, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") {
|
||||
if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil {
|
||||
log.Printf("[clip] wl types: %s", strings.TrimSpace(string(types)))
|
||||
pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"}
|
||||
seen := map[string]bool{}
|
||||
for _, p := range pref {
|
||||
seen[p] = true
|
||||
}
|
||||
order := append([]string{}, pref...)
|
||||
for _, t := range strings.Split(string(types), "\n") {
|
||||
t = strings.TrimSpace(t)
|
||||
if t == "" || !strings.HasPrefix(t, "image/") {
|
||||
continue
|
||||
}
|
||||
if !seen[t] {
|
||||
order = append(order, t)
|
||||
}
|
||||
}
|
||||
for _, t := range order {
|
||||
if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil {
|
||||
log.Printf("[clip] got type %s size=%d", t, len(data))
|
||||
if img, ok := tryDecode(data); ok {
|
||||
// save debug
|
||||
_ = os.MkdirAll("qr_debug", 0o755)
|
||||
fn := filepath.Join("qr_debug", fmt.Sprintf("clip_raw_%d.png", time.Now().UnixNano()))
|
||||
if f, e := os.Create(fn); e == nil {
|
||||
_ = png.Encode(f, img)
|
||||
f.Close()
|
||||
log.Printf("[clip] saved %s", fn)
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if data, err := exec.Command("wl-paste").Output(); err == nil {
|
||||
log.Printf("[clip] generic wl-paste size=%d", len(data))
|
||||
if img, ok := tryDecode(data); ok {
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if has("xclip") {
|
||||
for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} {
|
||||
if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil {
|
||||
log.Printf("[clip] xclip type %s size=%d", m, len(data))
|
||||
if img, ok := tryDecode(data); ok {
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
|
||||
log.Printf("[clip] xclip raw size=%d", len(data))
|
||||
if img, ok := tryDecode(data); ok {
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("nenalezen žádný obrázek ve schránce")
|
||||
}
|
||||
|
||||
// Identity tab
|
||||
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||
// Toolbar: choose what to encode into the QR to keep density manageable
|
||||
mode := "combined" // combined, cert, pub
|
||||
chooser := widget.NewSelect([]string{"Veřejný+Cert", "Jen Cert", "Jen Veřejný"}, func(string) {})
|
||||
chooser.Selected = "Veřejný+Cert"
|
||||
var update func()
|
||||
chooser.OnChanged = func(v string) {
|
||||
switch v {
|
||||
case "Jen Cert":
|
||||
mode = "cert"
|
||||
case "Jen Veřejný":
|
||||
mode = "pub"
|
||||
default:
|
||||
mode = "combined"
|
||||
}
|
||||
update()
|
||||
}
|
||||
deleteBtn := widget.NewButton("Smazat identitu", func() {
|
||||
pw := widget.NewPasswordEntry()
|
||||
form := widget.NewForm(widget.NewFormItem("Heslo", pw))
|
||||
warn := widget.NewLabel("Smazat vše?")
|
||||
d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", container.NewVBox(warn, form), func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw.Text); err != nil {
|
||||
dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show()
|
||||
return
|
||||
}
|
||||
_ = os.Remove(vaultPath)
|
||||
fyne.CurrentApp().Quit()
|
||||
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
||||
d.Resize(fyne.NewSize(420, 200))
|
||||
d.Show()
|
||||
})
|
||||
makeQR := func(data string, target *canvas.Image) {
|
||||
if data == "" {
|
||||
target.Image = nil
|
||||
target.Refresh()
|
||||
return
|
||||
}
|
||||
if b, err := GenerateQRPNG(data, 512); err == nil {
|
||||
if im, err2 := LoadPNG(b); err2 == nil {
|
||||
target.Image = im
|
||||
target.Refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
update = func() {
|
||||
if parts.showQR {
|
||||
var text string
|
||||
switch mode {
|
||||
case "cert":
|
||||
text = strings.TrimSpace(svc.PublicCert())
|
||||
case "pub":
|
||||
text = strings.TrimSpace(svc.PublicPEM())
|
||||
default:
|
||||
text = strings.TrimSpace(svc.PublicPEM() + "\n" + svc.PublicCert())
|
||||
}
|
||||
makeQR(text, parts.pubQR)
|
||||
// debug save for comparison
|
||||
if parts.pubQR.Image != nil {
|
||||
_ = os.MkdirAll("qr_debug", 0o755)
|
||||
if f, e := os.Create(filepath.Join("qr_debug", "identity_current.png")); e == nil {
|
||||
png.Encode(f, parts.pubQR.Image)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parts.pubQR.Image = nil
|
||||
parts.pubQR.Refresh()
|
||||
}
|
||||
}
|
||||
update()
|
||||
box := container.NewVBox(container.NewHBox(widget.NewLabel("QR obsah:"), chooser, layout.NewSpacer(), widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) })), parts.pubQR)
|
||||
return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteBtn))
|
||||
}
|
||||
|
||||
// Decrypt tab
|
||||
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
parts.plainOut.Disable()
|
||||
parts.payload.Disable()
|
||||
parts.payload.SetPlaceHolder("Cyphertext se načte po vložení QR kódu…")
|
||||
parts.payload.OnChanged = nil
|
||||
parts.payloadQR.FillMode = canvas.ImageFillContain
|
||||
parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))
|
||||
|
||||
decrypt := func(text string) {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed == "" {
|
||||
parts.plainOut.SetText("")
|
||||
return
|
||||
}
|
||||
go func(j string) {
|
||||
res, err := svc.Decrypt(j)
|
||||
if err != nil {
|
||||
fyne.Do(func() {
|
||||
parts.plainOut.SetText("")
|
||||
parts.showToast("Chyba dešifrování")
|
||||
})
|
||||
return
|
||||
}
|
||||
fyne.Do(func() { parts.plainOut.SetText(res) })
|
||||
}(trimmed)
|
||||
}
|
||||
|
||||
setPayload := func(cipher string, img image.Image) {
|
||||
parts.payload.SetText(cipher)
|
||||
parts.payloadQR.Image = img
|
||||
parts.payloadQR.Refresh()
|
||||
decrypt(cipher)
|
||||
}
|
||||
|
||||
decodeFromImage := func(img image.Image) {
|
||||
if img == nil {
|
||||
parts.showToast("Žádný QR obrázek")
|
||||
return
|
||||
}
|
||||
txt, err := DecodeQR(img)
|
||||
if err != nil {
|
||||
bounds := img.Bounds()
|
||||
inv := image.NewRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, a := img.At(x, y).RGBA()
|
||||
inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
|
||||
}
|
||||
}
|
||||
if txt2, err2 := DecodeQR(inv); err2 == nil {
|
||||
setPayload(txt2, img)
|
||||
parts.showToast("Načteno z invert QR")
|
||||
return
|
||||
}
|
||||
parts.showToast("QR nenalezen: " + err.Error())
|
||||
return
|
||||
}
|
||||
setPayload(txt, img)
|
||||
parts.showToast("Načteno z QR")
|
||||
}
|
||||
|
||||
pasteQRBtn := widget.NewButtonWithIcon("Vložit ze schránky", theme.ContentPasteIcon(), func() {
|
||||
img, err := readImageClipboard()
|
||||
if err != nil {
|
||||
parts.showToast("Chyba schránky: " + err.Error())
|
||||
return
|
||||
}
|
||||
decodeFromImage(img)
|
||||
})
|
||||
|
||||
openQRBtn := widget.NewButtonWithIcon("Otevřít obrázek", theme.FolderOpenIcon(), func() {
|
||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||
fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
|
||||
if err != nil || rc == nil {
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
data, readErr := io.ReadAll(rc)
|
||||
if readErr != nil {
|
||||
parts.showToast("Chyba čtení souboru")
|
||||
return
|
||||
}
|
||||
img, _, decErr := image.Decode(bytes.NewReader(data))
|
||||
if decErr != nil {
|
||||
parts.showToast("Neplatný obrázek: " + decErr.Error())
|
||||
return
|
||||
}
|
||||
decodeFromImage(img)
|
||||
}, win)
|
||||
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
|
||||
fd.Show()
|
||||
})
|
||||
|
||||
copyPayloadBtn := widget.NewButtonWithIcon("Kopírovat payload", theme.ContentCopyIcon(), func() {
|
||||
if parts.payload.Text == "" {
|
||||
return
|
||||
}
|
||||
copyClip(parts.payload.Text, parts)
|
||||
})
|
||||
|
||||
clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
|
||||
parts.payload.SetText("")
|
||||
parts.payloadQR.Image = nil
|
||||
parts.payloadQR.Refresh()
|
||||
parts.plainOut.SetText("")
|
||||
parts.showToast("Vymazáno")
|
||||
})
|
||||
|
||||
toolbar := container.NewHBox(pasteQRBtn, openQRBtn, copyPayloadBtn, clearBtn, layout.NewSpacer())
|
||||
|
||||
return container.NewVBox(
|
||||
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
toolbar,
|
||||
container.NewHBox(parts.payloadQR, layout.NewSpacer()),
|
||||
widget.NewLabel("Payload"),
|
||||
parts.payload,
|
||||
widget.NewLabel("Výsledek"),
|
||||
parts.plainOut,
|
||||
)
|
||||
}
|
||||
|
||||
// Per-contact encryption popup (QR-only output)
|
||||
func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
|
||||
msgEntry := widget.NewMultiLineEntry()
|
||||
msgEntry.SetMinRowsVisible(6)
|
||||
msgEntry.Wrapping = fyne.TextWrapWord
|
||||
status := widget.NewLabel("Zadej zprávu…")
|
||||
qrImg := canvas.NewImageFromImage(nil)
|
||||
qrImg.FillMode = canvas.ImageFillContain
|
||||
qrImg.SetMinSize(fyne.NewSize(300, 300))
|
||||
updateQR := func(text string) {
|
||||
if strings.TrimSpace(text) == "" {
|
||||
qrImg.Image = nil
|
||||
qrImg.Refresh()
|
||||
return
|
||||
}
|
||||
if b, err := GenerateQRPNG(text, 512); err == nil {
|
||||
if im, err2 := LoadPNG(b); err2 == nil {
|
||||
qrImg.Image = im
|
||||
qrImg.Refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||
lastCipher := ""
|
||||
doEncrypt := func() {
|
||||
m := strings.TrimSpace(msgEntry.Text)
|
||||
fyne.Do(func() {
|
||||
if m == "" {
|
||||
status.SetText("Zpráva je prázdná")
|
||||
lastCipher = ""
|
||||
updateQR("")
|
||||
return
|
||||
}
|
||||
status.SetText("Šifruji…")
|
||||
})
|
||||
if m == "" {
|
||||
return
|
||||
}
|
||||
go func(txt string) {
|
||||
res, err := svc.Encrypt(txt, ct.Cert)
|
||||
if err != nil {
|
||||
fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
|
||||
return
|
||||
}
|
||||
fyne.Do(func() { lastCipher = res; updateQR(res); status.SetText("Hotovo") })
|
||||
}(m)
|
||||
}
|
||||
var tmr *time.Timer
|
||||
msgEntry.OnChanged = func(string) {
|
||||
if tmr != nil {
|
||||
tmr.Stop()
|
||||
}
|
||||
tmr = time.AfterFunc(300*time.Millisecond, doEncrypt)
|
||||
}
|
||||
copyPayloadBtn := widget.NewButton("Kopírovat payload", func() {
|
||||
if lastCipher != "" {
|
||||
copyClip(lastCipher, parts)
|
||||
status.SetText("Zkopírováno")
|
||||
}
|
||||
})
|
||||
copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) })
|
||||
saveQRBtn := widget.NewButton("Uložit QR", func() {
|
||||
if qrImg.Image == nil {
|
||||
return
|
||||
}
|
||||
img := qrImg.Image
|
||||
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
|
||||
if err != nil || wc == nil {
|
||||
return
|
||||
}
|
||||
defer wc.Close()
|
||||
_ = png.Encode(wc, img)
|
||||
status.SetText("QR uložen")
|
||||
}, win)
|
||||
fd.SetFileName("message_qr.png")
|
||||
fd.Show()
|
||||
})
|
||||
content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyPayloadBtn, copyQRBtn, saveQRBtn), qrImg, status)
|
||||
title := ct.Name
|
||||
if title == "" {
|
||||
title = "(bez názvu)"
|
||||
}
|
||||
if cn := extractCN(ct.Cert); cn != "" && !strings.Contains(title, cn) {
|
||||
title = fmt.Sprintf("%s (%s)", title, cn)
|
||||
}
|
||||
dlg := dialog.NewCustom(fmt.Sprintf("Poslat zprávu: %s", title), "Zavřít", content, win)
|
||||
dlg.Resize(fyne.NewSize(640, 520))
|
||||
dlg.Show()
|
||||
}
|
||||
|
||||
// Contacts tab with QR-only popup
|
||||
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
var all, filtered []Contact
|
||||
load := func() { items, _ := svc.ListContacts(); all = items; filtered = items }
|
||||
apply := func(q string) {
|
||||
if q == "" {
|
||||
filtered = all
|
||||
return
|
||||
}
|
||||
low := strings.ToLower(q)
|
||||
tmp := make([]Contact, 0, len(all))
|
||||
for _, c := range all {
|
||||
if strings.Contains(strings.ToLower(c.Name), low) || strings.Contains(strings.ToLower(c.Cert), low) {
|
||||
tmp = append(tmp, c)
|
||||
}
|
||||
}
|
||||
filtered = tmp
|
||||
}
|
||||
makeDefault := func() string {
|
||||
base := "Nový kontakt"
|
||||
exists := false
|
||||
maxN := 1
|
||||
for _, c := range all {
|
||||
if c.Name == base {
|
||||
exists = true
|
||||
}
|
||||
if strings.HasPrefix(c.Name, base+" ") {
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(c.Name, "Nový kontakt %d", &n); err == nil && n >= maxN {
|
||||
maxN = n + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
return base
|
||||
}
|
||||
return fmt.Sprintf("%s %d", base, maxN)
|
||||
}
|
||||
search := widget.NewEntry()
|
||||
var list *widget.List
|
||||
openPopup := func(existing *Contact) {
|
||||
nameEntry := widget.NewEntry()
|
||||
if existing != nil {
|
||||
nameEntry.SetText(existing.Name)
|
||||
} else {
|
||||
nameEntry.SetText(makeDefault())
|
||||
}
|
||||
var certValue string
|
||||
if existing != nil {
|
||||
certValue = existing.Cert
|
||||
}
|
||||
manualEntry := widget.NewMultiLineEntry()
|
||||
manualEntry.SetMinRowsVisible(4)
|
||||
manualEntry.Wrapping = fyne.TextWrapWord
|
||||
manualEntry.SetPlaceHolder("Sem lze vložit text PEM pokud QR selže…")
|
||||
if certValue != "" {
|
||||
manualEntry.SetText(certValue)
|
||||
}
|
||||
qrImg := canvas.NewImageFromImage(nil)
|
||||
qrImg.FillMode = canvas.ImageFillContain
|
||||
qrImg.SetMinSize(fyne.NewSize(300, 300))
|
||||
updateQR := func() {
|
||||
if strings.TrimSpace(certValue) == "" {
|
||||
qrImg.Image = nil
|
||||
qrImg.Refresh()
|
||||
return
|
||||
}
|
||||
if b, err := GenerateQRPNG(certValue, 512); err == nil {
|
||||
if im, err2 := LoadPNG(b); err2 == nil {
|
||||
qrImg.Image = im
|
||||
qrImg.Refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateQR()
|
||||
// now that updateQR exists, bind manualEntry changes
|
||||
manualEntry.OnChanged = func(s string) {
|
||||
certValue = s
|
||||
updateQR()
|
||||
}
|
||||
pasteText := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
|
||||
clip := fyne.CurrentApp().Clipboard().Content()
|
||||
if strings.TrimSpace(clip) == "" {
|
||||
parts.showToast("Schránka prázdná")
|
||||
return
|
||||
}
|
||||
certValue = clip
|
||||
manualEntry.SetText(certValue)
|
||||
updateQR()
|
||||
parts.showToast("Vloženo")
|
||||
})
|
||||
pasteQR := widget.NewToolbarAction(theme.ComputerIcon(), func() {
|
||||
img, err := readImageClipboard()
|
||||
if err != nil {
|
||||
parts.showToast("Chyba čtení schránky: " + err.Error())
|
||||
return
|
||||
}
|
||||
if img == nil {
|
||||
parts.showToast("Žádný QR obrázek")
|
||||
return
|
||||
}
|
||||
|
||||
txt, decErr := DecodeQR(img)
|
||||
if decErr != nil {
|
||||
bounds := img.Bounds()
|
||||
inv := image.NewRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, a := img.At(x, y).RGBA()
|
||||
inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
|
||||
}
|
||||
}
|
||||
if txt2, err2 := DecodeQR(inv); err2 == nil {
|
||||
certValue = txt2
|
||||
manualEntry.SetText(certValue)
|
||||
updateQR()
|
||||
parts.showToast("Načteno z invert QR")
|
||||
return
|
||||
}
|
||||
debugDir := "qr_debug"
|
||||
_ = os.MkdirAll(debugDir, 0o755)
|
||||
fp := filepath.Join(debugDir, fmt.Sprintf("qr_clip_%d.png", time.Now().UnixNano()))
|
||||
if f, e := os.Create(fp); e == nil {
|
||||
_ = png.Encode(f, img)
|
||||
_ = f.Close()
|
||||
parts.showToast("QR nenalezen: " + decErr.Error() + " (" + fp + ")")
|
||||
} else {
|
||||
parts.showToast("QR nenalezen: " + decErr.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
certValue = txt
|
||||
manualEntry.SetText(certValue)
|
||||
updateQR()
|
||||
parts.showToast("Načteno z QR")
|
||||
})
|
||||
openImg := widget.NewToolbarAction(theme.FolderOpenIcon(), func() {
|
||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||
fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
|
||||
if err != nil || rc == nil {
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
data, _ := io.ReadAll(rc)
|
||||
img, _, e2 := image.Decode(bytes.NewReader(data))
|
||||
if e2 != nil {
|
||||
parts.showToast("Neplatný obrázek: " + e2.Error())
|
||||
return
|
||||
}
|
||||
|
||||
txt, e3 := DecodeQR(img)
|
||||
if e3 != nil {
|
||||
parts.showToast("QR nenalezeno: " + e3.Error())
|
||||
return
|
||||
}
|
||||
certValue = txt
|
||||
manualEntry.SetText(certValue)
|
||||
updateQR()
|
||||
parts.showToast("Načteno z QR")
|
||||
}, win)
|
||||
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
|
||||
fd.Show()
|
||||
})
|
||||
clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; manualEntry.SetText(""); updateQR() })
|
||||
copyAct := widget.NewToolbarAction(theme.ContentCopyIcon(), func() {
|
||||
if certValue != "" {
|
||||
copyClip(certValue, parts)
|
||||
}
|
||||
})
|
||||
saveAct := widget.NewToolbarAction(theme.DocumentSaveIcon(), func() {
|
||||
if qrImg.Image == nil {
|
||||
return
|
||||
}
|
||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||
img := qrImg.Image
|
||||
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
|
||||
if err != nil || wc == nil {
|
||||
return
|
||||
}
|
||||
defer wc.Close()
|
||||
_ = png.Encode(wc, img)
|
||||
parts.showToast("QR uložen")
|
||||
}, win)
|
||||
fd.SetFileName("contact_qr.png")
|
||||
fd.Show()
|
||||
})
|
||||
toolbar := widget.NewToolbar(pasteText, pasteQR, openImg, widget.NewToolbarSeparator(), copyAct, saveAct, clearAct)
|
||||
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
||||
var popup dialog.Dialog
|
||||
save := func(useEncrypt bool) {
|
||||
name := strings.TrimSpace(nameEntry.Text)
|
||||
cert := strings.TrimSpace(certValue)
|
||||
if cert == "" {
|
||||
parts.showToast("Chybí cert")
|
||||
return
|
||||
}
|
||||
cn := extractCN(cert)
|
||||
ask := cn != "" && (name == "" || name == "Kontakt" || name == "Nový kontakt" || name != cn)
|
||||
proceed := func(final string) {
|
||||
if final == "" || final == "Kontakt" || final == "Nový kontakt" {
|
||||
final = makeDefault()
|
||||
}
|
||||
if existing == nil {
|
||||
_ = svc.SaveContact(Contact{Name: final, Cert: cert})
|
||||
} else {
|
||||
c := *existing
|
||||
c.Name = final
|
||||
c.Cert = cert
|
||||
_ = svc.SaveContact(c)
|
||||
}
|
||||
load()
|
||||
apply(strings.TrimSpace(search.Text))
|
||||
list.Refresh()
|
||||
parts.showToast("Uloženo")
|
||||
if useEncrypt {
|
||||
parts.peer.SetText(cert)
|
||||
}
|
||||
if popup != nil {
|
||||
popup.Hide()
|
||||
}
|
||||
}
|
||||
if ask {
|
||||
dialog.NewCustomConfirm("Common Name", "Použít", "Ponechat", widget.NewLabel(fmt.Sprintf("Common Name: %s\nPoužít jako název?", cn)), func(ok bool) {
|
||||
if ok {
|
||||
proceed(cn)
|
||||
} else {
|
||||
proceed(name)
|
||||
}
|
||||
}, win).Show()
|
||||
return
|
||||
}
|
||||
proceed(name)
|
||||
}
|
||||
delBtn := widget.NewButtonWithIcon("Smazat", theme.DeleteIcon(), func() {
|
||||
if existing == nil {
|
||||
popup.Hide()
|
||||
return
|
||||
}
|
||||
dialog.NewConfirm("Smazat", "Opravdu smazat?", func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = svc.DeleteContact(existing.ID)
|
||||
load()
|
||||
apply(strings.TrimSpace(search.Text))
|
||||
list.Refresh()
|
||||
popup.Hide()
|
||||
parts.showToast("Smazáno")
|
||||
}, win).Show()
|
||||
})
|
||||
useBtn := widget.NewButton("Použít ve Šifrování", func() { save(true) })
|
||||
saveBtn := widget.NewButton("Uložit", func() { save(false) })
|
||||
row := container.NewHBox(saveBtn, useBtn, layout.NewSpacer(), delBtn)
|
||||
title := "Nový kontakt"
|
||||
if existing != nil {
|
||||
title = "Upravit kontakt"
|
||||
}
|
||||
// manual entry area below QR for fallback or direct edit
|
||||
popup = dialog.NewCustom(title, "Zavřít", container.NewVBox(
|
||||
widget.NewLabel("Název"), nameEntry,
|
||||
widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg,
|
||||
widget.NewLabel("Text PEM"), manualEntry,
|
||||
widget.NewSeparator(), row), win)
|
||||
popup.Resize(fyne.NewSize(640, 520))
|
||||
popup.Show()
|
||||
}
|
||||
list = widget.NewList(func() int { return len(filtered) }, func() fyne.CanvasObject {
|
||||
lbl := widget.NewLabel("")
|
||||
msg := widget.NewButton("Zpráva", nil)
|
||||
edit := widget.NewButton("Upravit", nil)
|
||||
msg.Importance = widget.LowImportance
|
||||
edit.Importance = widget.LowImportance
|
||||
return container.NewBorder(nil, nil, lbl, container.NewHBox(msg, edit))
|
||||
}, func(i widget.ListItemID, o fyne.CanvasObject) {
|
||||
if int(i) < 0 || int(i) >= len(filtered) {
|
||||
return
|
||||
}
|
||||
c := filtered[i]
|
||||
row := o.(*fyne.Container)
|
||||
lbl := row.Objects[0].(*widget.Label)
|
||||
btnBox := row.Objects[1].(*fyne.Container)
|
||||
msgBtn := btnBox.Objects[0].(*widget.Button)
|
||||
editBtn := btnBox.Objects[1].(*widget.Button)
|
||||
name := c.Name
|
||||
if name == "" {
|
||||
name = "(bez názvu)"
|
||||
}
|
||||
if cn := extractCN(c.Cert); cn != "" {
|
||||
lbl.SetText(fmt.Sprintf("%s (%s)", name, cn))
|
||||
} else {
|
||||
lbl.SetText(name)
|
||||
}
|
||||
msgBtn.OnTapped = func() { openEncryptPopup(parts, svc, c) }
|
||||
editBtn.OnTapped = func() {
|
||||
var ptr *Contact
|
||||
for i := range all {
|
||||
if all[i].ID == c.ID {
|
||||
ptr = &all[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
openPopup(ptr)
|
||||
}
|
||||
})
|
||||
search.SetPlaceHolder("Hledat…")
|
||||
search.OnChanged = func(s string) { apply(s); list.Refresh() }
|
||||
addBtn := widget.NewButtonWithIcon("Přidat", theme.ContentAddIcon(), func() { openPopup(nil) })
|
||||
header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), addBtn)
|
||||
load()
|
||||
list.Refresh()
|
||||
return container.NewBorder(header, nil, nil, nil, container.NewBorder(search, nil, nil, nil, list))
|
||||
}
|
||||
|
||||
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)),
|
||||
container.NewTabItem("Kontakty", buildContactsTab(parts, svc)),
|
||||
container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)),
|
||||
)
|
||||
fyne.CurrentApp().Settings().SetTheme(simpleTheme{})
|
||||
return container.NewBorder(nil, parts.toastLabel, nil, nil, tabs)
|
||||
}
|
||||
252
vault_service.go
Executable file
252
vault_service.go
Executable file
@ -0,0 +1,252 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
)
|
||||
|
||||
// VaultService implementuje ServiceFacade nad SecureJSONStore.
|
||||
// Využívá existující hybridní šifrování z encrypt.Service (re-use kódu voláním helperů?)
|
||||
// Pro jednoduchost zopakujeme potřebnou část: Encrypt/Decrypt využijí dočasný encrypt.Service
|
||||
// inicializovaný z načtených klíčů.
|
||||
type VaultService struct {
|
||||
store encrypt.SecureJSONStore
|
||||
priv *rsa.PrivateKey
|
||||
pubPEM string
|
||||
certPEM string
|
||||
}
|
||||
|
||||
func NewVaultService(store encrypt.SecureJSONStore) (*VaultService, error) {
|
||||
vs := &VaultService{store: store}
|
||||
if err := vs.loadIdentity(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vs, nil
|
||||
}
|
||||
|
||||
func (v *VaultService) loadIdentity() error {
|
||||
privPem := v.store.IdentityPrivatePEM()
|
||||
if privPem == "" {
|
||||
return errors.New("missing private key in store")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(privPem))
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
return errors.New("invalid private key PEM")
|
||||
}
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.priv = key
|
||||
v.pubPEM = v.store.IdentityPublicPEM()
|
||||
v.certPEM = v.store.IdentityCertPEM()
|
||||
if v.pubPEM == "" { // derive public if missing
|
||||
pubASN1, _ := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
v.pubPEM = string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1}))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VaultService) PublicPEM() string { return v.pubPEM }
|
||||
func (v *VaultService) PublicCert() string { return v.certPEM }
|
||||
|
||||
// Encrypt provede hybridní šifrování stejně jako původní Service.Encrypt.
|
||||
func (v *VaultService) Encrypt(message, peerPEMorCert string) (string, error) {
|
||||
// Vytvoříme ad-hoc Service s privátním klíčem jen pro volání existující logiky? Nejjednodušší je zkopírovat potřebnou část kódu.
|
||||
return encryptHybrid(v.priv, message, peerPEMorCert)
|
||||
}
|
||||
|
||||
// Decrypt provede rozšifrování.
|
||||
func (v *VaultService) Decrypt(payload string) (string, error) { return decryptHybrid(v.priv, payload) }
|
||||
|
||||
// --- Lokální helpery (duplikace z encrypt.Service, zredukované) ---
|
||||
|
||||
type hybridEnvelope struct {
|
||||
EK string `json:"ek"`
|
||||
N string `json:"n"`
|
||||
CT string `json:"ct"`
|
||||
}
|
||||
|
||||
// --- Contacts management ---
|
||||
|
||||
type Contact struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Cert string `json:"cert"` // full PEM cert (or public key PEM)
|
||||
}
|
||||
|
||||
const contactsKey = "contacts"
|
||||
|
||||
func (v *VaultService) ListContacts() ([]Contact, error) {
|
||||
var list []Contact
|
||||
if !v.store.Has(contactsKey) {
|
||||
return []Contact{}, nil
|
||||
}
|
||||
if err := v.store.Get(contactsKey, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (v *VaultService) SaveContact(c Contact) error {
|
||||
list, _ := v.ListContacts()
|
||||
// upsert by ID; if empty ID, assign a new unique random ID to avoid overwriting
|
||||
if c.ID == "" {
|
||||
c.ID = generateUniqueContactID(list)
|
||||
}
|
||||
replaced := false
|
||||
for i := range list {
|
||||
if list[i].ID == c.ID {
|
||||
list[i] = c
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
list = append(list, c)
|
||||
}
|
||||
if err := v.store.Put(contactsKey, list); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.store.Flush()
|
||||
}
|
||||
|
||||
func (v *VaultService) DeleteContact(id string) error {
|
||||
list, _ := v.ListContacts()
|
||||
out := list[:0]
|
||||
for _, c := range list {
|
||||
if c.ID != id {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
if err := v.store.Put(contactsKey, out); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.store.Flush()
|
||||
}
|
||||
|
||||
func deriveContactID(c Contact) string {
|
||||
// simple stable ID: prefer CN from cert; fallback to sha256 of cert
|
||||
if cn := extractCN(c.Cert); cn != "" {
|
||||
return cn
|
||||
}
|
||||
sum := sha256.Sum256([]byte(c.Cert))
|
||||
return fmt.Sprintf("%x", sum[:8])
|
||||
}
|
||||
|
||||
// generateUniqueContactID creates a short random hex ID that does not collide with existing list.
|
||||
func generateUniqueContactID(existing []Contact) string {
|
||||
exists := make(map[string]struct{}, len(existing))
|
||||
for _, c := range existing {
|
||||
exists[c.ID] = struct{}{}
|
||||
}
|
||||
for {
|
||||
b := make([]byte, 6)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// fallback to time-based seed rand
|
||||
n := mrand.Int63()
|
||||
return fmt.Sprintf("%x", n)
|
||||
}
|
||||
id := fmt.Sprintf("%x", b)
|
||||
if _, ok := exists[id]; !ok {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractCN(pemText string) string {
|
||||
// Support multiple concatenated PEM blocks (public key + cert, or chain)
|
||||
rest := []byte(pemText)
|
||||
for len(rest) > 0 {
|
||||
var block *pem.Block
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type == "CERTIFICATE" {
|
||||
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
|
||||
return cert.Subject.CommonName
|
||||
}
|
||||
}
|
||||
// continue to next block until certificate found
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func encryptHybrid(priv *rsa.PrivateKey, message, peerPEMorCert string) (string, error) {
|
||||
pubKey, err := encrypt.ParsePeerPublicKey(peerPEMorCert)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
aesKey := make([]byte, 32)
|
||||
if _, err := rand.Read(aesKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ct := gcm.Seal(nil, nonce, []byte(message), nil)
|
||||
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, []byte{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
env := hybridEnvelope{EK: base64.StdEncoding.EncodeToString(ek), N: base64.StdEncoding.EncodeToString(nonce), CT: base64.StdEncoding.EncodeToString(ct)}
|
||||
out, _ := json.MarshalIndent(env, "", " ")
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func decryptHybrid(priv *rsa.PrivateKey, payload string) (string, error) {
|
||||
var env hybridEnvelope
|
||||
if err := json.Unmarshal([]byte(payload), &env); err != nil {
|
||||
return "", fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
ek, err := base64.StdEncoding.DecodeString(env.EK)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ek b64: %w", err)
|
||||
}
|
||||
nonce, err := base64.StdEncoding.DecodeString(env.N)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("n b64: %w", err)
|
||||
}
|
||||
ct, err := base64.StdEncoding.DecodeString(env.CT)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ct b64: %w", err)
|
||||
}
|
||||
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, []byte{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("RSA-OAEP decrypt: %w", err)
|
||||
}
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pt, err := gcm.Open(nil, nonce, ct, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GCM open: %w", err)
|
||||
}
|
||||
return string(pt), nil
|
||||
}
|
||||
49
vendor/fyne.io/fyne/v2/.gitignore
generated
vendored
Normal file
49
vendor/fyne.io/fyne/v2/.gitignore
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
### Binaries and project specific files
|
||||
cmd/fyne/fyne
|
||||
cmd/fyne_demo/fyne_demo
|
||||
cmd/fyne_settings/fyne_settings
|
||||
cmd/hello/hello
|
||||
fyne-cross
|
||||
*.exe
|
||||
*.apk
|
||||
*.app
|
||||
*.tar.xz
|
||||
*.zip
|
||||
|
||||
### Tests
|
||||
**/testdata/failed
|
||||
|
||||
### Go
|
||||
# Output of the coverage tool
|
||||
*.out
|
||||
|
||||
### macOS
|
||||
# General
|
||||
.DS_Store
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
### JetBrains
|
||||
.idea
|
||||
|
||||
### VSCode
|
||||
.vscode
|
||||
|
||||
### Vim
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-v][a-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
1
vendor/fyne.io/fyne/v2/.godocdown.import
generated
vendored
Normal file
1
vendor/fyne.io/fyne/v2/.godocdown.import
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
fyne.io/fyne/v2
|
||||
16
vendor/fyne.io/fyne/v2/AUTHORS
generated
vendored
Normal file
16
vendor/fyne.io/fyne/v2/AUTHORS
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
Andy Williams <andy@andy.xyz>
|
||||
Steve OConnor <steveoc64@gmail.com>
|
||||
Luca Corbo <lu.corbo@gmail.com>
|
||||
Paul Hovey <paul@paulhovey.org>
|
||||
Charles Corbett <nafredy@gmail.com>
|
||||
Tilo Prütz <tilo@pruetz.net>
|
||||
Stephen Houston <smhouston88@gmail.com>
|
||||
Storm Hess <stormhess@gloryskulls.com>
|
||||
Stuart Scott <stuart.murray.scott@gmail.com>
|
||||
Jacob Alzén <jacalz@tutanota.com>
|
||||
Charles A. Daniels <charles@cdaniels.net>
|
||||
Pablo Fuentes <f.pablo1@hotmail.com>
|
||||
Changkun Ou <hi@changkun.de>
|
||||
Cedric Bail
|
||||
Drew Weymouth
|
||||
Simon Dassow
|
||||
1609
vendor/fyne.io/fyne/v2/CHANGELOG.md
generated
vendored
Normal file
1609
vendor/fyne.io/fyne/v2/CHANGELOG.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
76
vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md
generated
vendored
Normal file
76
vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at info@fyne.io. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
63
vendor/fyne.io/fyne/v2/CONTRIBUTING.md
generated
vendored
Normal file
63
vendor/fyne.io/fyne/v2/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
Thanks very much for your interest in contributing to Fyne!
|
||||
The community is what makes this project successful and we are glad to welcome you on board.
|
||||
|
||||
There are various ways to contribute, perhaps the following helps you know how to get started.
|
||||
|
||||
## Reporting a bug
|
||||
|
||||
If you've found something wrong we want to know about it, please help us understand the problem so we can resolve it.
|
||||
|
||||
1. Check to see if this already is recorded, if so add some more information [issue list](https://github.com/fyne-io/fyne/issues)
|
||||
2. If not then create a new issue using the [bug report template](https://github.com/fyne-io/fyne/issues/new?assignees=&labels=&template=bug_report.md&title=)
|
||||
3. Stay involved in the conversation on the issue as it is triaged and progressed.
|
||||
|
||||
|
||||
## Fixing an issue
|
||||
|
||||
Great! You found an issue and figured you can fix it for us.
|
||||
If you can follow these steps then your code should get accepted fast.
|
||||
|
||||
1. Read through the "Contributing Code" section further down this page.
|
||||
2. Write a unit test to show it is broken.
|
||||
3. Create the fix and you should see the test passes.
|
||||
4. Run the tests and make sure everything still works as expected using `go test ./...`.
|
||||
5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist.
|
||||
|
||||
|
||||
## Adding a feature
|
||||
|
||||
It's always good news to hear that people want to contribute functionality.
|
||||
But first of all check that it fits within our [Vision](https://github.com/fyne-io/fyne/wiki/Vision) and if we are already considering it on our [Roadmap](https://github.com/fyne-io/fyne/wiki/Roadmap).
|
||||
If you're not sure then you should join our #fyne-contributors channel on the [Gophers Slack server](https://gophers.slack.com/app_redirect?channel=fyne-contributors).
|
||||
|
||||
Once you are ready to code then the following steps should give you a smooth process:
|
||||
|
||||
1. Read through the [Contributing Code](#contributing-code) section further down this page.
|
||||
2. Think about how you would structure your code and how it can be tested.
|
||||
3. Write some code and enjoy the ease of writing Go code for even a complex project :).
|
||||
4. Run the tests and make sure everything still works as expected using `go test ./...`.
|
||||
5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist.
|
||||
|
||||
|
||||
# Contributing Code
|
||||
|
||||
We aim to maintain a very high standard of code, through design, test and implementation.
|
||||
To manage this we have various checks and processes in place that everyone should follow, including:
|
||||
|
||||
* We use the Go standard format (with tabs not spaces) - you can run `gofmt` before committing
|
||||
* Imports should be ordered according to the GoImports spec - you can use the `goimports` tool instead of `gofmt`.
|
||||
* Everything should have a unit test attached (as much as possible, to keep our coverage up)
|
||||
|
||||
For detailed Code style, check [Contributing](https://github.com/fyne-io/fyne/wiki/Contributing#code-style) in our wiki please.
|
||||
|
||||
# Decision Process
|
||||
|
||||
The following points apply to our decision making process:
|
||||
|
||||
* Any decisions or votes will be opened on the #fyne-contributors channel and follows lazy consensus.
|
||||
* Any contributors not responding in 4 days will be deemed in agreement.
|
||||
* Any PR that has not been responded to within 7 days can be automatically approved.
|
||||
* No functionality will be added unless at least 2 developers agree it belongs.
|
||||
|
||||
Bear in mind that this is a cross platform project so any new features would normally
|
||||
be required to work on multiple desktop and mobile platforms.
|
||||
28
vendor/fyne.io/fyne/v2/LICENSE
generated
vendored
Normal file
28
vendor/fyne.io/fyne/v2/LICENSE
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (C) 2018 Fyne.io developers (see AUTHORS)
|
||||
All rights reserved.
|
||||
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Fyne.io nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
184
vendor/fyne.io/fyne/v2/README.md
generated
vendored
Normal file
184
vendor/fyne.io/fyne/v2/README.md
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/fyne.io/fyne/v2?tab=doc" title="Go API Reference" rel="nofollow"><img src="https://img.shields.io/badge/go-documentation-blue.svg?style=flat" alt="Go API Reference"></a>
|
||||
<a href="https://img.shields.io/github/v/release/fyne-io/fyne?include_prereleases" title="Latest Release" rel="nofollow"><img src="https://img.shields.io/github/v/release/fyne-io/fyne?include_prereleases" alt="Latest Release"></a>
|
||||
<a href='https://gophers.slack.com/messages/fyne'><img src='https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=blue' alt='Join us on Slack' /></a>
|
||||
<br />
|
||||
<a href="https://goreportcard.com/report/fyne.io/fyne/v2"><img src="https://goreportcard.com/badge/fyne.io/fyne/v2" alt="Code Status" /></a>
|
||||
<a href="https://github.com/fyne-io/fyne/actions"><img src="https://github.com/fyne-io/fyne/workflows/Platform%20Tests/badge.svg" alt="Build Status" /></a>
|
||||
<a href='https://coveralls.io/github/fyne-io/fyne?branch=develop'><img src='https://coveralls.io/repos/github/fyne-io/fyne/badge.svg?branch=develop' alt='Coverage Status' /></a>
|
||||
</p>
|
||||
|
||||
# About
|
||||
|
||||
[Fyne](https://fyne.io) is an easy-to-use UI toolkit and app API written in Go.
|
||||
It is designed to build applications that run on desktop and mobile devices with a
|
||||
single codebase.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
To develop apps using Fyne you will need Go version 1.17 or later, a C compiler and your system's development tools.
|
||||
If you're not sure if that's all installed or you don't know how then check out our
|
||||
[Getting Started](https://fyne.io/develop/) document.
|
||||
|
||||
Using the standard go tools you can install Fyne's core library using:
|
||||
|
||||
go get fyne.io/fyne/v2@latest
|
||||
|
||||
After importing a new module, run the following command before compiling the code for the first time. Avoid running it before writing code that uses the module to prevent accidental removal of dependencies:
|
||||
|
||||
go mod tidy
|
||||
|
||||
# Widget demo
|
||||
|
||||
To run a showcase of the features of Fyne execute the following:
|
||||
|
||||
go install fyne.io/fyne/v2/cmd/fyne_demo@latest
|
||||
fyne_demo
|
||||
|
||||
And you should see something like this (after you click a few buttons):
|
||||
|
||||
<p align="center" markdown="1" style="max-width: 100%">
|
||||
<img src="img/widgets-dark.png" width="752" alt="Fyne Demo Dark Theme" style="max-width: 100%" />
|
||||
</p>
|
||||
|
||||
Or if you are using the light theme:
|
||||
|
||||
<p align="center" markdown="1" style="max-width: 100%">
|
||||
<img src="img/widgets-light.png" width="752" alt="Fyne Demo Light Theme" style="max-width: 100%" />
|
||||
</p>
|
||||
|
||||
And even running on a mobile device:
|
||||
|
||||
<p align="center" markdown="1" style="max-width: 100%">
|
||||
<img src="img/widgets-mobile-light.png" width="348" alt="Fyne Demo Mobile Light Theme" style="max-width: 100%" />
|
||||
</p>
|
||||
|
||||
# Getting Started
|
||||
|
||||
Fyne is designed to be really easy to code with.
|
||||
If you have followed the prerequisite steps above then all you need is a
|
||||
Go IDE (or a text editor).
|
||||
|
||||
Open a new file and you're ready to write your first app!
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := app.New()
|
||||
w := a.NewWindow("Hello")
|
||||
|
||||
hello := widget.NewLabel("Hello Fyne!")
|
||||
w.SetContent(container.NewVBox(
|
||||
hello,
|
||||
widget.NewButton("Hi!", func() {
|
||||
hello.SetText("Welcome :)")
|
||||
}),
|
||||
))
|
||||
|
||||
w.ShowAndRun()
|
||||
}
|
||||
```
|
||||
|
||||
And you can run that simply as:
|
||||
|
||||
go run main.go
|
||||
|
||||
> [!NOTE]
|
||||
> The first compilation of Fyne on Windows _can_ take up to 10 minutes, depending on your hardware. Subsequent builds will be fast.
|
||||
|
||||
It should look like this:
|
||||
|
||||
<div align="center">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: auto; border-collapse: collapse;">
|
||||
<tr style="border: none;"><td style="border: none;">
|
||||
<img src="img/hello-light.png" width="207" alt="Fyne Hello Dark Theme" />
|
||||
</td><td style="border: none;">
|
||||
<img src="img/hello-dark.png" width="207" alt="Fyne Hello Dark Theme" />
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## Run in mobile simulation
|
||||
|
||||
There is a helpful mobile simulation mode that gives a hint of how your app would work on a mobile device:
|
||||
|
||||
go run -tags mobile main.go
|
||||
|
||||
Another option is to use `fyne` command, see [Packaging for mobile](#packaging-for-mobile).
|
||||
|
||||
# Installing
|
||||
|
||||
Using `go install` will copy the executable into your go `bin` dir.
|
||||
To install the application with icons etc into your operating system's standard
|
||||
application location you can use the fyne utility and the "install" subcommand.
|
||||
|
||||
go install fyne.io/fyne/v2/cmd/fyne@latest
|
||||
fyne install
|
||||
|
||||
# Packaging for mobile
|
||||
|
||||
To run on a mobile device it is necessary to package up the application.
|
||||
To do this we can use the fyne utility "package" subcommand.
|
||||
You will need to add appropriate parameters as prompted, but the basic command is shown below.
|
||||
Once packaged you can install using the platform development tools or the fyne "install" subcommand.
|
||||
|
||||
fyne package -os android -appID my.domain.appname
|
||||
fyne install -os android
|
||||
|
||||
The built Android application can run either in a real device or an Android emulator.
|
||||
However, building for iOS is slightly different.
|
||||
If the "-os" argument is "ios", it is build only for a real iOS device.
|
||||
Specify "-os" to "iossimulator" allows the application be able to run in an iOS simulator:
|
||||
|
||||
fyne package -os ios -appID my.domain.appname
|
||||
fyne package -os iossimulator -appID my.domain.appname
|
||||
|
||||
# Preparing a release
|
||||
|
||||
Using the fyne utility "release" subcommand you can package up your app for release
|
||||
to app stores and market places. Make sure you have the standard build tools installed
|
||||
and have followed the platform documentation for setting up accounts and signing.
|
||||
Then you can execute something like the following, notice the `-os ios` parameter allows
|
||||
building an iOS app from macOS computer. Other combinations work as well :)
|
||||
|
||||
$ fyne release -os ios -certificate "Apple Distribution" -profile "My App Distribution" -appID "com.example.myapp"
|
||||
|
||||
The above command will create a '.ipa' file that can then be uploaded to the iOS App Store.
|
||||
|
||||
# Documentation
|
||||
|
||||
More documentation is available at the [Fyne developer website](https://developer.fyne.io/) or on [pkg.go.dev](https://pkg.go.dev/fyne.io/fyne/v2?tab=doc).
|
||||
|
||||
# Examples
|
||||
|
||||
You can find many example applications in the [examples repository](https://github.com/fyne-io/examples/).
|
||||
Alternatively a list of applications using fyne can be found at [our website](https://apps.fyne.io/).
|
||||
|
||||
# Shipping the Fyne Toolkit
|
||||
|
||||
All Fyne apps will work without pre-installed libraries, this is one reason the apps are so portable.
|
||||
However, if looking to support Fyne in a bigger way on your operating system then you can install some utilities that help to make a more complete experience.
|
||||
|
||||
## Additional apps
|
||||
|
||||
It is recommended that you install the following additional apps:
|
||||
|
||||
| app | go install | description |
|
||||
| ------------- | ----------------------------------- | ---------------------------------------------------------------------- |
|
||||
| fyne_settings | `fyne.io/fyne/v2/cmd/fyne_settings` | A GUI for managing your global Fyne settings like theme and scaling |
|
||||
| apps | `github.com/fyne-io/apps` | A graphical installer for the Fyne apps listed at https://apps.fyne.io |
|
||||
|
||||
These are optional applications but can help to create a more complete desktop experience.
|
||||
|
||||
## FyneDesk (Linux / BSD)
|
||||
|
||||
To go all the way with Fyne on your desktop / laptop computer you could install [FyneDesk](https://github.com/fyshos/fynedesk) as well :)
|
||||
|
||||

|
||||
15
vendor/fyne.io/fyne/v2/SECURITY.md
generated
vendored
Normal file
15
vendor/fyne.io/fyne/v2/SECURITY.md
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Minor releases will receive security updates and fixes until the next minor or major release.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.6.x | :white_check_mark: |
|
||||
| < 2.6.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Report security vulnerabilities using the [advisories](https://github.com/fyne-io/fyne/security/advisories) page on GitHub.
|
||||
The team of core developers will evaluate and address the issue as appropriate.
|
||||
84
vendor/fyne.io/fyne/v2/animation.go
generated
vendored
Normal file
84
vendor/fyne.io/fyne/v2/animation.go
generated
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
package fyne
|
||||
|
||||
import "time"
|
||||
|
||||
// AnimationCurve represents an animation algorithm for calculating the progress through a timeline.
|
||||
// Custom animations can be provided by implementing the "func(float32) float32" definition.
|
||||
// The input parameter will start at 0.0 when an animation starts and travel up to 1.0 at which point it will end.
|
||||
// A linear animation would return the same output value as is passed in.
|
||||
type AnimationCurve func(float32) float32
|
||||
|
||||
// AnimationRepeatForever is an AnimationCount value that indicates it should not stop looping.
|
||||
//
|
||||
// Since: 2.0
|
||||
const AnimationRepeatForever = -1
|
||||
|
||||
var (
|
||||
// AnimationEaseInOut is the default easing, it starts slowly, accelerates to the middle and slows to the end.
|
||||
//
|
||||
// Since: 2.0
|
||||
AnimationEaseInOut = animationEaseInOut
|
||||
// AnimationEaseIn starts slowly and accelerates to the end.
|
||||
//
|
||||
// Since: 2.0
|
||||
AnimationEaseIn = animationEaseIn
|
||||
// AnimationEaseOut starts at speed and slows to the end.
|
||||
//
|
||||
// Since: 2.0
|
||||
AnimationEaseOut = animationEaseOut
|
||||
// AnimationLinear is a linear mapping for animations that progress uniformly through their duration.
|
||||
//
|
||||
// Since: 2.0
|
||||
AnimationLinear = animationLinear
|
||||
)
|
||||
|
||||
// Animation represents an animated element within a Fyne canvas.
|
||||
// These animations may control individual objects or entire scenes.
|
||||
//
|
||||
// Since: 2.0
|
||||
type Animation struct {
|
||||
AutoReverse bool
|
||||
Curve AnimationCurve
|
||||
Duration time.Duration
|
||||
RepeatCount int
|
||||
Tick func(float32)
|
||||
}
|
||||
|
||||
// NewAnimation creates a very basic animation where the callback function will be called for every
|
||||
// rendered frame between [time.Now] and the specified duration. The callback values start at 0.0 and
|
||||
// will be 1.0 when the animation completes.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewAnimation(d time.Duration, fn func(float32)) *Animation {
|
||||
return &Animation{Duration: d, Tick: fn}
|
||||
}
|
||||
|
||||
// Start registers the animation with the application run-loop and starts its execution.
|
||||
func (a *Animation) Start() {
|
||||
CurrentApp().Driver().StartAnimation(a)
|
||||
}
|
||||
|
||||
// Stop will end this animation and remove it from the run-loop.
|
||||
func (a *Animation) Stop() {
|
||||
CurrentApp().Driver().StopAnimation(a)
|
||||
}
|
||||
|
||||
func animationEaseIn(val float32) float32 {
|
||||
return val * val
|
||||
}
|
||||
|
||||
func animationEaseInOut(val float32) float32 {
|
||||
if val <= 0.5 {
|
||||
return val * val * 2
|
||||
}
|
||||
|
||||
return -1 + (4-val*2)*val
|
||||
}
|
||||
|
||||
func animationEaseOut(val float32) float32 {
|
||||
return val * (2 - val)
|
||||
}
|
||||
|
||||
func animationLinear(val float32) float32 {
|
||||
return val
|
||||
}
|
||||
145
vendor/fyne.io/fyne/v2/app.go
generated
vendored
Normal file
145
vendor/fyne.io/fyne/v2/app.go
generated
vendored
Normal file
@ -0,0 +1,145 @@
|
||||
package fyne
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// An App is the definition of a graphical application.
|
||||
// Apps can have multiple windows, by default they will exit when all windows
|
||||
// have been closed. This can be modified using SetMaster or SetCloseIntercept.
|
||||
// To start an application you need to call Run somewhere in your main function.
|
||||
// Alternatively use the [fyne.io/fyne/v2.Window.ShowAndRun] function for your main window.
|
||||
type App interface {
|
||||
// Create a new window for the application.
|
||||
// The first window to open is considered the "master" and when closed
|
||||
// the application will exit.
|
||||
NewWindow(title string) Window
|
||||
|
||||
// Open a URL in the default browser application.
|
||||
OpenURL(url *url.URL) error
|
||||
|
||||
// Icon returns the application icon, this is used in various ways
|
||||
// depending on operating system.
|
||||
// This is also the default icon for new windows.
|
||||
Icon() Resource
|
||||
|
||||
// SetIcon sets the icon resource used for this application instance.
|
||||
SetIcon(Resource)
|
||||
|
||||
// Run the application - this starts the event loop and waits until [App.Quit]
|
||||
// is called or the last window closes.
|
||||
// This should be called near the end of a main() function as it will block.
|
||||
Run()
|
||||
|
||||
// Calling Quit on the application will cause the application to exit
|
||||
// cleanly, closing all open windows.
|
||||
// This function does no thing on a mobile device as the application lifecycle is
|
||||
// managed by the operating system.
|
||||
Quit()
|
||||
|
||||
// Driver returns the driver that is rendering this application.
|
||||
// Typically not needed for day to day work, mostly internal functionality.
|
||||
Driver() Driver
|
||||
|
||||
// UniqueID returns the application unique identifier, if set.
|
||||
// This must be set for use of the [App.Preferences]. see [NewWithID].
|
||||
UniqueID() string
|
||||
|
||||
// SendNotification sends a system notification that will be displayed in the operating system's notification area.
|
||||
SendNotification(*Notification)
|
||||
|
||||
// Settings return the globally set settings, determining theme and so on.
|
||||
Settings() Settings
|
||||
|
||||
// Preferences returns the application preferences, used for storing configuration and state
|
||||
Preferences() Preferences
|
||||
|
||||
// Storage returns a storage handler specific to this application.
|
||||
Storage() Storage
|
||||
|
||||
// Lifecycle returns a type that allows apps to hook in to lifecycle events.
|
||||
//
|
||||
// Since: 2.1
|
||||
Lifecycle() Lifecycle
|
||||
|
||||
// Metadata returns the application metadata that was set at compile time.
|
||||
// The items of metadata are available after "fyne package" or when running "go run"
|
||||
// Building with "go build" may cause this to be unavailable.
|
||||
//
|
||||
// Since: 2.2
|
||||
Metadata() AppMetadata
|
||||
|
||||
// CloudProvider returns the current app cloud provider,
|
||||
// if one has been registered by the developer or chosen by the user.
|
||||
//
|
||||
// Since: 2.3
|
||||
CloudProvider() CloudProvider // get the (if any) configured provider
|
||||
|
||||
// SetCloudProvider allows developers to specify how this application should integrate with cloud services.
|
||||
// See [fyne.io/cloud] package for implementation details.
|
||||
//
|
||||
// Since: 2.3
|
||||
SetCloudProvider(CloudProvider) // configure cloud for this app
|
||||
|
||||
// Clipboard returns the system clipboard.
|
||||
//
|
||||
// Since: 2.6
|
||||
Clipboard() Clipboard
|
||||
}
|
||||
|
||||
var app atomic.Pointer[App]
|
||||
|
||||
// SetCurrentApp is an internal function to set the app instance currently running.
|
||||
func SetCurrentApp(current App) {
|
||||
app.Store(¤t)
|
||||
}
|
||||
|
||||
// CurrentApp returns the current application, for which there is only 1 per process.
|
||||
func CurrentApp() App {
|
||||
val := app.Load()
|
||||
if val == nil {
|
||||
LogError("Attempt to access current Fyne app when none is started", nil)
|
||||
return nil
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
// AppMetadata captures the build metadata for an application.
|
||||
//
|
||||
// Since: 2.2
|
||||
type AppMetadata struct {
|
||||
// ID is the unique ID of this application, used by many distribution platforms.
|
||||
ID string
|
||||
// Name is the human friendly name of this app.
|
||||
Name string
|
||||
// Version represents the version of this application, normally following semantic versioning.
|
||||
Version string
|
||||
// Build is the build number of this app, some times appended to the version number.
|
||||
Build int
|
||||
// Icon contains, if present, a resource of the icon that was bundled at build time.
|
||||
Icon Resource
|
||||
// Release if true this binary was build in release mode
|
||||
// Since: 2.3
|
||||
Release bool
|
||||
// Custom contain the custom metadata defined either in FyneApp.toml or on the compile command line
|
||||
// Since: 2.3
|
||||
Custom map[string]string
|
||||
// Migrations allows an app to opt into features before they are standard
|
||||
// Since: 2.6
|
||||
Migrations map[string]bool
|
||||
}
|
||||
|
||||
// Lifecycle represents the various phases that an app can transition through.
|
||||
//
|
||||
// Since: 2.1
|
||||
type Lifecycle interface {
|
||||
// SetOnEnteredForeground hooks into the app becoming foreground and gaining focus.
|
||||
SetOnEnteredForeground(func())
|
||||
// SetOnExitedForeground hooks into the app losing input focus and going into the background.
|
||||
SetOnExitedForeground(func())
|
||||
// SetOnStarted hooks into an event that says the app is now running.
|
||||
SetOnStarted(func())
|
||||
// SetOnStopped hooks into an event that says the app is no longer running.
|
||||
SetOnStopped(func())
|
||||
}
|
||||
189
vendor/fyne.io/fyne/v2/app/app.go
generated
vendored
Normal file
189
vendor/fyne.io/fyne/v2/app/app.go
generated
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
// Package app provides app implementations for working with Fyne graphical interfaces.
|
||||
// The fastest way to get started is to call app.New() which will normally load a new desktop application.
|
||||
// If the "ci" tag is passed to go (go run -tags ci myapp.go) it will run an in-memory application.
|
||||
package app // import "fyne.io/fyne/v2/app"
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal"
|
||||
"fyne.io/fyne/v2/internal/app"
|
||||
intRepo "fyne.io/fyne/v2/internal/repository"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
"fyne.io/fyne/v2/storage/repository"
|
||||
)
|
||||
|
||||
// Declare conformity with App interface
|
||||
var _ fyne.App = (*fyneApp)(nil)
|
||||
|
||||
type fyneApp struct {
|
||||
driver fyne.Driver
|
||||
clipboard fyne.Clipboard
|
||||
icon fyne.Resource
|
||||
uniqueID string
|
||||
|
||||
cloud fyne.CloudProvider
|
||||
lifecycle app.Lifecycle
|
||||
settings *settings
|
||||
storage fyne.Storage
|
||||
prefs fyne.Preferences
|
||||
}
|
||||
|
||||
func (a *fyneApp) CloudProvider() fyne.CloudProvider {
|
||||
return a.cloud
|
||||
}
|
||||
|
||||
func (a *fyneApp) Icon() fyne.Resource {
|
||||
if a.icon != nil {
|
||||
return a.icon
|
||||
}
|
||||
|
||||
if a.Metadata().Icon == nil || len(a.Metadata().Icon.Content()) == 0 {
|
||||
return nil
|
||||
}
|
||||
return a.Metadata().Icon
|
||||
}
|
||||
|
||||
func (a *fyneApp) SetIcon(icon fyne.Resource) {
|
||||
a.icon = icon
|
||||
}
|
||||
|
||||
func (a *fyneApp) UniqueID() string {
|
||||
if a.uniqueID != "" {
|
||||
return a.uniqueID
|
||||
}
|
||||
if a.Metadata().ID != "" {
|
||||
return a.Metadata().ID
|
||||
}
|
||||
|
||||
fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil)
|
||||
a.uniqueID = "missing-id-" + strconv.FormatInt(time.Now().Unix(), 10) // This is a fake unique - it just has to not be reused...
|
||||
return a.uniqueID
|
||||
}
|
||||
|
||||
func (a *fyneApp) NewWindow(title string) fyne.Window {
|
||||
return a.driver.CreateWindow(title)
|
||||
}
|
||||
|
||||
func (a *fyneApp) Run() {
|
||||
go a.lifecycle.RunEventQueue(a.driver.DoFromGoroutine)
|
||||
|
||||
if !a.driver.Device().IsMobile() {
|
||||
a.settings.watchSettings()
|
||||
}
|
||||
|
||||
a.driver.Run()
|
||||
}
|
||||
|
||||
func (a *fyneApp) Quit() {
|
||||
for _, window := range a.driver.AllWindows() {
|
||||
window.Close()
|
||||
}
|
||||
|
||||
a.driver.Quit()
|
||||
a.settings.stopWatching()
|
||||
}
|
||||
|
||||
func (a *fyneApp) Driver() fyne.Driver {
|
||||
return a.driver
|
||||
}
|
||||
|
||||
// Settings returns the application settings currently configured.
|
||||
func (a *fyneApp) Settings() fyne.Settings {
|
||||
return a.settings
|
||||
}
|
||||
|
||||
func (a *fyneApp) Storage() fyne.Storage {
|
||||
return a.storage
|
||||
}
|
||||
|
||||
func (a *fyneApp) Preferences() fyne.Preferences {
|
||||
if a.UniqueID() == "" {
|
||||
fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil)
|
||||
}
|
||||
return a.prefs
|
||||
}
|
||||
|
||||
func (a *fyneApp) Lifecycle() fyne.Lifecycle {
|
||||
return &a.lifecycle
|
||||
}
|
||||
|
||||
func (a *fyneApp) newDefaultPreferences() *preferences {
|
||||
p := newPreferences(a)
|
||||
if a.uniqueID != "" {
|
||||
p.load()
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (a *fyneApp) Clipboard() fyne.Clipboard {
|
||||
return a.clipboard
|
||||
}
|
||||
|
||||
// New returns a new application instance with the default driver and no unique ID (unless specified in FyneApp.toml)
|
||||
func New() fyne.App {
|
||||
if meta.ID == "" {
|
||||
checkLocalMetadata() // if no ID passed, check if it was in toml
|
||||
if meta.ID == "" {
|
||||
internal.LogHint("Applications should be created with a unique ID using app.NewWithID()")
|
||||
}
|
||||
}
|
||||
return NewWithID(meta.ID)
|
||||
}
|
||||
|
||||
func makeStoreDocs(id string, s *store) *internal.Docs {
|
||||
if id == "" {
|
||||
return &internal.Docs{} // an empty impl to avoid crashes
|
||||
}
|
||||
if root := s.a.storageRoot(); root != "" {
|
||||
uri, err := storage.ParseURI(root)
|
||||
if err != nil {
|
||||
uri = storage.NewFileURI(root)
|
||||
}
|
||||
|
||||
exists, err := storage.Exists(uri)
|
||||
if !exists || err != nil {
|
||||
err = storage.CreateListable(uri)
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to create app storage space", err)
|
||||
}
|
||||
}
|
||||
|
||||
root, _ := s.docRootURI()
|
||||
return &internal.Docs{RootDocURI: root}
|
||||
} else {
|
||||
return &internal.Docs{} // an empty impl to avoid crashes
|
||||
}
|
||||
}
|
||||
|
||||
func newAppWithDriver(d fyne.Driver, clipboard fyne.Clipboard, id string) fyne.App {
|
||||
newApp := &fyneApp{uniqueID: id, clipboard: clipboard, driver: d}
|
||||
fyne.SetCurrentApp(newApp)
|
||||
|
||||
newApp.prefs = newApp.newDefaultPreferences()
|
||||
newApp.lifecycle.InitEventQueue()
|
||||
newApp.lifecycle.SetOnStoppedHookExecuted(func() {
|
||||
if prefs, ok := newApp.prefs.(*preferences); ok {
|
||||
prefs.forceImmediateSave()
|
||||
}
|
||||
})
|
||||
|
||||
newApp.registerRepositories() // for web this may provide docs / settings
|
||||
newApp.settings = loadSettings()
|
||||
store := &store{a: newApp}
|
||||
store.Docs = makeStoreDocs(id, store)
|
||||
newApp.storage = store
|
||||
|
||||
httpHandler := intRepo.NewHTTPRepository()
|
||||
repository.Register("http", httpHandler)
|
||||
repository.Register("https", httpHandler)
|
||||
return newApp
|
||||
}
|
||||
|
||||
// marker interface to pass system tray to supporting drivers
|
||||
type systrayDriver interface {
|
||||
SetSystemTrayMenu(*fyne.Menu)
|
||||
SetSystemTrayIcon(resource fyne.Resource)
|
||||
}
|
||||
59
vendor/fyne.io/fyne/v2/app/app_darwin.go
generated
vendored
Normal file
59
vendor/fyne.io/fyne/v2/app/app_darwin.go
generated
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
//go:build !ci && !wasm && !test_web_driver && !mobile
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
bool isBundled();
|
||||
void sendNotification(char *title, char *content);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
func (a *fyneApp) SendNotification(n *fyne.Notification) {
|
||||
if C.isBundled() {
|
||||
titleStr := C.CString(n.Title)
|
||||
defer C.free(unsafe.Pointer(titleStr))
|
||||
contentStr := C.CString(n.Content)
|
||||
defer C.free(unsafe.Pointer(contentStr))
|
||||
|
||||
C.sendNotification(titleStr, contentStr)
|
||||
return
|
||||
}
|
||||
|
||||
fallbackNotification(n.Title, n.Content)
|
||||
}
|
||||
|
||||
func escapeNotificationString(in string) string {
|
||||
noSlash := strings.ReplaceAll(in, "\\", "\\\\")
|
||||
return strings.ReplaceAll(noSlash, "\"", "\\\"")
|
||||
}
|
||||
|
||||
//export fallbackSend
|
||||
func fallbackSend(cTitle, cContent *C.char) {
|
||||
title := C.GoString(cTitle)
|
||||
content := C.GoString(cContent)
|
||||
fallbackNotification(title, content)
|
||||
}
|
||||
|
||||
func fallbackNotification(title, content string) {
|
||||
template := `display notification "%s" with title "%s"`
|
||||
script := fmt.Sprintf(template, escapeNotificationString(content), escapeNotificationString(title))
|
||||
|
||||
err := exec.Command("osascript", "-e", script).Start()
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to launch darwin notify script", err)
|
||||
}
|
||||
}
|
||||
60
vendor/fyne.io/fyne/v2/app/app_darwin.m
generated
vendored
Normal file
60
vendor/fyne.io/fyne/v2/app/app_darwin.m
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
//go:build !ci && !wasm && !test_web_driver && !mobile
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 || TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#endif
|
||||
|
||||
static int notifyNum = 0;
|
||||
|
||||
extern void fallbackSend(char *cTitle, char *cBody);
|
||||
|
||||
bool isBundled() {
|
||||
return [[NSBundle mainBundle] bundleIdentifier] != nil;
|
||||
}
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 || TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
|
||||
void doSendNotification(UNUserNotificationCenter *center, NSString *title, NSString *body) {
|
||||
UNMutableNotificationContent *content = [UNMutableNotificationContent new];
|
||||
[content autorelease];
|
||||
content.title = title;
|
||||
content.body = body;
|
||||
|
||||
notifyNum++;
|
||||
NSString *identifier = [NSString stringWithFormat:@"fyne-notify-%d", notifyNum];
|
||||
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
|
||||
content:content trigger:nil];
|
||||
|
||||
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
NSLog(@"Could not send notification: %@", error);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void sendNotification(char *cTitle, char *cBody) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
NSString *title = [NSString stringWithUTF8String:cTitle];
|
||||
NSString *body = [NSString stringWithUTF8String:cBody];
|
||||
|
||||
UNAuthorizationOptions options = UNAuthorizationOptionAlert;
|
||||
[center requestAuthorizationWithOptions:options
|
||||
completionHandler:^(BOOL granted, NSError *_Nullable error) {
|
||||
if (!granted) {
|
||||
if (error != NULL) {
|
||||
NSLog(@"Error asking for permission to send notifications %@", error);
|
||||
// this happens if our app was not signed, so do it the old way
|
||||
fallbackSend((char *)[title UTF8String], (char *)[body UTF8String]);
|
||||
} else {
|
||||
NSLog(@"Unable to get permission to send notifications");
|
||||
}
|
||||
} else {
|
||||
doSendNotification(center, title, body);
|
||||
}
|
||||
}];
|
||||
}
|
||||
#else
|
||||
void sendNotification(char *cTitle, char *cBody) {
|
||||
fallbackSend(cTitle, cBody);
|
||||
}
|
||||
#endif
|
||||
54
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go
generated
vendored
Normal file
54
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go
generated
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
//go:build !ci && !ios && !wasm && !test_web_driver && !mobile
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation
|
||||
|
||||
#include <AppKit/AppKit.h>
|
||||
|
||||
bool isBundled();
|
||||
void watchTheme();
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
cmd := exec.Command("open", url.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// SetSystemTrayIcon sets a custom image for the system tray icon.
|
||||
// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
|
||||
func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
|
||||
a.Driver().(systrayDriver).SetSystemTrayIcon(icon)
|
||||
}
|
||||
|
||||
// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
|
||||
// By default this will use the application icon.
|
||||
func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
|
||||
if desk, ok := a.Driver().(systrayDriver); ok {
|
||||
desk.SetSystemTrayMenu(menu)
|
||||
}
|
||||
}
|
||||
|
||||
//export themeChanged
|
||||
func themeChanged() {
|
||||
fyne.CurrentApp().Settings().(*settings).setupTheme()
|
||||
}
|
||||
|
||||
func watchTheme(_ *settings) {
|
||||
C.watchTheme()
|
||||
}
|
||||
|
||||
func (a *fyneApp) registerRepositories() {
|
||||
// no-op
|
||||
}
|
||||
12
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m
generated
vendored
Normal file
12
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
//go:build !ci && !ios && !wasm && !test_web_driver && !mobile
|
||||
|
||||
extern void themeChanged();
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void watchTheme() {
|
||||
[[NSDistributedNotificationCenter defaultCenter] addObserverForName:@"AppleInterfaceThemeChangedNotification" object:nil queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
themeChanged(); // calls back into Go
|
||||
}];
|
||||
}
|
||||
14
vendor/fyne.io/fyne/v2/app/app_gl.go
generated
vendored
Normal file
14
vendor/fyne.io/fyne/v2/app/app_gl.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build !ci && !android && !ios && !mobile
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/driver/glfw"
|
||||
)
|
||||
|
||||
// NewWithID returns a new app instance using the appropriate runtime driver.
|
||||
// The ID string should be globally unique to this app.
|
||||
func NewWithID(id string) fyne.App {
|
||||
return newAppWithDriver(glfw.NewGLDriver(), glfw.NewClipboard(), id)
|
||||
}
|
||||
26
vendor/fyne.io/fyne/v2/app/app_mobile.go
generated
vendored
Normal file
26
vendor/fyne.io/fyne/v2/app/app_mobile.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
//go:build !ci && (android || ios || mobile)
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
internalapp "fyne.io/fyne/v2/internal/app"
|
||||
"fyne.io/fyne/v2/internal/driver/mobile"
|
||||
)
|
||||
|
||||
// NewWithID returns a new app instance using the appropriate runtime driver.
|
||||
// The ID string should be globally unique to this app.
|
||||
func NewWithID(id string) fyne.App {
|
||||
d := mobile.NewGoMobileDriver()
|
||||
a := newAppWithDriver(d, mobile.NewClipboard(), id)
|
||||
d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) {
|
||||
internalapp.SystemTheme = c.SystemTheme
|
||||
|
||||
a.Settings().(*settings).setupTheme()
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *fyneApp) registerRepositories() {
|
||||
// no-op
|
||||
}
|
||||
130
vendor/fyne.io/fyne/v2/app/app_mobile_and.c
generated
vendored
Normal file
130
vendor/fyne.io/fyne/v2/app/app_mobile_and.c
generated
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
//go:build !ci && android
|
||||
|
||||
#include <android/log.h>
|
||||
#include <jni.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__)
|
||||
|
||||
static jclass find_class(JNIEnv *env, const char *class_name) {
|
||||
jclass clazz = (*env)->FindClass(env, class_name);
|
||||
if (clazz == NULL) {
|
||||
(*env)->ExceptionClear(env);
|
||||
LOG_FATAL("cannot find %s", class_name);
|
||||
return NULL;
|
||||
}
|
||||
return clazz;
|
||||
}
|
||||
|
||||
static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
|
||||
if (m == 0) {
|
||||
(*env)->ExceptionClear(env);
|
||||
LOG_FATAL("cannot find method %s %s", name, sig);
|
||||
return 0;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig);
|
||||
if (m == 0) {
|
||||
(*env)->ExceptionClear(env);
|
||||
LOG_FATAL("cannot find method %s %s", name, sig);
|
||||
return 0;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
jobject getSystemService(uintptr_t jni_env, uintptr_t ctx, char *service) {
|
||||
JNIEnv *env = (JNIEnv*)jni_env;
|
||||
jstring serviceStr = (*env)->NewStringUTF(env, service);
|
||||
|
||||
jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
|
||||
jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
|
||||
|
||||
return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, serviceStr);
|
||||
}
|
||||
|
||||
int nextId = 1;
|
||||
|
||||
bool isOreoOrLater(JNIEnv *env) {
|
||||
jclass versionClass = find_class(env, "android/os/Build$VERSION" );
|
||||
jfieldID sdkIntFieldID = (*env)->GetStaticFieldID(env, versionClass, "SDK_INT", "I" );
|
||||
int sdkVersion = (*env)->GetStaticIntField(env, versionClass, sdkIntFieldID );
|
||||
|
||||
return sdkVersion >= 26; // O = Oreo, will not be defined for older builds
|
||||
}
|
||||
|
||||
jobject parseURL(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
||||
JNIEnv *env = (JNIEnv*)jni_env;
|
||||
|
||||
jstring uriStr = (*env)->NewStringUTF(env, uriCstr);
|
||||
jclass uriClass = find_class(env, "android/net/Uri");
|
||||
jmethodID parse = find_static_method(env, uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
|
||||
|
||||
return (jobject)(*env)->CallStaticObjectMethod(env, uriClass, parse, uriStr);
|
||||
}
|
||||
|
||||
void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url) {
|
||||
JNIEnv *env = (JNIEnv*)jni_env;
|
||||
jobject uri = parseURL(jni_env, ctx, url);
|
||||
|
||||
jclass intentClass = find_class(env, "android/content/Intent");
|
||||
jfieldID viewFieldID = (*env)->GetStaticFieldID(env, intentClass, "ACTION_VIEW", "Ljava/lang/String;" );
|
||||
jstring view = (*env)->GetStaticObjectField(env, intentClass, viewFieldID);
|
||||
|
||||
jmethodID constructor = find_method(env, intentClass, "<init>", "(Ljava/lang/String;Landroid/net/Uri;)V");
|
||||
jobject intent = (*env)->NewObject(env, intentClass, constructor, view, uri);
|
||||
|
||||
jclass contextClass = find_class(env, "android/content/Context");
|
||||
jmethodID start = find_method(env, contextClass, "startActivity", "(Landroid/content/Intent;)V");
|
||||
(*env)->CallVoidMethod(env, (jobject)ctx, start, intent);
|
||||
}
|
||||
|
||||
void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *body) {
|
||||
JNIEnv *env = (JNIEnv*)jni_env;
|
||||
jstring titleStr = (*env)->NewStringUTF(env, title);
|
||||
jstring bodyStr = (*env)->NewStringUTF(env, body);
|
||||
|
||||
jclass cls = find_class(env, "android/app/Notification$Builder");
|
||||
jmethodID constructor = find_method(env, cls, "<init>", "(Landroid/content/Context;)V");
|
||||
jobject builder = (*env)->NewObject(env, cls, constructor, ctx);
|
||||
|
||||
jclass mgrCls = find_class(env, "android/app/NotificationManager");
|
||||
jobject mgr = getSystemService((uintptr_t)env, ctx, "notification");
|
||||
|
||||
if (isOreoOrLater(env)) {
|
||||
jstring channelId = (*env)->NewStringUTF(env, "fyne-notif");
|
||||
jstring name = (*env)->NewStringUTF(env, "Fyne Notification");
|
||||
int importance = 4; // IMPORTANCE_HIGH
|
||||
|
||||
jclass chanCls = find_class(env, "android/app/NotificationChannel");
|
||||
jmethodID constructor = find_method(env, chanCls, "<init>", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V");
|
||||
jobject channel = (*env)->NewObject(env, chanCls, constructor, channelId, name, importance);
|
||||
|
||||
jmethodID createChannel = find_method(env, mgrCls, "createNotificationChannel", "(Landroid/app/NotificationChannel;)V");
|
||||
(*env)->CallVoidMethod(env, mgr, createChannel, channel);
|
||||
|
||||
jmethodID setChannelId = find_method(env, cls, "setChannelId", "(Ljava/lang/String;)Landroid/app/Notification$Builder;");
|
||||
(*env)->CallObjectMethod(env, builder, setChannelId, channelId);
|
||||
}
|
||||
|
||||
jmethodID setContentTitle = find_method(env, cls, "setContentTitle", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;");
|
||||
(*env)->CallObjectMethod(env, builder, setContentTitle, titleStr);
|
||||
|
||||
jmethodID setContentText = find_method(env, cls, "setContentText", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;");
|
||||
(*env)->CallObjectMethod(env, builder, setContentText, bodyStr);
|
||||
|
||||
int iconID = 17629184; // constant of "unknown app icon"
|
||||
jmethodID setSmallIcon = find_method(env, cls, "setSmallIcon", "(I)Landroid/app/Notification$Builder;");
|
||||
(*env)->CallObjectMethod(env, builder, setSmallIcon, iconID);
|
||||
|
||||
jmethodID build = find_method(env, cls, "build", "()Landroid/app/Notification;");
|
||||
jobject notif = (*env)->CallObjectMethod(env, builder, build);
|
||||
|
||||
jmethodID notify = find_method(env, mgrCls, "notify", "(ILandroid/app/Notification;)V");
|
||||
(*env)->CallVoidMethod(env, mgr, notify, nextId, notif);
|
||||
nextId++;
|
||||
}
|
||||
43
vendor/fyne.io/fyne/v2/app/app_mobile_and.go
generated
vendored
Normal file
43
vendor/fyne.io/fyne/v2/app/app_mobile_and.go
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
//go:build !ci && android
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -landroid -llog
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url);
|
||||
void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *content);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"net/url"
|
||||
"unsafe"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/driver/mobile/app"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
urlStr := C.CString(url.String())
|
||||
defer C.free(unsafe.Pointer(urlStr))
|
||||
|
||||
app.RunOnJVM(func(vm, env, ctx uintptr) error {
|
||||
C.openURL(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), urlStr)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *fyneApp) SendNotification(n *fyne.Notification) {
|
||||
titleStr := C.CString(n.Title)
|
||||
defer C.free(unsafe.Pointer(titleStr))
|
||||
contentStr := C.CString(n.Content)
|
||||
defer C.free(unsafe.Pointer(contentStr))
|
||||
|
||||
app.RunOnJVM(func(vm, env, ctx uintptr) error {
|
||||
C.sendNotification(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), titleStr, contentStr)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
26
vendor/fyne.io/fyne/v2/app/app_mobile_ios.go
generated
vendored
Normal file
26
vendor/fyne.io/fyne/v2/app/app_mobile_ios.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
//go:build !ci && ios && !mobile
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework UIKit -framework UserNotifications
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
void openURL(char *urlStr);
|
||||
void sendNotification(char *title, char *content);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"net/url"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
urlStr := C.CString(url.String())
|
||||
C.openURL(urlStr)
|
||||
C.free(unsafe.Pointer(urlStr))
|
||||
|
||||
return nil
|
||||
}
|
||||
10
vendor/fyne.io/fyne/v2/app/app_mobile_ios.m
generated
vendored
Normal file
10
vendor/fyne.io/fyne/v2/app/app_mobile_ios.m
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
//go:build !ci && ios
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
void openURL(char *urlStr) {
|
||||
UIApplication *app = [UIApplication sharedApplication];
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlStr]];
|
||||
[app openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
|
||||
22
vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go
generated
vendored
Normal file
22
vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
//go:build !ci && mobile && !android && !ios
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(_ *url.URL) error {
|
||||
return errors.New("mobile simulator does not support open URLs yet")
|
||||
}
|
||||
|
||||
func (a *fyneApp) SendNotification(_ *fyne.Notification) {
|
||||
fyne.LogError("Notifications are not supported in the mobile simulator yet", nil)
|
||||
}
|
||||
|
||||
func watchTheme(_ *settings) {
|
||||
// not implemented yet
|
||||
}
|
||||
8
vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go
generated
vendored
Normal file
8
vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
//go:build !ci && !legacy && !wasm && !test_web_driver
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework Foundation -framework UserNotifications
|
||||
*/
|
||||
import "C"
|
||||
18
vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go
generated
vendored
Normal file
18
vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
//go:build !ci && wasm
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
window := js.Global().Call("open", url.String(), "_blank", "")
|
||||
if window.Equal(js.Null()) {
|
||||
return fmt.Errorf("Unable to open a new window/tab for URL: %v.", url)
|
||||
}
|
||||
window.Call("focus")
|
||||
return nil
|
||||
}
|
||||
12
vendor/fyne.io/fyne/v2/app/app_openurl_web.go
generated
vendored
Normal file
12
vendor/fyne.io/fyne/v2/app/app_openurl_web.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
//go:build !ci && !wasm && test_web_driver
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
return errors.New("OpenURL is not supported with the test web driver.")
|
||||
}
|
||||
26
vendor/fyne.io/fyne/v2/app/app_other.go
generated
vendored
Normal file
26
vendor/fyne.io/fyne/v2/app/app_other.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
//go:build ci || (!ios && !android && !linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver)
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
func (a *fyneApp) OpenURL(_ *url.URL) error {
|
||||
return errors.New("Unable to open url for unknown operating system")
|
||||
}
|
||||
|
||||
func (a *fyneApp) SendNotification(_ *fyne.Notification) {
|
||||
fyne.LogError("Refusing to show notification for unknown operating system", nil)
|
||||
}
|
||||
|
||||
func watchTheme(_ *settings) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
func (a *fyneApp) registerRepositories() {
|
||||
// no-op
|
||||
}
|
||||
15
vendor/fyne.io/fyne/v2/app/app_software.go
generated
vendored
Normal file
15
vendor/fyne.io/fyne/v2/app/app_software.go
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build ci
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/painter/software"
|
||||
"fyne.io/fyne/v2/test"
|
||||
)
|
||||
|
||||
// NewWithID returns a new app instance using the test (headless) driver.
|
||||
// The ID string should be globally unique to this app.
|
||||
func NewWithID(id string) fyne.App {
|
||||
return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), test.NewClipboard(), id)
|
||||
}
|
||||
77
vendor/fyne.io/fyne/v2/app/app_wasm.go
generated
vendored
Normal file
77
vendor/fyne.io/fyne/v2/app/app_wasm.go
generated
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
//go:build !ci && (!android || !ios || !mobile) && (wasm || test_web_driver)
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"syscall/js"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
intRepo "fyne.io/fyne/v2/internal/repository"
|
||||
"fyne.io/fyne/v2/storage/repository"
|
||||
)
|
||||
|
||||
func (a *fyneApp) SendNotification(n *fyne.Notification) {
|
||||
window := js.Global().Get("window")
|
||||
if window.IsUndefined() {
|
||||
fyne.LogError("Current browser does not support notifications.", nil)
|
||||
return
|
||||
}
|
||||
notification := window.Get("Notification")
|
||||
if window.IsUndefined() {
|
||||
fyne.LogError("Current browser does not support notifications.", nil)
|
||||
return
|
||||
}
|
||||
// check permission
|
||||
permission := notification.Get("permission")
|
||||
showNotification := func() {
|
||||
icon := a.icon.Content()
|
||||
base64Str := base64.StdEncoding.EncodeToString(icon)
|
||||
mimeType := http.DetectContentType(icon)
|
||||
base64Img := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)
|
||||
notification.New(n.Title, map[string]any{
|
||||
"body": n.Content,
|
||||
"icon": base64Img,
|
||||
})
|
||||
fyne.LogError("done show...", nil)
|
||||
}
|
||||
if permission.Type() != js.TypeString || permission.String() != "granted" {
|
||||
// need to request for permission
|
||||
notification.Call("requestPermission", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) > 0 && args[0].Type() == js.TypeString && args[0].String() == "granted" {
|
||||
showNotification()
|
||||
} else {
|
||||
fyne.LogError("User rejected the request for notifications.", nil)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
} else {
|
||||
showNotification()
|
||||
}
|
||||
}
|
||||
|
||||
var themeChanged = js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) > 0 && args[0].Type() == js.TypeObject {
|
||||
fyne.CurrentApp().Settings().(*settings).setupTheme()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
func watchTheme(_ *settings) {
|
||||
js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("addEventListener", "change", themeChanged)
|
||||
}
|
||||
func stopWatchingTheme() {
|
||||
js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("removeEventListener", "change", themeChanged)
|
||||
}
|
||||
|
||||
func (a *fyneApp) registerRepositories() {
|
||||
repo, err := intRepo.NewIndexDBRepository()
|
||||
if err != nil {
|
||||
fyne.LogError("failed to create repository: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
repository.Register("idbfile", repo)
|
||||
}
|
||||
100
vendor/fyne.io/fyne/v2/app/app_windows.go
generated
vendored
Normal file
100
vendor/fyne.io/fyne/v2/app/app_windows.go
generated
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
//go:build !ci && !android && !ios && !wasm && !test_web_driver
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
internalapp "fyne.io/fyne/v2/internal/app"
|
||||
)
|
||||
|
||||
const notificationTemplate = `$title = "%s"
|
||||
$content = "%s"
|
||||
$iconPath = "file:///%s"
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
||||
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastImageAndText02)
|
||||
$toastXml = [xml] $template.GetXml()
|
||||
$toastXml.GetElementsByTagName("text")[0].AppendChild($toastXml.CreateTextNode($title)) > $null
|
||||
$toastXml.GetElementsByTagName("text")[1].AppendChild($toastXml.CreateTextNode($content)) > $null
|
||||
$toastXml.GetElementsByTagName("image")[0].SetAttribute("src", $iconPath) > $null
|
||||
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
$xml.LoadXml($toastXml.OuterXml)
|
||||
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("%s").Show($toast);`
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", url.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
var scriptNum = 0
|
||||
|
||||
func (a *fyneApp) SendNotification(n *fyne.Notification) {
|
||||
title := escapeNotificationString(n.Title)
|
||||
content := escapeNotificationString(n.Content)
|
||||
iconFilePath := a.cachedIconPath()
|
||||
appID := a.UniqueID()
|
||||
if appID == "" || strings.Index(appID, "missing-id") == 0 {
|
||||
appID = a.Metadata().Name
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(notificationTemplate, title, content, iconFilePath, appID)
|
||||
go runScript("notify", script)
|
||||
}
|
||||
|
||||
// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
|
||||
// By default this will use the application icon.
|
||||
func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
|
||||
a.Driver().(systrayDriver).SetSystemTrayMenu(menu)
|
||||
}
|
||||
|
||||
// SetSystemTrayIcon sets a custom image for the system tray icon.
|
||||
// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
|
||||
func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
|
||||
a.Driver().(systrayDriver).SetSystemTrayIcon(icon)
|
||||
}
|
||||
|
||||
func escapeNotificationString(in string) string {
|
||||
noSlash := strings.ReplaceAll(in, "`", "``")
|
||||
return strings.ReplaceAll(noSlash, "\"", "`\"")
|
||||
}
|
||||
|
||||
func runScript(name, script string) {
|
||||
scriptNum++
|
||||
appID := fyne.CurrentApp().UniqueID()
|
||||
fileName := fmt.Sprintf("fyne-%s-%s-%d.ps1", appID, name, scriptNum)
|
||||
|
||||
tmpFilePath := filepath.Join(os.TempDir(), fileName)
|
||||
err := os.WriteFile(tmpFilePath, []byte(script), 0600)
|
||||
if err != nil {
|
||||
fyne.LogError("Could not write script to show notification", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmpFilePath)
|
||||
|
||||
launch := "(Get-Content -Encoding UTF8 -Path " + tmpFilePath + " -Raw) | Invoke-Expression"
|
||||
cmd := exec.Command("PowerShell", "-ExecutionPolicy", "Bypass", launch)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to launch windows notify script", err)
|
||||
}
|
||||
}
|
||||
|
||||
func watchTheme(s *settings) {
|
||||
go internalapp.WatchTheme(func() {
|
||||
fyne.Do(s.setupTheme)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *fyneApp) registerRepositories() {
|
||||
// no-op
|
||||
}
|
||||
139
vendor/fyne.io/fyne/v2/app/app_xdg.go
generated
vendored
Normal file
139
vendor/fyne.io/fyne/v2/app/app_xdg.go
generated
vendored
Normal file
@ -0,0 +1,139 @@
|
||||
//go:build !ci && !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd)
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/rymdport/portal/notification"
|
||||
"github.com/rymdport/portal/openuri"
|
||||
portalSettings "github.com/rymdport/portal/settings"
|
||||
"github.com/rymdport/portal/settings/appearance"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
internalapp "fyne.io/fyne/v2/internal/app"
|
||||
"fyne.io/fyne/v2/internal/build"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
)
|
||||
|
||||
const systemTheme = fyne.ThemeVariant(99)
|
||||
|
||||
func (a *fyneApp) OpenURL(url *url.URL) error {
|
||||
if build.IsFlatpak {
|
||||
err := openuri.OpenURI("", url.String(), nil)
|
||||
if err != nil {
|
||||
fyne.LogError("Opening url in portal failed", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("xdg-open", url.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// fetch color variant from dbus portal desktop settings.
|
||||
func findFreedesktopColorScheme() fyne.ThemeVariant {
|
||||
colorScheme, err := appearance.GetColorScheme()
|
||||
if err != nil {
|
||||
return systemTheme
|
||||
}
|
||||
|
||||
return colorSchemeToThemeVariant(colorScheme)
|
||||
}
|
||||
|
||||
func colorSchemeToThemeVariant(colorScheme appearance.ColorScheme) fyne.ThemeVariant {
|
||||
switch colorScheme {
|
||||
case appearance.Light:
|
||||
return theme.VariantLight
|
||||
case appearance.Dark:
|
||||
return theme.VariantDark
|
||||
}
|
||||
|
||||
// Default to light theme to support Gnome's default see https://github.com/fyne-io/fyne/pull/3561
|
||||
return theme.VariantLight
|
||||
}
|
||||
|
||||
func (a *fyneApp) SendNotification(n *fyne.Notification) {
|
||||
if build.IsFlatpak {
|
||||
err := a.sendNotificationThroughPortal(n)
|
||||
if err != nil {
|
||||
fyne.LogError("Sending notification using portal failed", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := dbus.SessionBus() // shared connection, don't close
|
||||
if err != nil {
|
||||
fyne.LogError("Unable to connect to session D-Bus", err)
|
||||
return
|
||||
}
|
||||
|
||||
appIcon := a.cachedIconPath()
|
||||
timeout := int32(0) // we don't support this yet
|
||||
|
||||
obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||
call := obj.Call("org.freedesktop.Notifications.Notify", 0, a.uniqueID, uint32(0),
|
||||
appIcon, n.Title, n.Content, []string{}, map[string]dbus.Variant{}, timeout)
|
||||
if call.Err != nil {
|
||||
fyne.LogError("Failed to send message to bus", call.Err)
|
||||
}
|
||||
}
|
||||
|
||||
// Sending with same ID replaces the old notification.
|
||||
var notificationID atomic.Uint64
|
||||
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/#gdbus-org.freedesktop.portal.Notification.
|
||||
func (a *fyneApp) sendNotificationThroughPortal(n *fyne.Notification) error {
|
||||
return notification.Add(
|
||||
uint(notificationID.Add(1)),
|
||||
notification.Content{
|
||||
Title: n.Title,
|
||||
Body: n.Content,
|
||||
Icon: a.uniqueID,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
|
||||
// By default this will use the application icon.
|
||||
func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
|
||||
if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag
|
||||
desk.SetSystemTrayMenu(menu)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSystemTrayIcon sets a custom image for the system tray icon.
|
||||
// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
|
||||
func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
|
||||
if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag
|
||||
desk.SetSystemTrayIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
func watchTheme(s *settings) {
|
||||
go func() {
|
||||
// Theme lookup hangs on some desktops. Update theme variant cache from within goroutine.
|
||||
themeVariant := findFreedesktopColorScheme()
|
||||
if themeVariant != systemTheme {
|
||||
internalapp.CurrentVariant.Store(uint64(themeVariant))
|
||||
fyne.Do(func() { s.applyVariant(themeVariant) })
|
||||
}
|
||||
|
||||
portalSettings.OnSignalSettingChanged(func(changed portalSettings.Changed) {
|
||||
if changed.Namespace == appearance.Namespace && changed.Key == "color-scheme" {
|
||||
themeVariant := colorSchemeToThemeVariant(appearance.ColorScheme(changed.Value.(uint32)))
|
||||
internalapp.CurrentVariant.Store(uint64(themeVariant))
|
||||
fyne.Do(func() { s.applyVariant(themeVariant) })
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *fyneApp) registerRepositories() {
|
||||
// no-op
|
||||
}
|
||||
47
vendor/fyne.io/fyne/v2/app/cloud.go
generated
vendored
Normal file
47
vendor/fyne.io/fyne/v2/app/cloud.go
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import "fyne.io/fyne/v2"
|
||||
|
||||
func (a *fyneApp) SetCloudProvider(p fyne.CloudProvider) {
|
||||
if p == nil {
|
||||
a.cloud = nil
|
||||
return
|
||||
}
|
||||
|
||||
a.transitionCloud(p)
|
||||
}
|
||||
|
||||
func (a *fyneApp) transitionCloud(p fyne.CloudProvider) {
|
||||
if a.cloud != nil {
|
||||
a.cloud.Cleanup(a)
|
||||
}
|
||||
|
||||
err := p.Setup(a)
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to set up cloud provider "+p.ProviderName(), err)
|
||||
return
|
||||
}
|
||||
a.cloud = p
|
||||
|
||||
listeners := a.prefs.ChangeListeners()
|
||||
if pp, ok := p.(fyne.CloudProviderPreferences); ok {
|
||||
a.prefs = pp.CloudPreferences(a)
|
||||
} else {
|
||||
a.prefs = a.newDefaultPreferences()
|
||||
}
|
||||
if cloud, ok := p.(fyne.CloudProviderStorage); ok {
|
||||
a.storage = cloud.CloudStorage(a)
|
||||
} else {
|
||||
store := &store{a: a}
|
||||
store.Docs = makeStoreDocs(a.uniqueID, store)
|
||||
a.storage = store
|
||||
}
|
||||
|
||||
for _, l := range listeners {
|
||||
a.prefs.AddChangeListener(l)
|
||||
l() // assume that preferences have changed because we replaced the provider
|
||||
}
|
||||
|
||||
// after transition ensure settings listener is fired
|
||||
a.settings.apply()
|
||||
}
|
||||
59
vendor/fyne.io/fyne/v2/app/icon_cache_file.go
generated
vendored
Normal file
59
vendor/fyne.io/fyne/v2/app/icon_cache_file.go
generated
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
|
||||
func (a *fyneApp) cachedIconPath() string {
|
||||
if a.Icon() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
dirPath := filepath.Join(rootCacheDir(), a.UniqueID())
|
||||
filePath := filepath.Join(dirPath, "icon.png")
|
||||
once.Do(func() {
|
||||
err := a.saveIconToCache(dirPath, filePath)
|
||||
if err != nil {
|
||||
filePath = ""
|
||||
}
|
||||
})
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
func rootCacheDir() string {
|
||||
desktopCache, _ := os.UserCacheDir()
|
||||
return filepath.Join(desktopCache, "fyne")
|
||||
}
|
||||
|
||||
func (a *fyneApp) saveIconToCache(dirPath, filePath string) error {
|
||||
err := os.MkdirAll(dirPath, 0700)
|
||||
if err != nil {
|
||||
fyne.LogError("Unable to create application cache directory", err)
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fyne.LogError("Unable to create icon file", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
if icon := a.Icon(); icon != nil {
|
||||
_, err = file.Write(icon.Content())
|
||||
if err != nil {
|
||||
fyne.LogError("Unable to write icon contents", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
36
vendor/fyne.io/fyne/v2/app/meta.go
generated
vendored
Normal file
36
vendor/fyne.io/fyne/v2/app/meta.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
var meta = fyne.AppMetadata{
|
||||
ID: "",
|
||||
Name: "",
|
||||
Version: "0.0.1",
|
||||
Build: 1,
|
||||
Release: false,
|
||||
Custom: map[string]string{},
|
||||
Migrations: map[string]bool{},
|
||||
}
|
||||
|
||||
// SetMetadata overrides the packaged application metadata.
|
||||
// This data can be used in many places like notifications and about screens.
|
||||
func SetMetadata(m fyne.AppMetadata) {
|
||||
meta = m
|
||||
|
||||
if meta.Custom == nil {
|
||||
meta.Custom = map[string]string{}
|
||||
}
|
||||
if meta.Migrations == nil {
|
||||
meta.Migrations = map[string]bool{}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *fyneApp) Metadata() fyne.AppMetadata {
|
||||
if meta.ID == "" && meta.Name == "" {
|
||||
checkLocalMetadata()
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
65
vendor/fyne.io/fyne/v2/app/meta_development.go
generated
vendored
Normal file
65
vendor/fyne.io/fyne/v2/app/meta_development.go
generated
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/build"
|
||||
"fyne.io/fyne/v2/internal/metadata"
|
||||
)
|
||||
|
||||
func checkLocalMetadata() {
|
||||
if build.NoMetadata || build.Mode == fyne.BuildRelease {
|
||||
return
|
||||
}
|
||||
|
||||
dir := getProjectPath()
|
||||
file := filepath.Join(dir, "FyneApp.toml")
|
||||
ref, err := os.Open(file)
|
||||
if err != nil { // no worries, this is just an optional fallback
|
||||
return
|
||||
}
|
||||
defer ref.Close()
|
||||
|
||||
data, err := metadata.Load(ref)
|
||||
if err != nil || data == nil {
|
||||
fyne.LogError("failed to parse FyneApp.toml", err)
|
||||
return
|
||||
}
|
||||
|
||||
meta.ID = data.Details.ID
|
||||
meta.Name = data.Details.Name
|
||||
meta.Version = data.Details.Version
|
||||
meta.Build = data.Details.Build
|
||||
|
||||
if data.Details.Icon != "" {
|
||||
res, err := fyne.LoadResourceFromPath(data.Details.Icon)
|
||||
if err == nil {
|
||||
meta.Icon = metadata.ScaleIcon(res, 512)
|
||||
}
|
||||
}
|
||||
|
||||
meta.Release = false
|
||||
meta.Custom = data.Development
|
||||
meta.Migrations = data.Migrations
|
||||
}
|
||||
|
||||
func getProjectPath() string {
|
||||
exe, err := os.Executable()
|
||||
work, _ := os.Getwd()
|
||||
|
||||
if err != nil {
|
||||
fyne.LogError("failed to lookup build executable", err)
|
||||
return work
|
||||
}
|
||||
|
||||
temp := os.TempDir()
|
||||
if strings.Contains(exe, temp) || strings.Contains(exe, "go-build") { // this happens with "go run"
|
||||
return work
|
||||
}
|
||||
|
||||
// we were called with an executable from "go build"
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
191
vendor/fyne.io/fyne/v2/app/preferences.go
generated
vendored
Normal file
191
vendor/fyne.io/fyne/v2/app/preferences.go
generated
vendored
Normal file
@ -0,0 +1,191 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal"
|
||||
)
|
||||
|
||||
type preferences struct {
|
||||
*internal.InMemoryPreferences
|
||||
|
||||
prefLock sync.RWMutex
|
||||
savedRecently bool
|
||||
changedDuringSaving bool
|
||||
|
||||
app *fyneApp
|
||||
needsSaveBeforeExit bool
|
||||
}
|
||||
|
||||
// Declare conformity with Preferences interface
|
||||
var _ fyne.Preferences = (*preferences)(nil)
|
||||
|
||||
// sentinel error to signal an empty preferences storage backend was loaded
|
||||
var errEmptyPreferencesStore = errors.New("empty preferences store")
|
||||
|
||||
// returned from storageWriter() - may be a file, browser local storage, etc
|
||||
type writeSyncCloser interface {
|
||||
io.WriteCloser
|
||||
Sync() error
|
||||
}
|
||||
|
||||
// forceImmediateSave writes preferences to storage immediately, ignoring the debouncing
|
||||
// logic in the change listener. Does nothing if preferences are not backed with a persistent store.
|
||||
func (p *preferences) forceImmediateSave() {
|
||||
if !p.needsSaveBeforeExit {
|
||||
return
|
||||
}
|
||||
err := p.save()
|
||||
if err != nil {
|
||||
fyne.LogError("Failed on force saving preferences", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *preferences) resetSavedRecently() {
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer.
|
||||
|
||||
// For test reasons we need to use current app not what we were initialised with as they can differ
|
||||
fyne.DoAndWait(func() {
|
||||
p.prefLock.Lock()
|
||||
p.savedRecently = false
|
||||
changedDuringSaving := p.changedDuringSaving
|
||||
p.changedDuringSaving = false
|
||||
p.prefLock.Unlock()
|
||||
|
||||
if changedDuringSaving {
|
||||
p.save()
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *preferences) save() error {
|
||||
storage, err := p.storageWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.saveToStorage(storage)
|
||||
}
|
||||
|
||||
func (p *preferences) saveToStorage(writer writeSyncCloser) error {
|
||||
p.prefLock.Lock()
|
||||
p.savedRecently = true
|
||||
p.prefLock.Unlock()
|
||||
defer p.resetSavedRecently()
|
||||
|
||||
defer writer.Close()
|
||||
encode := json.NewEncoder(writer)
|
||||
|
||||
var err error
|
||||
p.InMemoryPreferences.ReadValues(func(values map[string]any) {
|
||||
err = encode.Encode(&values)
|
||||
})
|
||||
|
||||
err2 := writer.Sync()
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *preferences) load() {
|
||||
storage, err := p.storageReader()
|
||||
if err == nil {
|
||||
err = p.loadFromStorage(storage)
|
||||
}
|
||||
if err != nil && err != errEmptyPreferencesStore {
|
||||
fyne.LogError("Preferences load error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *preferences) loadFromStorage(storage io.ReadCloser) (err error) {
|
||||
defer func() {
|
||||
if r := storage.Close(); r != nil && err == nil {
|
||||
err = r
|
||||
}
|
||||
}()
|
||||
decode := json.NewDecoder(storage)
|
||||
|
||||
p.InMemoryPreferences.WriteValues(func(values map[string]any) {
|
||||
err = decode.Decode(&values)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
convertLists(values)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func newPreferences(app *fyneApp) *preferences {
|
||||
p := &preferences{}
|
||||
p.app = app
|
||||
p.InMemoryPreferences = internal.NewInMemoryPreferences()
|
||||
|
||||
// don't load or watch if not setup
|
||||
if app.uniqueID == "" && app.Metadata().ID == "" {
|
||||
return p
|
||||
}
|
||||
|
||||
p.needsSaveBeforeExit = true
|
||||
p.AddChangeListener(func() {
|
||||
if p != app.prefs {
|
||||
return
|
||||
}
|
||||
p.prefLock.Lock()
|
||||
shouldIgnoreChange := p.savedRecently
|
||||
if p.savedRecently {
|
||||
p.changedDuringSaving = true
|
||||
}
|
||||
p.prefLock.Unlock()
|
||||
|
||||
if shouldIgnoreChange { // callback after loading from storage, or too many updates in a row
|
||||
return
|
||||
}
|
||||
|
||||
err := p.save()
|
||||
if err != nil {
|
||||
fyne.LogError("Failed on saving preferences", err)
|
||||
}
|
||||
})
|
||||
p.watch()
|
||||
return p
|
||||
}
|
||||
|
||||
func convertLists(values map[string]any) {
|
||||
for k, v := range values {
|
||||
if items, ok := v.([]any); ok {
|
||||
if len(items) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch items[0].(type) {
|
||||
case bool:
|
||||
bools := make([]bool, len(items))
|
||||
for i, item := range items {
|
||||
bools[i] = item.(bool)
|
||||
}
|
||||
values[k] = bools
|
||||
case float64:
|
||||
floats := make([]float64, len(items))
|
||||
for i, item := range items {
|
||||
floats[i] = item.(float64)
|
||||
}
|
||||
values[k] = floats
|
||||
//case int: // json has no int!
|
||||
case string:
|
||||
strings := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
strings[i] = item.(string)
|
||||
}
|
||||
values[k] = strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
vendor/fyne.io/fyne/v2/app/preferences_android.go
generated
vendored
Normal file
24
vendor/fyne.io/fyne/v2/app/preferences_android.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
//go:build android
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2/internal/app"
|
||||
)
|
||||
|
||||
// storagePath returns the location of the settings storage
|
||||
func (p *preferences) storagePath() string {
|
||||
// we have no global storage, use app global instead - rootConfigDir looks up in app_mobile_and.go
|
||||
return filepath.Join(p.app.storageRoot(), "preferences.json")
|
||||
}
|
||||
|
||||
// storageRoot returns the location of the app storage
|
||||
func (a *fyneApp) storageRoot() string {
|
||||
return app.RootConfigDir() // we are in a sandbox, so no app ID added to this path
|
||||
}
|
||||
|
||||
func (p *preferences) watch() {
|
||||
// no-op on mobile
|
||||
}
|
||||
25
vendor/fyne.io/fyne/v2/app/preferences_ios.go
generated
vendored
Normal file
25
vendor/fyne.io/fyne/v2/app/preferences_ios.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
//go:build ios
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2/internal/app"
|
||||
)
|
||||
import "C"
|
||||
|
||||
// storagePath returns the location of the settings storage
|
||||
func (p *preferences) storagePath() string {
|
||||
ret := filepath.Join(p.app.storageRoot(), "preferences.json")
|
||||
return ret
|
||||
}
|
||||
|
||||
// storageRoot returns the location of the app storage
|
||||
func (a *fyneApp) storageRoot() string {
|
||||
return app.RootConfigDir() // we are in a sandbox, so no app ID added to this path
|
||||
}
|
||||
|
||||
func (p *preferences) watch() {
|
||||
// no-op on mobile
|
||||
}
|
||||
23
vendor/fyne.io/fyne/v2/app/preferences_mobile.go
generated
vendored
Normal file
23
vendor/fyne.io/fyne/v2/app/preferences_mobile.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
//go:build mobile
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2/internal/app"
|
||||
)
|
||||
|
||||
// storagePath returns the location of the settings storage
|
||||
func (p *preferences) storagePath() string {
|
||||
return filepath.Join(p.app.storageRoot(), "preferences.json")
|
||||
}
|
||||
|
||||
// storageRoot returns the location of the app storage
|
||||
func (a *fyneApp) storageRoot() string {
|
||||
return filepath.Join(app.RootConfigDir(), a.UniqueID())
|
||||
}
|
||||
|
||||
func (p *preferences) watch() {
|
||||
// no-op as we are in mobile simulation mode
|
||||
}
|
||||
67
vendor/fyne.io/fyne/v2/app/preferences_nonweb.go
generated
vendored
Normal file
67
vendor/fyne.io/fyne/v2/app/preferences_nonweb.go
generated
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
//go:build !wasm
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (p *preferences) storageWriter() (writeSyncCloser, error) {
|
||||
return p.storageWriterForPath(p.storagePath())
|
||||
}
|
||||
|
||||
func (p *preferences) storageReader() (io.ReadCloser, error) {
|
||||
return p.storageReaderForPath(p.storagePath())
|
||||
}
|
||||
|
||||
func (p *preferences) storageWriterForPath(path string) (writeSyncCloser, error) {
|
||||
err := os.MkdirAll(filepath.Dir(path), 0700)
|
||||
if err != nil { // this is not an exists error according to docs
|
||||
return nil, err
|
||||
}
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
file, err = os.Open(path) // #nosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (p *preferences) storageReaderForPath(path string) (io.ReadCloser, error) {
|
||||
file, err := os.Open(path) // #nosec
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errEmptyPreferencesStore
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// the following are only used in tests to save preferences to a tmp file
|
||||
|
||||
func (p *preferences) saveToFile(path string) error {
|
||||
file, err := p.storageWriterForPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.saveToStorage(file)
|
||||
}
|
||||
|
||||
func (p *preferences) loadFromFile(path string) error {
|
||||
file, err := p.storageReaderForPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.loadFromStorage(file)
|
||||
}
|
||||
32
vendor/fyne.io/fyne/v2/app/preferences_other.go
generated
vendored
Normal file
32
vendor/fyne.io/fyne/v2/app/preferences_other.go
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
//go:build !ios && !android && !mobile && !wasm
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2/internal/app"
|
||||
)
|
||||
|
||||
// storagePath returns the location of the settings storage
|
||||
func (p *preferences) storagePath() string {
|
||||
return filepath.Join(p.app.storageRoot(), "preferences.json")
|
||||
}
|
||||
|
||||
// storageRoot returns the location of the app storage
|
||||
func (a *fyneApp) storageRoot() string {
|
||||
return filepath.Join(app.RootConfigDir(), a.UniqueID())
|
||||
}
|
||||
|
||||
func (p *preferences) watch() {
|
||||
watchFile(p.storagePath(), func() {
|
||||
p.prefLock.RLock()
|
||||
shouldIgnoreChange := p.savedRecently
|
||||
p.prefLock.RUnlock()
|
||||
if shouldIgnoreChange {
|
||||
return
|
||||
}
|
||||
|
||||
p.load()
|
||||
})
|
||||
}
|
||||
62
vendor/fyne.io/fyne/v2/app/preferences_wasm.go
generated
vendored
Normal file
62
vendor/fyne.io/fyne/v2/app/preferences_wasm.go
generated
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
//go:build wasm
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
const preferencesLocalStorageKey = "fyne-preferences.json"
|
||||
|
||||
func (a *fyneApp) storageRoot() string {
|
||||
return "idbfile:///fyne/"
|
||||
}
|
||||
|
||||
func (p *preferences) storageReader() (io.ReadCloser, error) {
|
||||
key := js.ValueOf(preferencesLocalStorageKey)
|
||||
data := js.Global().Get("localStorage").Call("getItem", key)
|
||||
if data.IsNull() || data.IsUndefined() {
|
||||
return nil, errEmptyPreferencesStore
|
||||
}
|
||||
|
||||
return readerNopCloser{reader: strings.NewReader(data.String())}, nil
|
||||
}
|
||||
|
||||
func (p *preferences) storageWriter() (writeSyncCloser, error) {
|
||||
return &localStorageWriter{key: preferencesLocalStorageKey}, nil
|
||||
}
|
||||
|
||||
func (p *preferences) watch() {
|
||||
// no-op for web driver
|
||||
}
|
||||
|
||||
type readerNopCloser struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r readerNopCloser) Read(b []byte) (int, error) {
|
||||
return r.reader.Read(b)
|
||||
}
|
||||
|
||||
func (r readerNopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type localStorageWriter struct {
|
||||
bytes.Buffer
|
||||
key string
|
||||
}
|
||||
|
||||
func (s *localStorageWriter) Sync() error {
|
||||
text := s.String()
|
||||
s.Reset()
|
||||
js.Global().Get("localStorage").Call("setItem", js.ValueOf(s.key), js.ValueOf(text))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *localStorageWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
176
vendor/fyne.io/fyne/v2/app/settings.go
generated
vendored
Normal file
176
vendor/fyne.io/fyne/v2/app/settings.go
generated
vendored
Normal file
@ -0,0 +1,176 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/app"
|
||||
"fyne.io/fyne/v2/internal/async"
|
||||
"fyne.io/fyne/v2/internal/build"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
)
|
||||
|
||||
// SettingsSchema is used for loading and storing global settings
|
||||
type SettingsSchema struct {
|
||||
// these items are used for global settings load
|
||||
ThemeName string `json:"theme"`
|
||||
Scale float32 `json:"scale"`
|
||||
PrimaryColor string `json:"primary_color"`
|
||||
CloudName string `json:"cloud_name"`
|
||||
CloudConfig string `json:"cloud_config"`
|
||||
DisableAnimations bool `json:"no_animations"`
|
||||
}
|
||||
|
||||
// StoragePath returns the location of the settings storage
|
||||
func (sc *SettingsSchema) StoragePath() string {
|
||||
return filepath.Join(app.RootConfigDir(), "settings.json")
|
||||
}
|
||||
|
||||
// Declare conformity with Settings interface
|
||||
var _ fyne.Settings = (*settings)(nil)
|
||||
|
||||
type settings struct {
|
||||
theme fyne.Theme
|
||||
themeSpecified bool
|
||||
variant fyne.ThemeVariant
|
||||
|
||||
listeners []func(fyne.Settings)
|
||||
changeListeners async.Map[chan fyne.Settings, bool]
|
||||
watcher any // normally *fsnotify.Watcher or nil - avoid import in this file
|
||||
|
||||
schema SettingsSchema
|
||||
}
|
||||
|
||||
func (s *settings) BuildType() fyne.BuildType {
|
||||
return build.Mode
|
||||
}
|
||||
|
||||
func (s *settings) PrimaryColor() string {
|
||||
return s.schema.PrimaryColor
|
||||
}
|
||||
|
||||
// OverrideTheme allows the settings app to temporarily preview different theme details.
|
||||
// Please make sure that you remember the original settings and call this again to revert the change.
|
||||
//
|
||||
// Deprecated: Use container.NewThemeOverride to change the appearance of part of your application.
|
||||
func (s *settings) OverrideTheme(theme fyne.Theme, name string) {
|
||||
s.schema.PrimaryColor = name
|
||||
s.theme = theme
|
||||
}
|
||||
|
||||
func (s *settings) Theme() fyne.Theme {
|
||||
if s == nil {
|
||||
fyne.LogError("Attempt to access current Fyne theme when no app is started", nil)
|
||||
return nil
|
||||
}
|
||||
return s.theme
|
||||
}
|
||||
|
||||
func (s *settings) SetTheme(theme fyne.Theme) {
|
||||
s.themeSpecified = true
|
||||
s.applyTheme(theme, s.variant)
|
||||
}
|
||||
|
||||
func (s *settings) ShowAnimations() bool {
|
||||
return !s.schema.DisableAnimations && !build.NoAnimations
|
||||
}
|
||||
|
||||
func (s *settings) ThemeVariant() fyne.ThemeVariant {
|
||||
return s.variant
|
||||
}
|
||||
|
||||
func (s *settings) applyTheme(theme fyne.Theme, variant fyne.ThemeVariant) {
|
||||
s.variant = variant
|
||||
s.theme = theme
|
||||
s.apply()
|
||||
}
|
||||
|
||||
func (s *settings) applyVariant(variant fyne.ThemeVariant) {
|
||||
s.variant = variant
|
||||
s.apply()
|
||||
}
|
||||
|
||||
func (s *settings) Scale() float32 {
|
||||
if s.schema.Scale < 0.0 {
|
||||
return 1.0 // catching any really old data still using the `-1` value for "auto" scale
|
||||
}
|
||||
return s.schema.Scale
|
||||
}
|
||||
|
||||
func (s *settings) AddChangeListener(listener chan fyne.Settings) {
|
||||
s.changeListeners.Store(listener, true) // the boolean is just a dummy value here.
|
||||
}
|
||||
|
||||
func (s *settings) AddListener(listener func(fyne.Settings)) {
|
||||
s.listeners = append(s.listeners, listener)
|
||||
}
|
||||
|
||||
func (s *settings) apply() {
|
||||
s.changeListeners.Range(func(listener chan fyne.Settings, _ bool) bool {
|
||||
select {
|
||||
case listener <- s:
|
||||
default:
|
||||
l := listener
|
||||
go func() { l <- s }()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, l := range s.listeners {
|
||||
l(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *settings) fileChanged() {
|
||||
s.load()
|
||||
s.apply()
|
||||
}
|
||||
|
||||
func (s *settings) loadSystemTheme() fyne.Theme {
|
||||
path := filepath.Join(app.RootConfigDir(), "theme.json")
|
||||
data, err := fyne.LoadResourceFromPath(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
fyne.LogError("Failed to load user theme file: "+path, err)
|
||||
}
|
||||
return theme.DefaultTheme()
|
||||
}
|
||||
if data != nil && data.Content() != nil {
|
||||
th, err := theme.FromJSONReader(bytes.NewReader(data.Content()))
|
||||
if err == nil {
|
||||
return th
|
||||
}
|
||||
fyne.LogError("Failed to parse user theme file: "+path, err)
|
||||
}
|
||||
return theme.DefaultTheme()
|
||||
}
|
||||
|
||||
func (s *settings) setupTheme() {
|
||||
name := s.schema.ThemeName
|
||||
if env := os.Getenv("FYNE_THEME"); env != "" {
|
||||
name = env
|
||||
}
|
||||
|
||||
variant := app.DefaultVariant()
|
||||
effectiveTheme := s.theme
|
||||
if !s.themeSpecified {
|
||||
effectiveTheme = s.loadSystemTheme()
|
||||
}
|
||||
switch name {
|
||||
case "light":
|
||||
variant = theme.VariantLight
|
||||
case "dark":
|
||||
variant = theme.VariantDark
|
||||
}
|
||||
|
||||
s.applyTheme(effectiveTheme, variant)
|
||||
}
|
||||
|
||||
func loadSettings() *settings {
|
||||
s := &settings{}
|
||||
s.load()
|
||||
|
||||
return s
|
||||
}
|
||||
80
vendor/fyne.io/fyne/v2/app/settings_desktop.go
generated
vendored
Normal file
80
vendor/fyne.io/fyne/v2/app/settings_desktop.go
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
//go:build !android && !ios && !mobile && !wasm && !test_web_driver
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func watchFileAddTarget(watcher *fsnotify.Watcher, path string) {
|
||||
dir := filepath.Dir(path)
|
||||
ensureDirExists(dir)
|
||||
|
||||
err := watcher.Add(dir)
|
||||
if err != nil {
|
||||
fyne.LogError("Settings watch error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureDirExists(dir string) {
|
||||
if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
fyne.LogError("Unable to create settings storage:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func watchFile(path string, callback func()) *fsnotify.Watcher {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to watch settings file:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
for event := range watcher.Events {
|
||||
if event.Op.Has(fsnotify.Remove) { // if it was deleted then watch again
|
||||
watcher.Remove(path) // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
|
||||
|
||||
watchFileAddTarget(watcher, path)
|
||||
} else {
|
||||
fyne.Do(callback)
|
||||
}
|
||||
}
|
||||
|
||||
err = watcher.Close()
|
||||
if err != nil {
|
||||
fyne.LogError("Settings un-watch error:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
watchFileAddTarget(watcher, path)
|
||||
return watcher
|
||||
}
|
||||
|
||||
func (s *settings) watchSettings() {
|
||||
if s.themeSpecified {
|
||||
return // we only watch for theme changes at this time so don't bother
|
||||
}
|
||||
s.watcher = watchFile(s.schema.StoragePath(), s.fileChanged)
|
||||
|
||||
a := fyne.CurrentApp()
|
||||
if a != nil && s != nil && a.Settings() == s { // ignore if testing
|
||||
watchTheme(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *settings) stopWatching() {
|
||||
if s.watcher == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.watcher.(*fsnotify.Watcher).Close() // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
|
||||
}
|
||||
34
vendor/fyne.io/fyne/v2/app/settings_file.go
generated
vendored
Normal file
34
vendor/fyne.io/fyne/v2/app/settings_file.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
//go:build !wasm && !test_web_driver
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
func (s *settings) load() {
|
||||
err := s.loadFromFile(s.schema.StoragePath())
|
||||
if err != nil && err != io.EOF { // we can get an EOF in windows settings writes
|
||||
fyne.LogError("Settings load error:", err)
|
||||
}
|
||||
|
||||
s.setupTheme()
|
||||
}
|
||||
|
||||
func (s *settings) loadFromFile(path string) error {
|
||||
file, err := os.Open(path) // #nosec
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
decode := json.NewDecoder(file)
|
||||
|
||||
return decode.Decode(&s.schema)
|
||||
}
|
||||
11
vendor/fyne.io/fyne/v2/app/settings_mobile.go
generated
vendored
Normal file
11
vendor/fyne.io/fyne/v2/app/settings_mobile.go
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build android || ios || mobile
|
||||
|
||||
package app
|
||||
|
||||
func (s *settings) watchSettings() {
|
||||
// no-op on mobile
|
||||
}
|
||||
|
||||
func (s *settings) stopWatching() {
|
||||
// no-op on mobile
|
||||
}
|
||||
25
vendor/fyne.io/fyne/v2/app/settings_wasm.go
generated
vendored
Normal file
25
vendor/fyne.io/fyne/v2/app/settings_wasm.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
//go:build wasm || test_web_driver
|
||||
|
||||
package app
|
||||
|
||||
// TODO: #2734
|
||||
|
||||
func (s *settings) load() {
|
||||
s.setupTheme()
|
||||
s.schema.Scale = 1
|
||||
}
|
||||
|
||||
func (s *settings) loadFromFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func watchFile(path string, callback func()) {
|
||||
}
|
||||
|
||||
func (s *settings) watchSettings() {
|
||||
watchTheme(s)
|
||||
}
|
||||
|
||||
func (s *settings) stopWatching() {
|
||||
stopWatchingTheme()
|
||||
}
|
||||
31
vendor/fyne.io/fyne/v2/app/storage.go
generated
vendored
Normal file
31
vendor/fyne.io/fyne/v2/app/storage.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
*internal.Docs
|
||||
a *fyneApp
|
||||
}
|
||||
|
||||
func (s *store) RootURI() fyne.URI {
|
||||
if s.a.UniqueID() == "" {
|
||||
fyne.LogError("Storage API requires a unique ID, use app.NewWithID()", nil)
|
||||
return storage.NewFileURI(os.TempDir())
|
||||
}
|
||||
|
||||
u, err := storage.ParseURI(s.a.storageRoot())
|
||||
if err == nil {
|
||||
return u
|
||||
}
|
||||
return storage.NewFileURI(s.a.storageRoot())
|
||||
}
|
||||
|
||||
func (s *store) docRootURI() (fyne.URI, error) {
|
||||
return storage.Child(s.RootURI(), "Documents")
|
||||
}
|
||||
58
vendor/fyne.io/fyne/v2/canvas.go
generated
vendored
Normal file
58
vendor/fyne.io/fyne/v2/canvas.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
package fyne
|
||||
|
||||
import "image"
|
||||
|
||||
// Canvas defines a graphical canvas to which a [CanvasObject] or Container can be added.
|
||||
// Each canvas has a scale which is automatically applied during the render process.
|
||||
type Canvas interface {
|
||||
Content() CanvasObject
|
||||
SetContent(CanvasObject)
|
||||
|
||||
Refresh(CanvasObject)
|
||||
|
||||
// Focus makes the provided item focused.
|
||||
// The item has to be added to the contents of the canvas before calling this.
|
||||
Focus(Focusable)
|
||||
// FocusNext focuses the next focusable item.
|
||||
// If no item is currently focused, the first focusable item is focused.
|
||||
// If the last focusable item is currently focused, the first focusable item is focused.
|
||||
//
|
||||
// Since: 2.0
|
||||
FocusNext()
|
||||
// FocusPrevious focuses the previous focusable item.
|
||||
// If no item is currently focused, the last focusable item is focused.
|
||||
// If the first focusable item is currently focused, the last focusable item is focused.
|
||||
//
|
||||
// Since: 2.0
|
||||
FocusPrevious()
|
||||
Unfocus()
|
||||
Focused() Focusable
|
||||
|
||||
// Size returns the current size of this canvas
|
||||
Size() Size
|
||||
// Scale returns the current scale (multiplication factor) this canvas uses to render
|
||||
// The pixel size of a [CanvasObject] can be found by multiplying by this value.
|
||||
Scale() float32
|
||||
|
||||
// Overlays returns the overlay stack.
|
||||
Overlays() OverlayStack
|
||||
|
||||
OnTypedRune() func(rune)
|
||||
SetOnTypedRune(func(rune))
|
||||
OnTypedKey() func(*KeyEvent)
|
||||
SetOnTypedKey(func(*KeyEvent))
|
||||
AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut))
|
||||
RemoveShortcut(shortcut Shortcut)
|
||||
|
||||
Capture() image.Image
|
||||
|
||||
// PixelCoordinateForPosition returns the x and y pixel coordinate for a given position on this canvas.
|
||||
// This can be used to find absolute pixel positions or pixel offsets relative to an object top left.
|
||||
PixelCoordinateForPosition(Position) (int, int)
|
||||
|
||||
// InteractiveArea returns the position and size of the central interactive area.
|
||||
// Operating system elements may overlap the portions outside this area and widgets should avoid being outside.
|
||||
//
|
||||
// Since: 1.4
|
||||
InteractiveArea() (Position, Size)
|
||||
}
|
||||
86
vendor/fyne.io/fyne/v2/canvas/animation.go
generated
vendored
Normal file
86
vendor/fyne.io/fyne/v2/canvas/animation.go
generated
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// DurationStandard is the time a standard interface animation will run.
|
||||
//
|
||||
// Since: 2.0
|
||||
DurationStandard = time.Millisecond * 300
|
||||
// DurationShort is the time a subtle or small transition should use.
|
||||
//
|
||||
// Since: 2.0
|
||||
DurationShort = time.Millisecond * 150
|
||||
)
|
||||
|
||||
// NewColorRGBAAnimation sets up a new animation that will transition from the start to stop Color over
|
||||
// the specified Duration. The colour transition will move linearly through the RGB colour space.
|
||||
// The content of fn should apply the color values to an object and refresh it.
|
||||
// You should call Start() on the returned animation to start it.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewColorRGBAAnimation(start, stop color.Color, d time.Duration, fn func(color.Color)) *fyne.Animation {
|
||||
r1, g1, b1, a1 := start.RGBA()
|
||||
r2, g2, b2, a2 := stop.RGBA()
|
||||
|
||||
rStart := int(r1 >> 8)
|
||||
gStart := int(g1 >> 8)
|
||||
bStart := int(b1 >> 8)
|
||||
aStart := int(a1 >> 8)
|
||||
rDelta := float32(int(r2>>8) - rStart)
|
||||
gDelta := float32(int(g2>>8) - gStart)
|
||||
bDelta := float32(int(b2>>8) - bStart)
|
||||
aDelta := float32(int(a2>>8) - aStart)
|
||||
|
||||
return &fyne.Animation{
|
||||
Duration: d,
|
||||
Tick: func(done float32) {
|
||||
fn(color.RGBA{R: scaleChannel(rStart, rDelta, done), G: scaleChannel(gStart, gDelta, done),
|
||||
B: scaleChannel(bStart, bDelta, done), A: scaleChannel(aStart, aDelta, done)})
|
||||
}}
|
||||
}
|
||||
|
||||
// NewPositionAnimation sets up a new animation that will transition from the start to stop Position over
|
||||
// the specified Duration. The content of fn should apply the position value to an object for the change
|
||||
// to be visible. You should call Start() on the returned animation to start it.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewPositionAnimation(start, stop fyne.Position, d time.Duration, fn func(fyne.Position)) *fyne.Animation {
|
||||
xDelta := float32(stop.X - start.X)
|
||||
yDelta := float32(stop.Y - start.Y)
|
||||
|
||||
return &fyne.Animation{
|
||||
Duration: d,
|
||||
Tick: func(done float32) {
|
||||
fn(fyne.NewPos(scaleVal(start.X, xDelta, done), scaleVal(start.Y, yDelta, done)))
|
||||
}}
|
||||
}
|
||||
|
||||
// NewSizeAnimation sets up a new animation that will transition from the start to stop Size over
|
||||
// the specified Duration. The content of fn should apply the size value to an object for the change
|
||||
// to be visible. You should call Start() on the returned animation to start it.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewSizeAnimation(start, stop fyne.Size, d time.Duration, fn func(fyne.Size)) *fyne.Animation {
|
||||
widthDelta := float32(stop.Width - start.Width)
|
||||
heightDelta := float32(stop.Height - start.Height)
|
||||
|
||||
return &fyne.Animation{
|
||||
Duration: d,
|
||||
Tick: func(done float32) {
|
||||
fn(fyne.NewSize(scaleVal(start.Width, widthDelta, done), scaleVal(start.Height, heightDelta, done)))
|
||||
}}
|
||||
}
|
||||
|
||||
func scaleChannel(start int, diff, done float32) uint8 {
|
||||
return uint8(start + int(diff*done))
|
||||
}
|
||||
|
||||
func scaleVal(start float32, delta, done float32) float32 {
|
||||
return start + delta*done
|
||||
}
|
||||
69
vendor/fyne.io/fyne/v2/canvas/base.go
generated
vendored
Normal file
69
vendor/fyne.io/fyne/v2/canvas/base.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
// Package canvas contains all of the primitive CanvasObjects that make up a Fyne GUI.
|
||||
//
|
||||
// The types implemented in this package are used as building blocks in order
|
||||
// to build higher order functionality. These types are designed to be
|
||||
// non-interactive, by design. If additional functionality is required,
|
||||
// it's usually a sign that this type should be used as part of a custom
|
||||
// widget.
|
||||
package canvas // import "fyne.io/fyne/v2/canvas"
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
type baseObject struct {
|
||||
size fyne.Size // The current size of the canvas object
|
||||
position fyne.Position // The current position of the object
|
||||
Hidden bool // Is this object currently hidden
|
||||
|
||||
min fyne.Size // The minimum size this object can be
|
||||
}
|
||||
|
||||
// Hide will set this object to not be visible.
|
||||
func (o *baseObject) Hide() {
|
||||
o.Hidden = true
|
||||
}
|
||||
|
||||
// MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
|
||||
func (o *baseObject) MinSize() fyne.Size {
|
||||
if o.min.IsZero() {
|
||||
return fyne.Size{Width: 1, Height: 1}
|
||||
}
|
||||
|
||||
return o.min
|
||||
}
|
||||
|
||||
// Move the object to a new position, relative to its parent.
|
||||
func (o *baseObject) Move(pos fyne.Position) {
|
||||
o.position = pos
|
||||
}
|
||||
|
||||
// Position gets the current position of this canvas object, relative to its parent.
|
||||
func (o *baseObject) Position() fyne.Position {
|
||||
return o.position
|
||||
}
|
||||
|
||||
// Resize sets a new size for the canvas object.
|
||||
func (o *baseObject) Resize(size fyne.Size) {
|
||||
o.size = size
|
||||
}
|
||||
|
||||
// SetMinSize specifies the smallest size this object should be.
|
||||
func (o *baseObject) SetMinSize(size fyne.Size) {
|
||||
o.min = size
|
||||
}
|
||||
|
||||
// Show will set this object to be visible.
|
||||
func (o *baseObject) Show() {
|
||||
o.Hidden = false
|
||||
}
|
||||
|
||||
// Size returns the current size of this canvas object.
|
||||
func (o *baseObject) Size() fyne.Size {
|
||||
return o.size
|
||||
}
|
||||
|
||||
// Visible returns true if this object is visible, false otherwise.
|
||||
func (o *baseObject) Visible() bool {
|
||||
return !o.Hidden
|
||||
}
|
||||
49
vendor/fyne.io/fyne/v2/canvas/canvas.go
generated
vendored
Normal file
49
vendor/fyne.io/fyne/v2/canvas/canvas.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/svg"
|
||||
)
|
||||
|
||||
// Refresh instructs the containing canvas to refresh the specified obj.
|
||||
func Refresh(obj fyne.CanvasObject) {
|
||||
app := fyne.CurrentApp()
|
||||
if app == nil || app.Driver() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := app.Driver().CanvasForObject(obj)
|
||||
if c != nil {
|
||||
c.Refresh(obj)
|
||||
}
|
||||
}
|
||||
|
||||
// RecolorSVG takes a []byte containing SVG content, and returns
|
||||
// new SVG content, re-colorized to be monochrome with the given color.
|
||||
// The content can be assigned to a new fyne.StaticResource with an appropriate name
|
||||
// to be used in a widget.Button, canvas.Image, etc.
|
||||
//
|
||||
// If an error occurs, the returned content will be the original un-modified content,
|
||||
// and a non-nil error is returned.
|
||||
//
|
||||
// Since: 2.6
|
||||
func RecolorSVG(svgContent []byte, color color.Color) ([]byte, error) {
|
||||
return svg.Colorize(svgContent, color)
|
||||
}
|
||||
|
||||
// repaint instructs the containing canvas to redraw, even if nothing changed.
|
||||
func repaint(obj fyne.CanvasObject) {
|
||||
app := fyne.CurrentApp()
|
||||
if app == nil || app.Driver() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := app.Driver().CanvasForObject(obj)
|
||||
if c != nil {
|
||||
if paint, ok := c.(interface{ SetDirty() }); ok {
|
||||
paint.SetDirty()
|
||||
}
|
||||
}
|
||||
}
|
||||
95
vendor/fyne.io/fyne/v2/canvas/circle.go
generated
vendored
Normal file
95
vendor/fyne.io/fyne/v2/canvas/circle.go
generated
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Circle)(nil)
|
||||
|
||||
// Circle describes a colored circle primitive in a Fyne canvas
|
||||
type Circle struct {
|
||||
Position1 fyne.Position // The current top-left position of the Circle
|
||||
Position2 fyne.Position // The current bottomright position of the Circle
|
||||
Hidden bool // Is this circle currently hidden
|
||||
|
||||
FillColor color.Color // The circle fill color
|
||||
StrokeColor color.Color // The circle stroke color
|
||||
StrokeWidth float32 // The stroke width of the circle
|
||||
}
|
||||
|
||||
// NewCircle returns a new Circle instance
|
||||
func NewCircle(color color.Color) *Circle {
|
||||
return &Circle{FillColor: color}
|
||||
}
|
||||
|
||||
// Hide will set this circle to not be visible
|
||||
func (c *Circle) Hide() {
|
||||
c.Hidden = true
|
||||
|
||||
repaint(c)
|
||||
}
|
||||
|
||||
// MinSize for a Circle simply returns Size{1, 1} as there is no
|
||||
// explicit content
|
||||
func (c *Circle) MinSize() fyne.Size {
|
||||
return fyne.NewSize(1, 1)
|
||||
}
|
||||
|
||||
// Move the circle object to a new position, relative to its parent / canvas
|
||||
func (c *Circle) Move(pos fyne.Position) {
|
||||
if c.Position1 == pos {
|
||||
return
|
||||
}
|
||||
|
||||
size := c.Size()
|
||||
c.Position1 = pos
|
||||
c.Position2 = c.Position1.Add(size)
|
||||
|
||||
repaint(c)
|
||||
}
|
||||
|
||||
// Position gets the current top-left position of this circle object, relative to its parent / canvas
|
||||
func (c *Circle) Position() fyne.Position {
|
||||
return c.Position1
|
||||
}
|
||||
|
||||
// Refresh causes this object to be redrawn with its configured state.
|
||||
func (c *Circle) Refresh() {
|
||||
Refresh(c)
|
||||
}
|
||||
|
||||
// Resize sets a new bottom-right position for the circle object
|
||||
// If it has a stroke width this will cause it to Refresh.
|
||||
func (c *Circle) Resize(size fyne.Size) {
|
||||
if size == c.Size() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Position2 = c.Position1.Add(size)
|
||||
|
||||
Refresh(c)
|
||||
}
|
||||
|
||||
// Show will set this circle to be visible
|
||||
func (c *Circle) Show() {
|
||||
c.Hidden = false
|
||||
|
||||
c.Refresh()
|
||||
}
|
||||
|
||||
// Size returns the current size of bounding box for this circle object
|
||||
func (c *Circle) Size() fyne.Size {
|
||||
return fyne.NewSize(
|
||||
float32(math.Abs(float64(c.Position2.X)-float64(c.Position1.X))),
|
||||
float32(math.Abs(float64(c.Position2.Y)-float64(c.Position1.Y))),
|
||||
)
|
||||
}
|
||||
|
||||
// Visible returns true if this circle is visible, false otherwise
|
||||
func (c *Circle) Visible() bool {
|
||||
return !c.Hidden
|
||||
}
|
||||
238
vendor/fyne.io/fyne/v2/canvas/gradient.go
generated
vendored
Normal file
238
vendor/fyne.io/fyne/v2/canvas/gradient.go
generated
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// LinearGradient defines a Gradient travelling straight at a given angle.
|
||||
// The only supported values for the angle are `0.0` (vertical) and `90.0` (horizontal), currently.
|
||||
type LinearGradient struct {
|
||||
baseObject
|
||||
|
||||
StartColor color.Color // The beginning color of the gradient
|
||||
EndColor color.Color // The end color of the gradient
|
||||
Angle float64 // The angle of the gradient (0/180 for vertical; 90/270 for horizontal)
|
||||
}
|
||||
|
||||
// Generate calculates an image of the gradient with the specified width and height.
|
||||
func (g *LinearGradient) Generate(iw, ih int) image.Image {
|
||||
w, h := float64(iw), float64(ih)
|
||||
var generator func(x, y float64) float64
|
||||
switch g.Angle {
|
||||
case 90: // horizontal flipped
|
||||
generator = func(x, _ float64) float64 {
|
||||
return (w - x) / w
|
||||
}
|
||||
case 270: // horizontal
|
||||
generator = func(x, _ float64) float64 {
|
||||
return x / w
|
||||
}
|
||||
case 45: // diagonal negative flipped
|
||||
generator = func(x, y float64) float64 {
|
||||
return math.Abs((w - x + y) / (w + h)) // ((w+h)-(x+h-y)) / (w+h)
|
||||
}
|
||||
case 225: // diagonal negative
|
||||
generator = func(x, y float64) float64 {
|
||||
return math.Abs((x + h - y) / (w + h))
|
||||
}
|
||||
case 135: // diagonal positive flipped
|
||||
generator = func(x, y float64) float64 {
|
||||
return math.Abs((w + h - (x + y)) / (w + h))
|
||||
}
|
||||
case 315: // diagonal positive
|
||||
generator = func(x, y float64) float64 {
|
||||
return math.Abs((x + y) / (w + h))
|
||||
}
|
||||
case 180: // vertical flipped
|
||||
generator = func(_, y float64) float64 {
|
||||
return (h - y) / h
|
||||
}
|
||||
default: // vertical
|
||||
generator = func(_, y float64) float64 {
|
||||
return y / h
|
||||
}
|
||||
}
|
||||
return computeGradient(generator, iw, ih, g.StartColor, g.EndColor)
|
||||
}
|
||||
|
||||
// Hide will set this gradient to not be visible
|
||||
func (g *LinearGradient) Hide() {
|
||||
g.baseObject.Hide()
|
||||
|
||||
repaint(g)
|
||||
}
|
||||
|
||||
// Move the gradient to a new position, relative to its parent / canvas
|
||||
func (g *LinearGradient) Move(pos fyne.Position) {
|
||||
if g.Position() == pos {
|
||||
return
|
||||
}
|
||||
|
||||
g.baseObject.Move(pos)
|
||||
|
||||
repaint(g)
|
||||
}
|
||||
|
||||
// Resize resizes the gradient to a new size.
|
||||
func (g *LinearGradient) Resize(size fyne.Size) {
|
||||
if size == g.Size() {
|
||||
return
|
||||
}
|
||||
g.baseObject.Resize(size)
|
||||
|
||||
// refresh needed to invalidate cached textures
|
||||
g.Refresh()
|
||||
}
|
||||
|
||||
// Refresh causes this gradient to be redrawn with its configured state.
|
||||
func (g *LinearGradient) Refresh() {
|
||||
Refresh(g)
|
||||
}
|
||||
|
||||
// RadialGradient defines a Gradient travelling radially from a center point outward.
|
||||
type RadialGradient struct {
|
||||
baseObject
|
||||
|
||||
StartColor color.Color // The beginning color of the gradient
|
||||
EndColor color.Color // The end color of the gradient
|
||||
// The offset of the center for generation of the gradient.
|
||||
// This is not a DP measure but relates to the width/height.
|
||||
// A value of 0.5 would move the center by the half width/height.
|
||||
CenterOffsetX, CenterOffsetY float64
|
||||
}
|
||||
|
||||
// Generate calculates an image of the gradient with the specified width and height.
|
||||
func (g *RadialGradient) Generate(iw, ih int) image.Image {
|
||||
w, h := float64(iw), float64(ih)
|
||||
// define center plus offset
|
||||
centerX := w/2 + w*g.CenterOffsetX
|
||||
centerY := h/2 + h*g.CenterOffsetY
|
||||
|
||||
// handle negative offsets
|
||||
var a, b float64
|
||||
if g.CenterOffsetX < 0 {
|
||||
a = w - centerX
|
||||
} else {
|
||||
a = centerX
|
||||
}
|
||||
if g.CenterOffsetY < 0 {
|
||||
b = h - centerY
|
||||
} else {
|
||||
b = centerY
|
||||
}
|
||||
|
||||
generator := func(x, y float64) float64 {
|
||||
// calculate distance from center for gradient multiplier
|
||||
dx, dy := centerX-x, centerY-y
|
||||
da := math.Sqrt(dx*dx + dy*dy*a*a/b/b)
|
||||
if da > a {
|
||||
return 1
|
||||
}
|
||||
return da / a
|
||||
}
|
||||
return computeGradient(generator, iw, ih, g.StartColor, g.EndColor)
|
||||
}
|
||||
|
||||
// Hide will set this gradient to not be visible
|
||||
func (g *RadialGradient) Hide() {
|
||||
g.baseObject.Hide()
|
||||
|
||||
repaint(g)
|
||||
}
|
||||
|
||||
// Move the gradient to a new position, relative to its parent / canvas
|
||||
func (g *RadialGradient) Move(pos fyne.Position) {
|
||||
g.baseObject.Move(pos)
|
||||
|
||||
repaint(g)
|
||||
}
|
||||
|
||||
// Resize resizes the gradient to a new size.
|
||||
func (g *RadialGradient) Resize(size fyne.Size) {
|
||||
if size == g.Size() {
|
||||
return
|
||||
}
|
||||
g.baseObject.Resize(size)
|
||||
|
||||
// refresh needed to invalidate cached textures
|
||||
g.Refresh()
|
||||
}
|
||||
|
||||
// Refresh causes this gradient to be redrawn with its configured state.
|
||||
func (g *RadialGradient) Refresh() {
|
||||
Refresh(g)
|
||||
}
|
||||
|
||||
func calculatePixel(d float64, startColor, endColor color.Color) color.Color {
|
||||
// fetch RGBA values
|
||||
aR, aG, aB, aA := startColor.RGBA()
|
||||
bR, bG, bB, bA := endColor.RGBA()
|
||||
|
||||
// Get difference
|
||||
dR := float64(bR) - float64(aR)
|
||||
dG := float64(bG) - float64(aG)
|
||||
dB := float64(bB) - float64(aB)
|
||||
dA := float64(bA) - float64(aA)
|
||||
|
||||
// Apply gradations
|
||||
pixel := &color.RGBA64{
|
||||
R: uint16(float64(aR) + d*dR),
|
||||
B: uint16(float64(aB) + d*dB),
|
||||
G: uint16(float64(aG) + d*dG),
|
||||
A: uint16(float64(aA) + d*dA),
|
||||
}
|
||||
|
||||
return pixel
|
||||
}
|
||||
|
||||
func computeGradient(generator func(x, y float64) float64, w, h int, startColor, endColor color.Color) image.Image {
|
||||
img := image.NewNRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
if startColor == nil && endColor == nil {
|
||||
return img
|
||||
} else if startColor == nil {
|
||||
startColor = color.Transparent
|
||||
} else if endColor == nil {
|
||||
endColor = color.Transparent
|
||||
}
|
||||
|
||||
for x := 0; x < w; x++ {
|
||||
for y := 0; y < h; y++ {
|
||||
distance := generator(float64(x)+0.5, float64(y)+0.5)
|
||||
img.Set(x, y, calculatePixel(distance, startColor, endColor))
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// NewHorizontalGradient creates a new horizontally travelling linear gradient.
|
||||
// The start color will be at the left of the gradient and the end color will be at the right.
|
||||
func NewHorizontalGradient(start, end color.Color) *LinearGradient {
|
||||
g := &LinearGradient{StartColor: start, EndColor: end}
|
||||
g.Angle = 270
|
||||
return g
|
||||
}
|
||||
|
||||
// NewLinearGradient creates a linear gradient at the specified angle.
|
||||
// The angle parameter is the degree angle along which the gradient is calculated.
|
||||
// A NewHorizontalGradient uses 270 degrees and NewVerticalGradient is 0 degrees.
|
||||
func NewLinearGradient(start, end color.Color, angle float64) *LinearGradient {
|
||||
g := &LinearGradient{StartColor: start, EndColor: end}
|
||||
g.Angle = angle
|
||||
return g
|
||||
}
|
||||
|
||||
// NewRadialGradient creates a new radial gradient.
|
||||
func NewRadialGradient(start, end color.Color) *RadialGradient {
|
||||
return &RadialGradient{StartColor: start, EndColor: end}
|
||||
}
|
||||
|
||||
// NewVerticalGradient creates a new vertically travelling linear gradient.
|
||||
// The start color will be at the top of the gradient and the end color will be at the bottom.
|
||||
func NewVerticalGradient(start color.Color, end color.Color) *LinearGradient {
|
||||
return &LinearGradient{StartColor: start, EndColor: end}
|
||||
}
|
||||
388
vendor/fyne.io/fyne/v2/canvas/image.go
generated
vendored
Normal file
388
vendor/fyne.io/fyne/v2/canvas/image.go
generated
vendored
Normal file
@ -0,0 +1,388 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"image"
|
||||
_ "image/jpeg" // avoid users having to import when using image widget
|
||||
_ "image/png" // avoid the same for PNG images
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/cache"
|
||||
"fyne.io/fyne/v2/internal/scale"
|
||||
"fyne.io/fyne/v2/internal/svg"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
// ImageFill defines the different type of ways an image can stretch to fill its space.
|
||||
type ImageFill int
|
||||
|
||||
const (
|
||||
// ImageFillStretch will scale the image to match the Size() values.
|
||||
// This is the default and does not maintain aspect ratio.
|
||||
ImageFillStretch ImageFill = iota
|
||||
// ImageFillContain makes the image fit within the object Size(),
|
||||
// centrally and maintaining aspect ratio.
|
||||
// There may be transparent sections top and bottom or left and right.
|
||||
ImageFillContain // (Fit)
|
||||
// ImageFillOriginal ensures that the container grows to the pixel dimensions
|
||||
// required to fit the original image. The aspect of the image will be maintained so,
|
||||
// as with ImageFillContain there may be transparent areas around the image.
|
||||
// Note that the minSize may be smaller than the image dimensions if scale > 1.
|
||||
ImageFillOriginal
|
||||
)
|
||||
|
||||
// ImageScale defines the different scaling filters used to scaling images
|
||||
type ImageScale int32
|
||||
|
||||
const (
|
||||
// ImageScaleSmooth will scale the image using ApproxBiLinear filter (or GL equivalent)
|
||||
ImageScaleSmooth ImageScale = iota
|
||||
// ImageScalePixels will scale the image using NearestNeighbor filter (or GL equivalent)
|
||||
ImageScalePixels
|
||||
// ImageScaleFastest will scale the image using hardware GPU if available
|
||||
//
|
||||
// Since: 2.0
|
||||
ImageScaleFastest
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Image)(nil)
|
||||
|
||||
// Image describes a drawable image area that can render in a Fyne canvas
|
||||
// The image may be a vector or a bitmap representation, it will fill the area.
|
||||
// The fill mode can be changed by setting FillMode to a different ImageFill.
|
||||
type Image struct {
|
||||
baseObject
|
||||
|
||||
aspect float32
|
||||
icon *svg.Decoder
|
||||
isSVG bool
|
||||
|
||||
// one of the following sources will provide our image data
|
||||
File string // Load the image from a file
|
||||
Resource fyne.Resource // Load the image from an in-memory resource
|
||||
Image image.Image // Specify a loaded image to use in this canvas object
|
||||
|
||||
Translucency float64 // Set a translucency value > 0.0 to fade the image
|
||||
FillMode ImageFill // Specify how the image should expand to fill or fit the available space
|
||||
ScaleMode ImageScale // Specify the type of scaling interpolation applied to the image
|
||||
|
||||
previousRender bool // did we successfully draw before? if so a nil content will need a reset
|
||||
}
|
||||
|
||||
// Alpha is a convenience function that returns the alpha value for an image
|
||||
// based on its Translucency value. The result is 1.0 - Translucency.
|
||||
func (i *Image) Alpha() float64 {
|
||||
return 1.0 - i.Translucency
|
||||
}
|
||||
|
||||
// Aspect will return the original content aspect after it was last refreshed.
|
||||
//
|
||||
// Since: 2.4
|
||||
func (i *Image) Aspect() float32 {
|
||||
if i.aspect == 0 {
|
||||
i.Refresh()
|
||||
}
|
||||
return i.aspect
|
||||
}
|
||||
|
||||
// Hide will set this image to not be visible
|
||||
func (i *Image) Hide() {
|
||||
i.baseObject.Hide()
|
||||
|
||||
repaint(i)
|
||||
}
|
||||
|
||||
// MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
|
||||
func (i *Image) MinSize() fyne.Size {
|
||||
if i.Image == nil || i.aspect == 0 {
|
||||
if i.File != "" || i.Resource != nil {
|
||||
i.Refresh()
|
||||
}
|
||||
}
|
||||
return i.baseObject.MinSize()
|
||||
}
|
||||
|
||||
// Move the image object to a new position, relative to its parent top, left corner.
|
||||
func (i *Image) Move(pos fyne.Position) {
|
||||
if i.Position() == pos {
|
||||
return
|
||||
}
|
||||
|
||||
i.baseObject.Move(pos)
|
||||
|
||||
repaint(i)
|
||||
}
|
||||
|
||||
// Refresh causes this image to be redrawn with its configured state.
|
||||
func (i *Image) Refresh() {
|
||||
rc, err := i.updateReader()
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to load image", err)
|
||||
return
|
||||
}
|
||||
if rc != nil {
|
||||
rcMem := rc
|
||||
defer rcMem.Close()
|
||||
}
|
||||
|
||||
if i.File != "" || i.Resource != nil || i.Image != nil {
|
||||
r, err := i.updateAspectAndMinSize(rc)
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to load image", err)
|
||||
return
|
||||
}
|
||||
rc = io.NopCloser(r)
|
||||
} else if i.previousRender {
|
||||
i.previousRender = false
|
||||
|
||||
Refresh(i)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if i.File != "" || i.Resource != nil {
|
||||
size := i.Size()
|
||||
width := size.Width
|
||||
height := size.Height
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if i.isSVG {
|
||||
tex, err := i.renderSVG(width, height)
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to render SVG", err)
|
||||
return
|
||||
}
|
||||
i.Image = tex
|
||||
} else {
|
||||
if rc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(rc)
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to render image", err)
|
||||
return
|
||||
}
|
||||
i.Image = img
|
||||
}
|
||||
}
|
||||
|
||||
i.previousRender = true
|
||||
Refresh(i)
|
||||
}
|
||||
|
||||
// Resize on an image will scale the content or reposition it according to FillMode.
|
||||
// It will normally cause a Refresh to ensure the pixels are recalculated.
|
||||
func (i *Image) Resize(s fyne.Size) {
|
||||
if s == i.Size() {
|
||||
return
|
||||
}
|
||||
i.baseObject.Resize(s)
|
||||
if i.FillMode == ImageFillOriginal && i.Size().Height > 2 { // we can just ask for a GPU redraw to align
|
||||
Refresh(i)
|
||||
return
|
||||
}
|
||||
|
||||
i.baseObject.Resize(s)
|
||||
if i.isSVG || i.Image == nil {
|
||||
i.Refresh() // we need to rasterise at the new size
|
||||
} else {
|
||||
Refresh(i) // just re-size using GPU scaling
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageFromFile creates a new image from a local file.
|
||||
// Images returned from this method will scale to fit the canvas object.
|
||||
// The method for scaling can be set using the Fill field.
|
||||
func NewImageFromFile(file string) *Image {
|
||||
return &Image{File: file}
|
||||
}
|
||||
|
||||
// NewImageFromURI creates a new image from named resource.
|
||||
// File URIs will read the file path and other schemes will download the data into a resource.
|
||||
// HTTP and HTTPs URIs will use the GET method by default to request the resource.
|
||||
// Images returned from this method will scale to fit the canvas object.
|
||||
// The method for scaling can be set using the Fill field.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewImageFromURI(uri fyne.URI) *Image {
|
||||
if uri.Scheme() == "file" && len(uri.String()) > 7 {
|
||||
return NewImageFromFile(uri.Path())
|
||||
}
|
||||
|
||||
var read io.ReadCloser
|
||||
|
||||
read, err := storage.Reader(uri) // attempt unknown / http file type
|
||||
if err != nil {
|
||||
fyne.LogError("Failed to open image URI", err)
|
||||
return &Image{}
|
||||
}
|
||||
|
||||
defer read.Close()
|
||||
return NewImageFromReader(read, filepath.Base(uri.String()))
|
||||
}
|
||||
|
||||
// NewImageFromReader creates a new image from a data stream.
|
||||
// The name parameter is required to uniquely identify this image (for caching etc.).
|
||||
// If the image in this io.Reader is an SVG, the name should end ".svg".
|
||||
// Images returned from this method will scale to fit the canvas object.
|
||||
// The method for scaling can be set using the Fill field.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewImageFromReader(read io.Reader, name string) *Image {
|
||||
data, err := io.ReadAll(read)
|
||||
if err != nil {
|
||||
fyne.LogError("Unable to read image data", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
res := &fyne.StaticResource{
|
||||
StaticName: name,
|
||||
StaticContent: data,
|
||||
}
|
||||
|
||||
return NewImageFromResource(res)
|
||||
}
|
||||
|
||||
// NewImageFromResource creates a new image by loading the specified resource.
|
||||
// Images returned from this method will scale to fit the canvas object.
|
||||
// The method for scaling can be set using the Fill field.
|
||||
func NewImageFromResource(res fyne.Resource) *Image {
|
||||
return &Image{Resource: res}
|
||||
}
|
||||
|
||||
// NewImageFromImage returns a new Image instance that is rendered from the Go
|
||||
// image.Image passed in.
|
||||
// Images returned from this method will scale to fit the canvas object.
|
||||
// The method for scaling can be set using the Fill field.
|
||||
func NewImageFromImage(img image.Image) *Image {
|
||||
return &Image{Image: img}
|
||||
}
|
||||
|
||||
func (i *Image) name() string {
|
||||
if i.Resource != nil {
|
||||
return i.Resource.Name()
|
||||
} else if i.File != "" {
|
||||
return i.File
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *Image) updateReader() (io.ReadCloser, error) {
|
||||
i.isSVG = false
|
||||
if i.Resource != nil {
|
||||
i.isSVG = svg.IsResourceSVG(i.Resource)
|
||||
content := i.Resource.Content()
|
||||
if res, ok := i.Resource.(fyne.ThemedResource); i.isSVG && ok {
|
||||
th := cache.WidgetTheme(i)
|
||||
if th != nil {
|
||||
col := th.Color(res.ThemeColorName(), fyne.CurrentApp().Settings().ThemeVariant())
|
||||
var err error
|
||||
content, err = svg.Colorize(content, col)
|
||||
if err != nil {
|
||||
fyne.LogError("", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(content)), nil
|
||||
} else if i.File != "" {
|
||||
var err error
|
||||
|
||||
fd, err := os.Open(i.File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.isSVG = svg.IsFileSVG(i.File)
|
||||
return fd, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) {
|
||||
var pixWidth, pixHeight int
|
||||
|
||||
if reader != nil {
|
||||
r, width, height, aspect, err := i.imageDetailsFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = r
|
||||
i.aspect = aspect
|
||||
pixWidth, pixHeight = width, height
|
||||
} else if i.Image != nil {
|
||||
original := i.Image.Bounds().Size()
|
||||
i.aspect = float32(original.X) / float32(original.Y)
|
||||
pixWidth, pixHeight = original.X, original.Y
|
||||
} else {
|
||||
return nil, errors.New("no matching image source")
|
||||
}
|
||||
|
||||
if i.FillMode == ImageFillOriginal {
|
||||
i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight))
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) {
|
||||
if source == nil {
|
||||
return nil, 0, 0, 0, errors.New("no matching reading reader")
|
||||
}
|
||||
|
||||
if i.isSVG {
|
||||
var err error
|
||||
|
||||
i.icon, err = svg.NewDecoder(source)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, err
|
||||
}
|
||||
config := i.icon.Config()
|
||||
width, height = config.Width, config.Height
|
||||
aspect = config.Aspect
|
||||
} else {
|
||||
var buf bytes.Buffer
|
||||
tee := io.TeeReader(source, &buf)
|
||||
reader = io.MultiReader(&buf, source)
|
||||
|
||||
config, _, err := image.DecodeConfig(tee)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, err
|
||||
}
|
||||
width, height = config.Width, config.Height
|
||||
aspect = float32(width) / float32(height)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Image) renderSVG(width, height float32) (image.Image, error) {
|
||||
c := fyne.CurrentApp().Driver().CanvasForObject(i)
|
||||
screenWidth, screenHeight := int(width), int(height)
|
||||
if c != nil {
|
||||
// We want real output pixel count not just the screen coordinate space (i.e. macOS Retina)
|
||||
screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height})
|
||||
} else { // no canvas info, assume HiDPI
|
||||
screenWidth *= 2
|
||||
screenHeight *= 2
|
||||
}
|
||||
|
||||
tex := cache.GetSvg(i.name(), i, screenWidth, screenHeight)
|
||||
if tex != nil {
|
||||
return tex, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
tex, err = i.icon.Draw(screenWidth, screenHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache.SetSvg(i.name(), i, tex, screenWidth, screenHeight)
|
||||
return tex, nil
|
||||
}
|
||||
108
vendor/fyne.io/fyne/v2/canvas/line.go
generated
vendored
Normal file
108
vendor/fyne.io/fyne/v2/canvas/line.go
generated
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Line)(nil)
|
||||
|
||||
// Line describes a colored line primitive in a Fyne canvas.
|
||||
// Lines are special as they can have a negative width or height to indicate
|
||||
// an inverse slope (i.e. slope up vs down).
|
||||
type Line struct {
|
||||
Position1 fyne.Position // The current top-left position of the Line
|
||||
Position2 fyne.Position // The current bottom-right position of the Line
|
||||
Hidden bool // Is this Line currently hidden
|
||||
|
||||
StrokeColor color.Color // The line stroke color
|
||||
StrokeWidth float32 // The stroke width of the line
|
||||
}
|
||||
|
||||
// Size returns the current size of bounding box for this line object
|
||||
func (l *Line) Size() fyne.Size {
|
||||
return fyne.NewSize(
|
||||
float32(math.Abs(float64(l.Position2.X)-float64(l.Position1.X))),
|
||||
float32(math.Abs(float64(l.Position2.Y)-float64(l.Position1.Y))),
|
||||
)
|
||||
}
|
||||
|
||||
// Resize sets a new bottom-right position for the line object, then it will then be refreshed.
|
||||
func (l *Line) Resize(size fyne.Size) {
|
||||
if size == l.Size() {
|
||||
return
|
||||
}
|
||||
|
||||
if l.Position1.X <= l.Position2.X {
|
||||
l.Position2.X = l.Position1.X + size.Width
|
||||
} else {
|
||||
l.Position1.X = l.Position2.X + size.Width
|
||||
}
|
||||
if l.Position1.Y <= l.Position2.Y {
|
||||
l.Position2.Y = l.Position1.Y + size.Height
|
||||
} else {
|
||||
l.Position1.Y = l.Position2.Y + size.Height
|
||||
}
|
||||
Refresh(l)
|
||||
}
|
||||
|
||||
// Position gets the current top-left position of this line object, relative to its parent / canvas
|
||||
func (l *Line) Position() fyne.Position {
|
||||
return fyne.NewPos(fyne.Min(l.Position1.X, l.Position2.X), fyne.Min(l.Position1.Y, l.Position2.Y))
|
||||
}
|
||||
|
||||
// Move the line object to a new position, relative to its parent / canvas
|
||||
func (l *Line) Move(pos fyne.Position) {
|
||||
oldPos := l.Position()
|
||||
if oldPos == pos {
|
||||
return
|
||||
}
|
||||
|
||||
deltaX := pos.X - oldPos.X
|
||||
deltaY := pos.Y - oldPos.Y
|
||||
|
||||
l.Position1 = l.Position1.AddXY(deltaX, deltaY)
|
||||
l.Position2 = l.Position2.AddXY(deltaX, deltaY)
|
||||
repaint(l)
|
||||
}
|
||||
|
||||
// MinSize for a Line simply returns Size{1, 1} as there is no
|
||||
// explicit content
|
||||
func (l *Line) MinSize() fyne.Size {
|
||||
return fyne.NewSize(1, 1)
|
||||
}
|
||||
|
||||
// Visible returns true if this line// Show will set this circle to be visible is visible, false otherwise
|
||||
func (l *Line) Visible() bool {
|
||||
return !l.Hidden
|
||||
}
|
||||
|
||||
// Show will set this line to be visible
|
||||
func (l *Line) Show() {
|
||||
l.Hidden = false
|
||||
|
||||
l.Refresh()
|
||||
}
|
||||
|
||||
// Hide will set this line to not be visible
|
||||
func (l *Line) Hide() {
|
||||
l.Hidden = true
|
||||
|
||||
repaint(l)
|
||||
}
|
||||
|
||||
// Refresh causes this line to be redrawn with its configured state.
|
||||
func (l *Line) Refresh() {
|
||||
Refresh(l)
|
||||
}
|
||||
|
||||
// NewLine returns a new Line instance
|
||||
func NewLine(color color.Color) *Line {
|
||||
return &Line{
|
||||
StrokeColor: color,
|
||||
StrokeWidth: 1,
|
||||
}
|
||||
}
|
||||
200
vendor/fyne.io/fyne/v2/canvas/raster.go
generated
vendored
Normal file
200
vendor/fyne.io/fyne/v2/canvas/raster.go
generated
vendored
Normal file
@ -0,0 +1,200 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Raster)(nil)
|
||||
|
||||
// Raster describes a raster image area that can render in a Fyne canvas
|
||||
type Raster struct {
|
||||
baseObject
|
||||
|
||||
// Render the raster image from code
|
||||
Generator func(w, h int) image.Image
|
||||
|
||||
// Set a translucency value > 0.0 to fade the raster
|
||||
Translucency float64
|
||||
// Specify the type of scaling interpolation applied to the raster if it is not full-size
|
||||
// Since: 1.4.1
|
||||
ScaleMode ImageScale
|
||||
}
|
||||
|
||||
// Alpha is a convenience function that returns the alpha value for a raster
|
||||
// based on its Translucency value. The result is 1.0 - Translucency.
|
||||
func (r *Raster) Alpha() float64 {
|
||||
return 1.0 - r.Translucency
|
||||
}
|
||||
|
||||
// Hide will set this raster to not be visible
|
||||
func (r *Raster) Hide() {
|
||||
r.baseObject.Hide()
|
||||
|
||||
repaint(r)
|
||||
}
|
||||
|
||||
// Move the raster to a new position, relative to its parent / canvas
|
||||
func (r *Raster) Move(pos fyne.Position) {
|
||||
if r.Position() == pos {
|
||||
return
|
||||
}
|
||||
|
||||
r.baseObject.Move(pos)
|
||||
|
||||
repaint(r)
|
||||
}
|
||||
|
||||
// Resize on a raster image causes the new size to be set and then calls Refresh.
|
||||
// This causes the underlying data to be recalculated and a new output to be drawn.
|
||||
func (r *Raster) Resize(s fyne.Size) {
|
||||
if s == r.Size() {
|
||||
return
|
||||
}
|
||||
|
||||
r.baseObject.Resize(s)
|
||||
Refresh(r)
|
||||
}
|
||||
|
||||
// Refresh causes this raster to be redrawn with its configured state.
|
||||
func (r *Raster) Refresh() {
|
||||
Refresh(r)
|
||||
}
|
||||
|
||||
// NewRaster returns a new Image instance that is rendered dynamically using
|
||||
// the specified generate function.
|
||||
// Images returned from this method should draw dynamically to fill the width
|
||||
// and height parameters passed to pixelColor.
|
||||
func NewRaster(generate func(w, h int) image.Image) *Raster {
|
||||
return &Raster{Generator: generate}
|
||||
}
|
||||
|
||||
type pixelRaster struct {
|
||||
r *Raster
|
||||
|
||||
img draw.Image
|
||||
}
|
||||
|
||||
// NewRasterWithPixels returns a new Image instance that is rendered dynamically
|
||||
// by iterating over the specified pixelColor function for each x, y pixel.
|
||||
// Images returned from this method should draw dynamically to fill the width
|
||||
// and height parameters passed to pixelColor.
|
||||
func NewRasterWithPixels(pixelColor func(x, y, w, h int) color.Color) *Raster {
|
||||
pix := &pixelRaster{}
|
||||
pix.r = &Raster{
|
||||
Generator: func(w, h int) image.Image {
|
||||
if pix.img == nil || pix.img.Bounds().Size().X != w || pix.img.Bounds().Size().Y != h {
|
||||
// raster first pixel, figure out color type
|
||||
var dst draw.Image
|
||||
rect := image.Rect(0, 0, w, h)
|
||||
switch pixelColor(0, 0, w, h).(type) {
|
||||
case color.Alpha:
|
||||
dst = image.NewAlpha(rect)
|
||||
case color.Alpha16:
|
||||
dst = image.NewAlpha16(rect)
|
||||
case color.CMYK:
|
||||
dst = image.NewCMYK(rect)
|
||||
case color.Gray:
|
||||
dst = image.NewGray(rect)
|
||||
case color.Gray16:
|
||||
dst = image.NewGray16(rect)
|
||||
case color.NRGBA:
|
||||
dst = image.NewNRGBA(rect)
|
||||
case color.NRGBA64:
|
||||
dst = image.NewNRGBA64(rect)
|
||||
case color.RGBA:
|
||||
dst = image.NewRGBA(rect)
|
||||
case color.RGBA64:
|
||||
dst = image.NewRGBA64(rect)
|
||||
default:
|
||||
dst = image.NewRGBA(rect)
|
||||
}
|
||||
pix.img = dst
|
||||
}
|
||||
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
pix.img.Set(x, y, pixelColor(x, y, w, h))
|
||||
}
|
||||
}
|
||||
|
||||
return pix.img
|
||||
},
|
||||
}
|
||||
return pix.r
|
||||
}
|
||||
|
||||
type subImg interface {
|
||||
SubImage(r image.Rectangle) image.Image
|
||||
}
|
||||
|
||||
// NewRasterFromImage returns a new Raster instance that is rendered from the Go
|
||||
// image.Image passed in.
|
||||
// Rasters returned from this method will map pixel for pixel to the screen
|
||||
// starting img.Bounds().Min pixels from the top left of the canvas object.
|
||||
// Truncates rather than scales the image.
|
||||
// If smaller than the target space, the image will be padded with zero-pixels to the target size.
|
||||
func NewRasterFromImage(img image.Image) *Raster {
|
||||
return &Raster{
|
||||
Generator: func(w int, h int) image.Image {
|
||||
bounds := img.Bounds()
|
||||
|
||||
rect := image.Rect(0, 0, w, h)
|
||||
|
||||
switch {
|
||||
case w == bounds.Max.X && h == bounds.Max.Y:
|
||||
return img
|
||||
case w >= bounds.Max.X && h >= bounds.Max.Y:
|
||||
// try quickly truncating
|
||||
if sub, ok := img.(subImg); ok {
|
||||
return sub.SubImage(image.Rectangle{
|
||||
Min: bounds.Min,
|
||||
Max: image.Point{
|
||||
X: bounds.Min.X + w,
|
||||
Y: bounds.Min.Y + h,
|
||||
},
|
||||
})
|
||||
}
|
||||
default:
|
||||
if !rect.Overlaps(bounds) {
|
||||
return image.NewUniform(color.RGBA{})
|
||||
}
|
||||
bounds = bounds.Intersect(rect)
|
||||
}
|
||||
|
||||
// respect the user's pixel format (if possible)
|
||||
var dst draw.Image
|
||||
switch i := img.(type) {
|
||||
case *image.Alpha:
|
||||
dst = image.NewAlpha(rect)
|
||||
case *image.Alpha16:
|
||||
dst = image.NewAlpha16(rect)
|
||||
case *image.CMYK:
|
||||
dst = image.NewCMYK(rect)
|
||||
case *image.Gray:
|
||||
dst = image.NewGray(rect)
|
||||
case *image.Gray16:
|
||||
dst = image.NewGray16(rect)
|
||||
case *image.NRGBA:
|
||||
dst = image.NewNRGBA(rect)
|
||||
case *image.NRGBA64:
|
||||
dst = image.NewNRGBA64(rect)
|
||||
case *image.Paletted:
|
||||
dst = image.NewPaletted(rect, i.Palette)
|
||||
case *image.RGBA:
|
||||
dst = image.NewRGBA(rect)
|
||||
case *image.RGBA64:
|
||||
dst = image.NewRGBA64(rect)
|
||||
default:
|
||||
dst = image.NewRGBA(rect)
|
||||
}
|
||||
|
||||
draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
|
||||
return dst
|
||||
},
|
||||
}
|
||||
}
|
||||
68
vendor/fyne.io/fyne/v2/canvas/rectangle.go
generated
vendored
Normal file
68
vendor/fyne.io/fyne/v2/canvas/rectangle.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Rectangle)(nil)
|
||||
|
||||
// Rectangle describes a colored rectangle primitive in a Fyne canvas
|
||||
type Rectangle struct {
|
||||
baseObject
|
||||
|
||||
FillColor color.Color // The rectangle fill color
|
||||
StrokeColor color.Color // The rectangle stroke color
|
||||
StrokeWidth float32 // The stroke width of the rectangle
|
||||
// The radius of the rectangle corners
|
||||
//
|
||||
// Since: 2.4
|
||||
CornerRadius float32
|
||||
}
|
||||
|
||||
// Hide will set this rectangle to not be visible
|
||||
func (r *Rectangle) Hide() {
|
||||
r.baseObject.Hide()
|
||||
|
||||
repaint(r)
|
||||
}
|
||||
|
||||
// Move the rectangle to a new position, relative to its parent / canvas
|
||||
func (r *Rectangle) Move(pos fyne.Position) {
|
||||
if r.Position() == pos {
|
||||
return
|
||||
}
|
||||
|
||||
r.baseObject.Move(pos)
|
||||
|
||||
repaint(r)
|
||||
}
|
||||
|
||||
// Refresh causes this rectangle to be redrawn with its configured state.
|
||||
func (r *Rectangle) Refresh() {
|
||||
Refresh(r)
|
||||
}
|
||||
|
||||
// Resize on a rectangle updates the new size of this object.
|
||||
// If it has a stroke width this will cause it to Refresh.
|
||||
func (r *Rectangle) Resize(s fyne.Size) {
|
||||
if s == r.Size() {
|
||||
return
|
||||
}
|
||||
|
||||
r.baseObject.Resize(s)
|
||||
if r.StrokeWidth == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
Refresh(r)
|
||||
}
|
||||
|
||||
// NewRectangle returns a new Rectangle instance
|
||||
func NewRectangle(color color.Color) *Rectangle {
|
||||
return &Rectangle{
|
||||
FillColor: color,
|
||||
}
|
||||
}
|
||||
85
vendor/fyne.io/fyne/v2/canvas/text.go
generated
vendored
Normal file
85
vendor/fyne.io/fyne/v2/canvas/text.go
generated
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
package canvas
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Text)(nil)
|
||||
|
||||
// Text describes a text primitive in a Fyne canvas.
|
||||
// A text object can have a style set which will apply to the whole string.
|
||||
// No formatting or text parsing will be performed
|
||||
type Text struct {
|
||||
baseObject
|
||||
Alignment fyne.TextAlign // The alignment of the text content
|
||||
|
||||
Color color.Color // The main text draw color
|
||||
Text string // The string content of this Text
|
||||
TextSize float32 // Size of the text - if the Canvas scale is 1.0 this will be equivalent to point size
|
||||
TextStyle fyne.TextStyle // The style of the text content
|
||||
|
||||
// FontSource defines a resource that can be used instead of the theme for looking up the font.
|
||||
// When a font source is set the `TextStyle` may not be effective, as it will be limited to the styles
|
||||
// present in the data provided.
|
||||
//
|
||||
// Since: 2.5
|
||||
FontSource fyne.Resource
|
||||
}
|
||||
|
||||
// Hide will set this text to not be visible
|
||||
func (t *Text) Hide() {
|
||||
t.baseObject.Hide()
|
||||
|
||||
repaint(t)
|
||||
}
|
||||
|
||||
// MinSize returns the minimum size of this text object based on its font size and content.
|
||||
// This is normally determined by the render implementation.
|
||||
func (t *Text) MinSize() fyne.Size {
|
||||
s, _ := fyne.CurrentApp().Driver().RenderedTextSize(t.Text, t.TextSize, t.TextStyle, t.FontSource)
|
||||
return s
|
||||
}
|
||||
|
||||
// Move the text to a new position, relative to its parent / canvas
|
||||
func (t *Text) Move(pos fyne.Position) {
|
||||
if t.Position() == pos {
|
||||
return
|
||||
}
|
||||
|
||||
t.baseObject.Move(pos)
|
||||
|
||||
repaint(t)
|
||||
}
|
||||
|
||||
// Resize on a text updates the new size of this object, which may not result in a visual change, depending on alignment.
|
||||
func (t *Text) Resize(s fyne.Size) {
|
||||
if s == t.Size() {
|
||||
return
|
||||
}
|
||||
|
||||
t.baseObject.Resize(s)
|
||||
Refresh(t)
|
||||
}
|
||||
|
||||
// SetMinSize has no effect as the smallest size this canvas object can be is based on its font size and content.
|
||||
func (t *Text) SetMinSize(fyne.Size) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// Refresh causes this text to be redrawn with its configured state.
|
||||
func (t *Text) Refresh() {
|
||||
Refresh(t)
|
||||
}
|
||||
|
||||
// NewText returns a new Text implementation
|
||||
func NewText(text string, color color.Color) *Text {
|
||||
return &Text{
|
||||
Color: color,
|
||||
Text: text,
|
||||
TextSize: theme.TextSize(),
|
||||
}
|
||||
}
|
||||
107
vendor/fyne.io/fyne/v2/canvasobject.go
generated
vendored
Normal file
107
vendor/fyne.io/fyne/v2/canvasobject.go
generated
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
package fyne
|
||||
|
||||
// CanvasObject describes any graphical object that can be added to a canvas.
|
||||
// Objects have a size and position that can be controlled through this API.
|
||||
// MinSize is used to determine the minimum size which this object should be displayed.
|
||||
// An object will be visible by default but can be hidden with Hide() and re-shown with Show().
|
||||
//
|
||||
// Note: If this object is controlled as part of a Layout you should not call
|
||||
// Resize(Size) or Move(Position).
|
||||
type CanvasObject interface {
|
||||
// geometry
|
||||
|
||||
// MinSize returns the minimum size this object needs to be drawn.
|
||||
MinSize() Size
|
||||
// Move moves this object to the given position relative to its parent.
|
||||
// This should only be called if your object is not in a container with a layout manager.
|
||||
Move(Position)
|
||||
// Position returns the current position of the object relative to its parent.
|
||||
Position() Position
|
||||
// Resize resizes this object to the given size.
|
||||
// This should only be called if your object is not in a container with a layout manager.
|
||||
Resize(Size)
|
||||
// Size returns the current size of this object.
|
||||
Size() Size
|
||||
|
||||
// visibility
|
||||
|
||||
// Hide hides this object.
|
||||
Hide()
|
||||
// Visible returns whether this object is visible or not.
|
||||
Visible() bool
|
||||
// Show shows this object.
|
||||
Show()
|
||||
|
||||
// Refresh must be called if this object should be redrawn because its inner state changed.
|
||||
Refresh()
|
||||
}
|
||||
|
||||
// Disableable describes any [CanvasObject] that can be disabled.
|
||||
// This is primarily used with objects that also implement the Tappable interface.
|
||||
type Disableable interface {
|
||||
Enable()
|
||||
Disable()
|
||||
Disabled() bool
|
||||
}
|
||||
|
||||
// DoubleTappable describes any [CanvasObject] that can also be double tapped.
|
||||
type DoubleTappable interface {
|
||||
DoubleTapped(*PointEvent)
|
||||
}
|
||||
|
||||
// Draggable indicates that a [CanvasObject] can be dragged.
|
||||
// This is used for any item that the user has indicated should be moved across the screen.
|
||||
type Draggable interface {
|
||||
Dragged(*DragEvent)
|
||||
DragEnd()
|
||||
}
|
||||
|
||||
// Focusable describes any [CanvasObject] that can respond to being focused.
|
||||
// It will receive the FocusGained and FocusLost events appropriately.
|
||||
// When focused it will also have TypedRune called as text is input and
|
||||
// TypedKey called when other keys are pressed.
|
||||
//
|
||||
// Note: You must not change canvas state (including overlays or focus) in FocusGained or FocusLost
|
||||
// or you would end up with a dead-lock.
|
||||
type Focusable interface {
|
||||
// FocusGained is a hook called by the focus handling logic after this object gained the focus.
|
||||
FocusGained()
|
||||
// FocusLost is a hook called by the focus handling logic after this object lost the focus.
|
||||
FocusLost()
|
||||
|
||||
// TypedRune is a hook called by the input handling logic on text input events if this object is focused.
|
||||
TypedRune(rune)
|
||||
// TypedKey is a hook called by the input handling logic on key events if this object is focused.
|
||||
TypedKey(*KeyEvent)
|
||||
}
|
||||
|
||||
// Scrollable describes any [CanvasObject] that can also be scrolled.
|
||||
// This is mostly used to implement the widget.ScrollContainer.
|
||||
type Scrollable interface {
|
||||
Scrolled(*ScrollEvent)
|
||||
}
|
||||
|
||||
// SecondaryTappable describes a [CanvasObject] that can be right-clicked or long-tapped.
|
||||
type SecondaryTappable interface {
|
||||
TappedSecondary(*PointEvent)
|
||||
}
|
||||
|
||||
// Shortcutable describes any [CanvasObject] that can respond to shortcut commands (quit, cut, copy, and paste).
|
||||
type Shortcutable interface {
|
||||
TypedShortcut(Shortcut)
|
||||
}
|
||||
|
||||
// Tabbable describes any object that needs to accept the Tab key presses.
|
||||
//
|
||||
// Since: 2.1
|
||||
type Tabbable interface {
|
||||
// AcceptsTab is a hook called by the key press handling logic.
|
||||
// If it returns true then the Tab key events will be sent using TypedKey.
|
||||
AcceptsTab() bool
|
||||
}
|
||||
|
||||
// Tappable describes any [CanvasObject] that can also be tapped.
|
||||
// This should be implemented by buttons etc that wish to handle pointer interactions.
|
||||
type Tappable interface {
|
||||
Tapped(*PointEvent)
|
||||
}
|
||||
9
vendor/fyne.io/fyne/v2/clipboard.go
generated
vendored
Normal file
9
vendor/fyne.io/fyne/v2/clipboard.go
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
package fyne
|
||||
|
||||
// Clipboard represents the system clipboard interface
|
||||
type Clipboard interface {
|
||||
// Content returns the clipboard content
|
||||
Content() string
|
||||
// SetContent sets the clipboard content
|
||||
SetContent(content string)
|
||||
}
|
||||
39
vendor/fyne.io/fyne/v2/cloud.go
generated
vendored
Normal file
39
vendor/fyne.io/fyne/v2/cloud.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
package fyne
|
||||
|
||||
// CloudProvider specifies the identifying information of a cloud provider.
|
||||
// This information is mostly used by the [fyne.io/cloud.ShowSettings] user flow.
|
||||
//
|
||||
// Since: 2.3
|
||||
type CloudProvider interface {
|
||||
// ProviderDescription returns a more detailed description of this cloud provider.
|
||||
ProviderDescription() string
|
||||
// ProviderIcon returns an icon resource that is associated with the given cloud service.
|
||||
ProviderIcon() Resource
|
||||
// ProviderName returns the name of this cloud provider, usually the name of the service it uses.
|
||||
ProviderName() string
|
||||
|
||||
// Cleanup is called when this provider is no longer used and should be disposed.
|
||||
// This is guaranteed to execute before a new provider is `Setup`
|
||||
Cleanup(App)
|
||||
// Setup is called when this provider is being used for the first time.
|
||||
// Returning an error will exit the cloud setup process, though it can be retried.
|
||||
Setup(App) error
|
||||
}
|
||||
|
||||
// CloudProviderPreferences interface defines the functionality that a cloud provider will include if it is capable
|
||||
// of synchronizing user preferences.
|
||||
//
|
||||
// Since: 2.3
|
||||
type CloudProviderPreferences interface {
|
||||
// CloudPreferences returns a preference provider that will sync values to the cloud this provider uses.
|
||||
CloudPreferences(App) Preferences
|
||||
}
|
||||
|
||||
// CloudProviderStorage interface defines the functionality that a cloud provider will include if it is capable
|
||||
// of synchronizing user documents.
|
||||
//
|
||||
// Since: 2.3
|
||||
type CloudProviderStorage interface {
|
||||
// CloudStorage returns a storage provider that will sync documents to the cloud this provider uses.
|
||||
CloudStorage(App) Storage
|
||||
}
|
||||
202
vendor/fyne.io/fyne/v2/container.go
generated
vendored
Normal file
202
vendor/fyne.io/fyne/v2/container.go
generated
vendored
Normal file
@ -0,0 +1,202 @@
|
||||
package fyne
|
||||
|
||||
// Declare conformity to [CanvasObject]
|
||||
var _ CanvasObject = (*Container)(nil)
|
||||
|
||||
// Container is a [CanvasObject] that contains a collection of child objects.
|
||||
// The layout of the children is set by the specified Layout.
|
||||
type Container struct {
|
||||
size Size // The current size of the Container
|
||||
position Position // The current position of the Container
|
||||
Hidden bool // Is this Container hidden
|
||||
|
||||
Layout Layout // The Layout algorithm for arranging child [CanvasObject]s
|
||||
Objects []CanvasObject // The set of [CanvasObject]s this container holds
|
||||
}
|
||||
|
||||
// NewContainer returns a new [Container] instance holding the specified [CanvasObject]s.
|
||||
//
|
||||
// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] to create a container that uses manual layout.
|
||||
func NewContainer(objects ...CanvasObject) *Container {
|
||||
return NewContainerWithoutLayout(objects...)
|
||||
}
|
||||
|
||||
// NewContainerWithoutLayout returns a new [Container] instance holding the specified
|
||||
// [CanvasObject]s that are manually arranged.
|
||||
//
|
||||
// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] instead.
|
||||
func NewContainerWithoutLayout(objects ...CanvasObject) *Container {
|
||||
ret := &Container{
|
||||
Objects: objects,
|
||||
}
|
||||
|
||||
ret.size = ret.MinSize()
|
||||
return ret
|
||||
}
|
||||
|
||||
// NewContainerWithLayout returns a new [Container] instance holding the specified
|
||||
// [CanvasObject]s which will be laid out according to the specified Layout.
|
||||
//
|
||||
// Deprecated: Use [fyne.io/fyne/v2/container.New] instead.
|
||||
func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container {
|
||||
ret := &Container{
|
||||
Objects: objects,
|
||||
Layout: layout,
|
||||
}
|
||||
|
||||
ret.size = layout.MinSize(objects)
|
||||
ret.layout()
|
||||
return ret
|
||||
}
|
||||
|
||||
// Add appends the specified object to the items this container manages.
|
||||
//
|
||||
// Since: 1.4
|
||||
func (c *Container) Add(add CanvasObject) {
|
||||
if add == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Objects = append(c.Objects, add)
|
||||
c.layout()
|
||||
}
|
||||
|
||||
// AddObject adds another [CanvasObject] to the set this Container holds.
|
||||
//
|
||||
// Deprecated: Use [Container.Add] instead.
|
||||
func (c *Container) AddObject(o CanvasObject) {
|
||||
c.Add(o)
|
||||
}
|
||||
|
||||
// Hide sets this container, and all its children, to be not visible.
|
||||
func (c *Container) Hide() {
|
||||
if c.Hidden {
|
||||
return
|
||||
}
|
||||
|
||||
c.Hidden = true
|
||||
repaint(c)
|
||||
}
|
||||
|
||||
// MinSize calculates the minimum size of c.
|
||||
// This is delegated to the [Container.Layout], if specified, otherwise it will be calculated.
|
||||
func (c *Container) MinSize() Size {
|
||||
if c.Layout != nil {
|
||||
return c.Layout.MinSize(c.Objects)
|
||||
}
|
||||
|
||||
minSize := NewSize(1, 1)
|
||||
for _, child := range c.Objects {
|
||||
minSize = minSize.Max(child.MinSize())
|
||||
}
|
||||
|
||||
return minSize
|
||||
}
|
||||
|
||||
// Move the container (and all its children) to a new position, relative to its parent.
|
||||
func (c *Container) Move(pos Position) {
|
||||
c.position = pos
|
||||
repaint(c)
|
||||
}
|
||||
|
||||
// Position gets the current position of c relative to its parent.
|
||||
func (c *Container) Position() Position {
|
||||
return c.position
|
||||
}
|
||||
|
||||
// Refresh causes this object to be redrawn in its current state
|
||||
func (c *Container) Refresh() {
|
||||
c.layout()
|
||||
|
||||
for _, child := range c.Objects {
|
||||
child.Refresh()
|
||||
}
|
||||
|
||||
// this is basically just canvas.Refresh(c) without the package loop
|
||||
o := CurrentApp().Driver().CanvasForObject(c)
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
o.Refresh(c)
|
||||
}
|
||||
|
||||
// Remove updates the contents of this container to no longer include the specified object.
|
||||
// This method is not intended to be used inside a loop, to remove all the elements.
|
||||
// It is much more efficient to call [Container.RemoveAll) instead.
|
||||
func (c *Container) Remove(rem CanvasObject) {
|
||||
if len(c.Objects) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i, o := range c.Objects {
|
||||
if o != rem {
|
||||
continue
|
||||
}
|
||||
copy(c.Objects[i:], c.Objects[i+1:])
|
||||
c.Objects[len(c.Objects)-1] = nil
|
||||
c.Objects = c.Objects[:len(c.Objects)-1]
|
||||
c.layout()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAll updates the contents of this container to no longer include any objects.
|
||||
//
|
||||
// Since: 2.2
|
||||
func (c *Container) RemoveAll() {
|
||||
c.Objects = nil
|
||||
c.layout()
|
||||
}
|
||||
|
||||
// Resize sets a new size for c.
|
||||
func (c *Container) Resize(size Size) {
|
||||
if c.size == size {
|
||||
return
|
||||
}
|
||||
|
||||
c.size = size
|
||||
c.layout()
|
||||
}
|
||||
|
||||
// Show sets this container, and all its children, to be visible.
|
||||
func (c *Container) Show() {
|
||||
if !c.Hidden {
|
||||
return
|
||||
}
|
||||
|
||||
c.Hidden = false
|
||||
}
|
||||
|
||||
// Size returns the current size c.
|
||||
func (c *Container) Size() Size {
|
||||
return c.size
|
||||
}
|
||||
|
||||
// Visible returns true if the container is currently visible, false otherwise.
|
||||
func (c *Container) Visible() bool {
|
||||
return !c.Hidden
|
||||
}
|
||||
|
||||
func (c *Container) layout() {
|
||||
if c.Layout == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Layout.Layout(c.Objects, c.size)
|
||||
}
|
||||
|
||||
// repaint instructs the containing canvas to redraw, even if nothing changed.
|
||||
// This method is a duplicate of what is in `canvas/canvas.go` to avoid a dependency loop or public API.
|
||||
func repaint(obj *Container) {
|
||||
app := CurrentApp()
|
||||
if app == nil || app.Driver() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := app.Driver().CanvasForObject(obj)
|
||||
if c != nil {
|
||||
if paint, ok := c.(interface{ SetDirty() }); ok {
|
||||
paint.SetDirty()
|
||||
}
|
||||
}
|
||||
}
|
||||
473
vendor/fyne.io/fyne/v2/container/apptabs.go
generated
vendored
Normal file
473
vendor/fyne.io/fyne/v2/container/apptabs.go
generated
vendored
Normal file
@ -0,0 +1,473 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// Declare conformity with Widget interface.
|
||||
var _ fyne.Widget = (*AppTabs)(nil)
|
||||
|
||||
// AppTabs container is used to split your application into various different areas identified by tabs.
|
||||
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
|
||||
// Each item is represented by a button at the edge of the container.
|
||||
//
|
||||
// Since: 1.4
|
||||
type AppTabs struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Items []*TabItem
|
||||
|
||||
// Deprecated: Use `OnSelected func(*TabItem)` instead.
|
||||
OnChanged func(*TabItem) `json:"-"`
|
||||
OnSelected func(*TabItem) `json:"-"`
|
||||
OnUnselected func(*TabItem) `json:"-"`
|
||||
|
||||
current int
|
||||
location TabLocation
|
||||
isTransitioning bool
|
||||
|
||||
popUpMenu *widget.PopUpMenu
|
||||
}
|
||||
|
||||
// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewAppTabs(items ...*TabItem) *AppTabs {
|
||||
tabs := &AppTabs{}
|
||||
tabs.BaseWidget.ExtendBaseWidget(tabs)
|
||||
tabs.SetItems(items)
|
||||
return tabs
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||||
//
|
||||
// Implements: fyne.Widget
|
||||
func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer {
|
||||
t.BaseWidget.ExtendBaseWidget(t)
|
||||
th := t.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
r := &appTabsRenderer{
|
||||
baseTabsRenderer: baseTabsRenderer{
|
||||
bar: &fyne.Container{},
|
||||
divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)),
|
||||
indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)),
|
||||
},
|
||||
appTabs: t,
|
||||
}
|
||||
r.action = r.buildOverflowTabsButton()
|
||||
r.tabs = t
|
||||
|
||||
// Initially setup the tab bar to only show one tab, all others will be in overflow.
|
||||
// When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit.
|
||||
r.updateTabs(1)
|
||||
r.updateIndicator(false)
|
||||
r.applyTheme(t)
|
||||
return r
|
||||
}
|
||||
|
||||
// Append adds a new TabItem to the end of the tab bar.
|
||||
func (t *AppTabs) Append(item *TabItem) {
|
||||
t.SetItems(append(t.Items, item))
|
||||
}
|
||||
|
||||
// CurrentTab returns the currently selected TabItem.
|
||||
//
|
||||
// Deprecated: Use `AppTabs.Selected() *TabItem` instead.
|
||||
func (t *AppTabs) CurrentTab() *TabItem {
|
||||
if t.current < 0 || t.current >= len(t.Items) {
|
||||
return nil
|
||||
}
|
||||
return t.Items[t.current]
|
||||
}
|
||||
|
||||
// CurrentTabIndex returns the index of the currently selected TabItem.
|
||||
//
|
||||
// Deprecated: Use `AppTabs.SelectedIndex() int` instead.
|
||||
func (t *AppTabs) CurrentTabIndex() int {
|
||||
return t.current
|
||||
}
|
||||
|
||||
// DisableIndex disables the TabItem at the specified index.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *AppTabs) DisableIndex(i int) {
|
||||
disableIndex(t, i)
|
||||
}
|
||||
|
||||
// DisableItem disables the specified TabItem.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *AppTabs) DisableItem(item *TabItem) {
|
||||
disableItem(t, item)
|
||||
}
|
||||
|
||||
// EnableIndex enables the TabItem at the specified index.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *AppTabs) EnableIndex(i int) {
|
||||
enableIndex(t, i)
|
||||
}
|
||||
|
||||
// EnableItem enables the specified TabItem.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *AppTabs) EnableItem(item *TabItem) {
|
||||
enableItem(t, item)
|
||||
}
|
||||
|
||||
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
|
||||
//
|
||||
// Deprecated: Support for extending containers is being removed
|
||||
func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) {
|
||||
t.BaseWidget.ExtendBaseWidget(wid)
|
||||
}
|
||||
|
||||
// Hide hides the widget.
|
||||
//
|
||||
// Implements: fyne.CanvasObject
|
||||
func (t *AppTabs) Hide() {
|
||||
if t.popUpMenu != nil {
|
||||
t.popUpMenu.Hide()
|
||||
t.popUpMenu = nil
|
||||
}
|
||||
t.BaseWidget.Hide()
|
||||
}
|
||||
|
||||
// MinSize returns the size that this widget should not shrink below
|
||||
//
|
||||
// Implements: fyne.CanvasObject
|
||||
func (t *AppTabs) MinSize() fyne.Size {
|
||||
t.BaseWidget.ExtendBaseWidget(t)
|
||||
return t.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
// Remove tab by value.
|
||||
func (t *AppTabs) Remove(item *TabItem) {
|
||||
removeItem(t, item)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// RemoveIndex removes tab by index.
|
||||
func (t *AppTabs) RemoveIndex(index int) {
|
||||
removeIndex(t, index)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// Select sets the specified TabItem to be selected and its content visible.
|
||||
func (t *AppTabs) Select(item *TabItem) {
|
||||
selectItem(t, item)
|
||||
}
|
||||
|
||||
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
|
||||
func (t *AppTabs) SelectIndex(index int) {
|
||||
selectIndex(t, index)
|
||||
}
|
||||
|
||||
// SelectTab sets the specified TabItem to be selected and its content visible.
|
||||
//
|
||||
// Deprecated: Use `AppTabs.Select(*TabItem)` instead.
|
||||
func (t *AppTabs) SelectTab(item *TabItem) {
|
||||
for i, child := range t.Items {
|
||||
if child == item {
|
||||
t.SelectTabIndex(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SelectTabIndex sets the TabItem at the specific index to be selected and its content visible.
|
||||
//
|
||||
// Deprecated: Use `AppTabs.SelectIndex(int)` instead.
|
||||
func (t *AppTabs) SelectTabIndex(index int) {
|
||||
if index < 0 || index >= len(t.Items) || t.current == index {
|
||||
return
|
||||
}
|
||||
t.current = index
|
||||
t.Refresh()
|
||||
|
||||
if t.OnChanged != nil {
|
||||
t.OnChanged(t.Items[t.current])
|
||||
}
|
||||
}
|
||||
|
||||
// Selected returns the currently selected TabItem.
|
||||
func (t *AppTabs) Selected() *TabItem {
|
||||
return selected(t)
|
||||
}
|
||||
|
||||
// SelectedIndex returns the index of the currently selected TabItem.
|
||||
func (t *AppTabs) SelectedIndex() int {
|
||||
return t.current
|
||||
}
|
||||
|
||||
// SetItems sets the containers items and refreshes.
|
||||
func (t *AppTabs) SetItems(items []*TabItem) {
|
||||
setItems(t, items)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// SetTabLocation sets the location of the tab bar
|
||||
func (t *AppTabs) SetTabLocation(l TabLocation) {
|
||||
t.location = tabsAdjustedLocation(l, t)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// Show this widget, if it was previously hidden
|
||||
//
|
||||
// Implements: fyne.CanvasObject
|
||||
func (t *AppTabs) Show() {
|
||||
t.BaseWidget.Show()
|
||||
t.SelectIndex(t.current)
|
||||
}
|
||||
|
||||
func (t *AppTabs) onUnselected() func(*TabItem) {
|
||||
return t.OnUnselected
|
||||
}
|
||||
|
||||
func (t *AppTabs) onSelected() func(*TabItem) {
|
||||
return func(tab *TabItem) {
|
||||
if f := t.OnChanged; f != nil {
|
||||
f(tab)
|
||||
}
|
||||
if f := t.OnSelected; f != nil {
|
||||
f(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AppTabs) items() []*TabItem {
|
||||
return t.Items
|
||||
}
|
||||
|
||||
func (t *AppTabs) selected() int {
|
||||
return t.current
|
||||
}
|
||||
|
||||
func (t *AppTabs) setItems(items []*TabItem) {
|
||||
t.Items = items
|
||||
}
|
||||
|
||||
func (t *AppTabs) setSelected(selected int) {
|
||||
t.current = selected
|
||||
}
|
||||
|
||||
func (t *AppTabs) setTransitioning(transitioning bool) {
|
||||
t.isTransitioning = transitioning
|
||||
}
|
||||
|
||||
func (t *AppTabs) tabLocation() TabLocation {
|
||||
return t.location
|
||||
}
|
||||
|
||||
func (t *AppTabs) transitioning() bool {
|
||||
return t.isTransitioning
|
||||
}
|
||||
|
||||
// Declare conformity with WidgetRenderer interface.
|
||||
var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil)
|
||||
|
||||
type appTabsRenderer struct {
|
||||
baseTabsRenderer
|
||||
appTabs *AppTabs
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) Layout(size fyne.Size) {
|
||||
// Try render as many tabs as will fit, others will appear in the overflow
|
||||
if len(r.appTabs.Items) == 0 {
|
||||
r.updateTabs(0)
|
||||
} else {
|
||||
for i := len(r.appTabs.Items); i > 0; i-- {
|
||||
r.updateTabs(i)
|
||||
barMin := r.bar.MinSize()
|
||||
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
|
||||
if barMin.Height <= size.Height {
|
||||
// Tab bar is short enough to fit
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if barMin.Width <= size.Width {
|
||||
// Tab bar is thin enough to fit
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.layout(r.appTabs, size)
|
||||
r.updateIndicator(r.appTabs.transitioning())
|
||||
if r.appTabs.transitioning() {
|
||||
r.appTabs.setTransitioning(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) MinSize() fyne.Size {
|
||||
return r.minSize(r.appTabs)
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects(r.appTabs)
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) Refresh() {
|
||||
r.Layout(r.appTabs.Size())
|
||||
|
||||
r.refresh(r.appTabs)
|
||||
|
||||
canvas.Refresh(r.appTabs)
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) {
|
||||
overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() {
|
||||
// Show pop up containing all tabs which did not fit in the tab bar
|
||||
|
||||
itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects)
|
||||
items := make([]*fyne.MenuItem, 0, itemLen-objLen)
|
||||
for i := objLen; i < itemLen; i++ {
|
||||
index := i // capture
|
||||
// FIXME MenuItem doesn't support icons (#1752)
|
||||
// FIXME MenuItem can't show if it is the currently selected tab (#1753)
|
||||
ti := r.appTabs.Items[i]
|
||||
mi := fyne.NewMenuItem(ti.Text, func() {
|
||||
r.appTabs.SelectIndex(index)
|
||||
if r.appTabs.popUpMenu != nil {
|
||||
r.appTabs.popUpMenu.Hide()
|
||||
r.appTabs.popUpMenu = nil
|
||||
}
|
||||
})
|
||||
if ti.Disabled() {
|
||||
mi.Disabled = true
|
||||
}
|
||||
items = append(items, mi)
|
||||
}
|
||||
|
||||
r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items)
|
||||
}}
|
||||
|
||||
return overflow
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
|
||||
buttons := &fyne.Container{}
|
||||
|
||||
var iconPos buttonIconPosition
|
||||
if isMobile(r.tabs) {
|
||||
cells := count
|
||||
if cells == 0 {
|
||||
cells = 1
|
||||
}
|
||||
if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom {
|
||||
buttons.Layout = layout.NewGridLayoutWithColumns(cells)
|
||||
} else {
|
||||
buttons.Layout = layout.NewGridLayoutWithRows(cells)
|
||||
}
|
||||
iconPos = buttonIconTop
|
||||
} else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
|
||||
buttons.Layout = layout.NewVBoxLayout()
|
||||
iconPos = buttonIconTop
|
||||
} else {
|
||||
buttons.Layout = layout.NewHBoxLayout()
|
||||
iconPos = buttonIconInline
|
||||
}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
item := r.appTabs.Items[i]
|
||||
if item.button == nil {
|
||||
item.button = &tabButton{
|
||||
onTapped: func() { r.appTabs.Select(item) },
|
||||
tabs: r.tabs,
|
||||
}
|
||||
}
|
||||
button := item.button
|
||||
button.icon = item.Icon
|
||||
button.iconPosition = iconPos
|
||||
if i == r.appTabs.current {
|
||||
button.importance = widget.HighImportance
|
||||
} else {
|
||||
button.importance = widget.MediumImportance
|
||||
}
|
||||
button.text = item.Text
|
||||
button.textAlignment = fyne.TextAlignCenter
|
||||
button.Refresh()
|
||||
buttons.Objects = append(buttons.Objects, button)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) updateIndicator(animate bool) {
|
||||
if r.appTabs.current < 0 {
|
||||
r.indicator.Hide()
|
||||
return
|
||||
}
|
||||
r.indicator.Show()
|
||||
|
||||
var selectedPos fyne.Position
|
||||
var selectedSize fyne.Size
|
||||
|
||||
buttons := r.bar.Objects[0].(*fyne.Container).Objects
|
||||
if r.appTabs.current >= len(buttons) {
|
||||
if a := r.action; a != nil {
|
||||
selectedPos = a.Position()
|
||||
selectedSize = a.Size()
|
||||
}
|
||||
} else {
|
||||
selected := buttons[r.appTabs.current]
|
||||
selectedPos = selected.Position()
|
||||
selectedSize = selected.Size()
|
||||
}
|
||||
|
||||
var indicatorPos fyne.Position
|
||||
var indicatorSize fyne.Size
|
||||
th := r.appTabs.Theme()
|
||||
pad := th.Size(theme.SizeNamePadding)
|
||||
|
||||
switch r.appTabs.location {
|
||||
case TabLocationTop:
|
||||
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height)
|
||||
indicatorSize = fyne.NewSize(selectedSize.Width, pad)
|
||||
case TabLocationLeading:
|
||||
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y)
|
||||
indicatorSize = fyne.NewSize(pad, selectedSize.Height)
|
||||
case TabLocationBottom:
|
||||
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-pad)
|
||||
indicatorSize = fyne.NewSize(selectedSize.Width, pad)
|
||||
case TabLocationTrailing:
|
||||
indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y)
|
||||
indicatorSize = fyne.NewSize(pad, selectedSize.Height)
|
||||
}
|
||||
|
||||
r.moveIndicator(indicatorPos, indicatorSize, th, animate)
|
||||
}
|
||||
|
||||
func (r *appTabsRenderer) updateTabs(max int) {
|
||||
tabCount := len(r.appTabs.Items)
|
||||
|
||||
// Set overflow action
|
||||
if tabCount <= max {
|
||||
r.action.Hide()
|
||||
r.bar.Layout = layout.NewStackLayout()
|
||||
} else {
|
||||
tabCount = max
|
||||
r.action.Show()
|
||||
|
||||
// Set layout of tab bar containing tab buttons and overflow action
|
||||
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
|
||||
r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil)
|
||||
} else {
|
||||
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action)
|
||||
}
|
||||
}
|
||||
|
||||
buttons := r.buildTabButtons(tabCount)
|
||||
|
||||
r.bar.Objects = []fyne.CanvasObject{buttons}
|
||||
if a := r.action; a != nil {
|
||||
r.bar.Objects = append(r.bar.Objects, a)
|
||||
}
|
||||
|
||||
r.bar.Refresh()
|
||||
}
|
||||
20
vendor/fyne.io/fyne/v2/container/container.go
generated
vendored
Normal file
20
vendor/fyne.io/fyne/v2/container/container.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
// Package container provides containers that are used to lay out and organise applications.
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// New returns a new Container instance holding the specified CanvasObjects which will be laid out according to the specified Layout.
|
||||
//
|
||||
// Since: 2.0
|
||||
func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return &fyne.Container{Layout: layout, Objects: objects}
|
||||
}
|
||||
|
||||
// NewWithoutLayout returns a new Container instance holding the specified CanvasObjects that are manually arranged.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewWithoutLayout(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return &fyne.Container{Objects: objects}
|
||||
}
|
||||
492
vendor/fyne.io/fyne/v2/container/doctabs.go
generated
vendored
Normal file
492
vendor/fyne.io/fyne/v2/container/doctabs.go
generated
vendored
Normal file
@ -0,0 +1,492 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// Declare conformity with Widget interface.
|
||||
var _ fyne.Widget = (*DocTabs)(nil)
|
||||
|
||||
// DocTabs container is used to display various pieces of content identified by tabs.
|
||||
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
|
||||
// Each item is represented by a button at the edge of the container.
|
||||
//
|
||||
// Since: 2.1
|
||||
type DocTabs struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Items []*TabItem
|
||||
|
||||
CreateTab func() *TabItem `json:"-"`
|
||||
CloseIntercept func(*TabItem) `json:"-"`
|
||||
OnClosed func(*TabItem) `json:"-"`
|
||||
OnSelected func(*TabItem) `json:"-"`
|
||||
OnUnselected func(*TabItem) `json:"-"`
|
||||
|
||||
current int
|
||||
location TabLocation
|
||||
isTransitioning bool
|
||||
|
||||
popUpMenu *widget.PopUpMenu
|
||||
}
|
||||
|
||||
// NewDocTabs creates a new tab container that allows the user to choose between various pieces of content.
|
||||
//
|
||||
// Since: 2.1
|
||||
func NewDocTabs(items ...*TabItem) *DocTabs {
|
||||
tabs := &DocTabs{}
|
||||
tabs.ExtendBaseWidget(tabs)
|
||||
tabs.SetItems(items)
|
||||
return tabs
|
||||
}
|
||||
|
||||
// Append adds a new TabItem to the end of the tab bar.
|
||||
func (t *DocTabs) Append(item *TabItem) {
|
||||
t.SetItems(append(t.Items, item))
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||||
//
|
||||
// Implements: fyne.Widget
|
||||
func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer {
|
||||
t.ExtendBaseWidget(t)
|
||||
th := t.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
r := &docTabsRenderer{
|
||||
baseTabsRenderer: baseTabsRenderer{
|
||||
bar: &fyne.Container{},
|
||||
divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)),
|
||||
indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)),
|
||||
},
|
||||
docTabs: t,
|
||||
scroller: NewScroll(&fyne.Container{}),
|
||||
}
|
||||
r.action = r.buildAllTabsButton()
|
||||
r.create = r.buildCreateTabsButton()
|
||||
r.tabs = t
|
||||
|
||||
r.box = NewHBox(r.create, r.action)
|
||||
r.scroller.OnScrolled = func(offset fyne.Position) {
|
||||
r.updateIndicator(false)
|
||||
}
|
||||
r.updateAllTabs()
|
||||
r.updateCreateTab()
|
||||
r.updateTabs()
|
||||
r.updateIndicator(false)
|
||||
r.applyTheme(t)
|
||||
return r
|
||||
}
|
||||
|
||||
// DisableIndex disables the TabItem at the specified index.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *DocTabs) DisableIndex(i int) {
|
||||
disableIndex(t, i)
|
||||
}
|
||||
|
||||
// DisableItem disables the specified TabItem.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *DocTabs) DisableItem(item *TabItem) {
|
||||
disableItem(t, item)
|
||||
}
|
||||
|
||||
// EnableIndex enables the TabItem at the specified index.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *DocTabs) EnableIndex(i int) {
|
||||
enableIndex(t, i)
|
||||
}
|
||||
|
||||
// EnableItem enables the specified TabItem.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (t *DocTabs) EnableItem(item *TabItem) {
|
||||
enableItem(t, item)
|
||||
}
|
||||
|
||||
// Hide hides the widget.
|
||||
//
|
||||
// Implements: fyne.CanvasObject
|
||||
func (t *DocTabs) Hide() {
|
||||
if t.popUpMenu != nil {
|
||||
t.popUpMenu.Hide()
|
||||
t.popUpMenu = nil
|
||||
}
|
||||
t.BaseWidget.Hide()
|
||||
}
|
||||
|
||||
// MinSize returns the size that this widget should not shrink below
|
||||
//
|
||||
// Implements: fyne.CanvasObject
|
||||
func (t *DocTabs) MinSize() fyne.Size {
|
||||
t.ExtendBaseWidget(t)
|
||||
return t.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
// Remove tab by value.
|
||||
func (t *DocTabs) Remove(item *TabItem) {
|
||||
removeItem(t, item)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// RemoveIndex removes tab by index.
|
||||
func (t *DocTabs) RemoveIndex(index int) {
|
||||
removeIndex(t, index)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// Select sets the specified TabItem to be selected and its content visible.
|
||||
func (t *DocTabs) Select(item *TabItem) {
|
||||
selectItem(t, item)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
|
||||
func (t *DocTabs) SelectIndex(index int) {
|
||||
selectIndex(t, index)
|
||||
}
|
||||
|
||||
// Selected returns the currently selected TabItem.
|
||||
func (t *DocTabs) Selected() *TabItem {
|
||||
return selected(t)
|
||||
}
|
||||
|
||||
// SelectedIndex returns the index of the currently selected TabItem.
|
||||
func (t *DocTabs) SelectedIndex() int {
|
||||
return t.current
|
||||
}
|
||||
|
||||
// SetItems sets the containers items and refreshes.
|
||||
func (t *DocTabs) SetItems(items []*TabItem) {
|
||||
setItems(t, items)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// SetTabLocation sets the location of the tab bar
|
||||
func (t *DocTabs) SetTabLocation(l TabLocation) {
|
||||
t.location = tabsAdjustedLocation(l, t)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// Show this widget, if it was previously hidden
|
||||
//
|
||||
// Implements: fyne.CanvasObject
|
||||
func (t *DocTabs) Show() {
|
||||
t.BaseWidget.Show()
|
||||
t.SelectIndex(t.current)
|
||||
}
|
||||
|
||||
func (t *DocTabs) close(item *TabItem) {
|
||||
if f := t.CloseIntercept; f != nil {
|
||||
f(item)
|
||||
} else {
|
||||
t.Remove(item)
|
||||
if f := t.OnClosed; f != nil {
|
||||
f(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DocTabs) onUnselected() func(*TabItem) {
|
||||
return t.OnUnselected
|
||||
}
|
||||
|
||||
func (t *DocTabs) onSelected() func(*TabItem) {
|
||||
return t.OnSelected
|
||||
}
|
||||
|
||||
func (t *DocTabs) items() []*TabItem {
|
||||
return t.Items
|
||||
}
|
||||
|
||||
func (t *DocTabs) selected() int {
|
||||
return t.current
|
||||
}
|
||||
|
||||
func (t *DocTabs) setItems(items []*TabItem) {
|
||||
t.Items = items
|
||||
}
|
||||
|
||||
func (t *DocTabs) setSelected(selected int) {
|
||||
t.current = selected
|
||||
}
|
||||
|
||||
func (t *DocTabs) setTransitioning(transitioning bool) {
|
||||
t.isTransitioning = transitioning
|
||||
}
|
||||
|
||||
func (t *DocTabs) tabLocation() TabLocation {
|
||||
return t.location
|
||||
}
|
||||
|
||||
func (t *DocTabs) transitioning() bool {
|
||||
return t.isTransitioning
|
||||
}
|
||||
|
||||
// Declare conformity with WidgetRenderer interface.
|
||||
var _ fyne.WidgetRenderer = (*docTabsRenderer)(nil)
|
||||
|
||||
type docTabsRenderer struct {
|
||||
baseTabsRenderer
|
||||
docTabs *DocTabs
|
||||
scroller *Scroll
|
||||
box *fyne.Container
|
||||
create *widget.Button
|
||||
lastSelected int
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) Layout(size fyne.Size) {
|
||||
r.updateAllTabs()
|
||||
r.updateCreateTab()
|
||||
r.updateTabs()
|
||||
r.layout(r.docTabs, size)
|
||||
|
||||
// lay out buttons before updating indicator, which is relative to their position
|
||||
buttons := r.scroller.Content.(*fyne.Container)
|
||||
buttons.Layout.Layout(buttons.Objects, buttons.Size())
|
||||
r.updateIndicator(r.docTabs.transitioning())
|
||||
|
||||
if r.docTabs.transitioning() {
|
||||
r.docTabs.setTransitioning(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) MinSize() fyne.Size {
|
||||
return r.minSize(r.docTabs)
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects(r.docTabs)
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) Refresh() {
|
||||
r.Layout(r.docTabs.Size())
|
||||
|
||||
if c := r.docTabs.current; c != r.lastSelected {
|
||||
if c >= 0 && c < len(r.docTabs.Items) {
|
||||
r.scrollToSelected()
|
||||
}
|
||||
r.lastSelected = c
|
||||
}
|
||||
|
||||
r.refresh(r.docTabs)
|
||||
|
||||
canvas.Refresh(r.docTabs)
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) buildAllTabsButton() (all *widget.Button) {
|
||||
all = &widget.Button{Importance: widget.LowImportance, OnTapped: func() {
|
||||
// Show pop up containing all tabs
|
||||
|
||||
items := make([]*fyne.MenuItem, len(r.docTabs.Items))
|
||||
for i := 0; i < len(r.docTabs.Items); i++ {
|
||||
index := i // capture
|
||||
// FIXME MenuItem doesn't support icons (#1752)
|
||||
items[i] = fyne.NewMenuItem(r.docTabs.Items[i].Text, func() {
|
||||
r.docTabs.SelectIndex(index)
|
||||
if r.docTabs.popUpMenu != nil {
|
||||
r.docTabs.popUpMenu.Hide()
|
||||
r.docTabs.popUpMenu = nil
|
||||
}
|
||||
})
|
||||
items[i].Checked = index == r.docTabs.current
|
||||
}
|
||||
|
||||
r.docTabs.popUpMenu = buildPopUpMenu(r.docTabs, all, items)
|
||||
}}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) buildCreateTabsButton() *widget.Button {
|
||||
create := widget.NewButton("", func() {
|
||||
if f := r.docTabs.CreateTab; f != nil {
|
||||
if tab := f(); tab != nil {
|
||||
r.docTabs.Append(tab)
|
||||
r.docTabs.SelectIndex(len(r.docTabs.Items) - 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
create.Importance = widget.LowImportance
|
||||
return create
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) {
|
||||
buttons.Objects = nil
|
||||
|
||||
var iconPos buttonIconPosition
|
||||
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
||||
buttons.Layout = layout.NewVBoxLayout()
|
||||
iconPos = buttonIconTop
|
||||
} else {
|
||||
buttons.Layout = layout.NewHBoxLayout()
|
||||
iconPos = buttonIconInline
|
||||
}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
item := r.docTabs.Items[i]
|
||||
if item.button == nil {
|
||||
item.button = &tabButton{
|
||||
onTapped: func() { r.docTabs.Select(item) },
|
||||
onClosed: func() { r.docTabs.close(item) },
|
||||
tabs: r.tabs,
|
||||
}
|
||||
}
|
||||
button := item.button
|
||||
button.icon = item.Icon
|
||||
button.iconPosition = iconPos
|
||||
if i == r.docTabs.current {
|
||||
button.importance = widget.HighImportance
|
||||
} else {
|
||||
button.importance = widget.MediumImportance
|
||||
}
|
||||
button.text = item.Text
|
||||
button.textAlignment = fyne.TextAlignLeading
|
||||
button.Refresh()
|
||||
buttons.Objects = append(buttons.Objects, button)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) scrollToSelected() {
|
||||
buttons := r.scroller.Content.(*fyne.Container)
|
||||
|
||||
// https://github.com/fyne-io/fyne/issues/3909
|
||||
// very dirty temporary fix to this crash!
|
||||
if r.docTabs.current < 0 || r.docTabs.current >= len(buttons.Objects) {
|
||||
return
|
||||
}
|
||||
|
||||
button := buttons.Objects[r.docTabs.current]
|
||||
pos := button.Position()
|
||||
size := button.Size()
|
||||
offset := r.scroller.Offset
|
||||
viewport := r.scroller.Size()
|
||||
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
||||
if pos.Y < offset.Y {
|
||||
offset.Y = pos.Y
|
||||
} else if pos.Y+size.Height > offset.Y+viewport.Height {
|
||||
offset.Y = pos.Y + size.Height - viewport.Height
|
||||
}
|
||||
} else {
|
||||
if pos.X < offset.X {
|
||||
offset.X = pos.X
|
||||
} else if pos.X+size.Width > offset.X+viewport.Width {
|
||||
offset.X = pos.X + size.Width - viewport.Width
|
||||
}
|
||||
}
|
||||
r.scroller.Offset = offset
|
||||
r.updateIndicator(false)
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) updateIndicator(animate bool) {
|
||||
th := r.docTabs.Theme()
|
||||
if r.docTabs.current < 0 {
|
||||
r.indicator.FillColor = color.Transparent
|
||||
r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), th, animate)
|
||||
return
|
||||
}
|
||||
|
||||
var selectedPos fyne.Position
|
||||
var selectedSize fyne.Size
|
||||
|
||||
buttons := r.scroller.Content.(*fyne.Container).Objects
|
||||
|
||||
if r.docTabs.current >= len(buttons) {
|
||||
if a := r.action; a != nil {
|
||||
selectedPos = a.Position()
|
||||
selectedSize = a.Size()
|
||||
minSize := a.MinSize()
|
||||
if minSize.Width > selectedSize.Width {
|
||||
selectedSize = minSize
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selected := buttons[r.docTabs.current]
|
||||
selectedPos = selected.Position()
|
||||
selectedSize = selected.Size()
|
||||
minSize := selected.MinSize()
|
||||
if minSize.Width > selectedSize.Width {
|
||||
selectedSize = minSize
|
||||
}
|
||||
}
|
||||
|
||||
scrollOffset := r.scroller.Offset
|
||||
scrollSize := r.scroller.Size()
|
||||
|
||||
var indicatorPos fyne.Position
|
||||
var indicatorSize fyne.Size
|
||||
pad := th.Size(theme.SizeNamePadding)
|
||||
|
||||
switch r.docTabs.location {
|
||||
case TabLocationTop:
|
||||
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height)
|
||||
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad)
|
||||
case TabLocationLeading:
|
||||
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y)
|
||||
indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
|
||||
case TabLocationBottom:
|
||||
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-pad)
|
||||
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad)
|
||||
case TabLocationTrailing:
|
||||
indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y-scrollOffset.Y)
|
||||
indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
|
||||
}
|
||||
|
||||
if indicatorPos.X < 0 {
|
||||
indicatorSize.Width = indicatorSize.Width + indicatorPos.X
|
||||
indicatorPos.X = 0
|
||||
}
|
||||
if indicatorPos.Y < 0 {
|
||||
indicatorSize.Height = indicatorSize.Height + indicatorPos.Y
|
||||
indicatorPos.Y = 0
|
||||
}
|
||||
if indicatorSize.Width < 0 || indicatorSize.Height < 0 {
|
||||
r.indicator.FillColor = color.Transparent
|
||||
r.indicator.Refresh()
|
||||
return
|
||||
}
|
||||
|
||||
r.moveIndicator(indicatorPos, indicatorSize, th, animate)
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) updateAllTabs() {
|
||||
if len(r.docTabs.Items) > 0 {
|
||||
r.action.Show()
|
||||
} else {
|
||||
r.action.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) updateCreateTab() {
|
||||
if r.docTabs.CreateTab != nil {
|
||||
r.create.SetIcon(theme.ContentAddIcon())
|
||||
r.create.Show()
|
||||
} else {
|
||||
r.create.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *docTabsRenderer) updateTabs() {
|
||||
tabCount := len(r.docTabs.Items)
|
||||
r.buildTabButtons(tabCount, r.scroller.Content.(*fyne.Container))
|
||||
|
||||
// Set layout of tab bar containing tab buttons and overflow action
|
||||
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
||||
r.bar.Layout = layout.NewBorderLayout(nil, r.box, nil, nil)
|
||||
r.scroller.Direction = ScrollVerticalOnly
|
||||
} else {
|
||||
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.box)
|
||||
r.scroller.Direction = ScrollHorizontalOnly
|
||||
}
|
||||
|
||||
r.bar.Objects = []fyne.CanvasObject{r.scroller, r.box}
|
||||
r.bar.Refresh()
|
||||
}
|
||||
442
vendor/fyne.io/fyne/v2/container/innerwindow.go
generated
vendored
Normal file
442
vendor/fyne.io/fyne/v2/container/innerwindow.go
generated
vendored
Normal file
@ -0,0 +1,442 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
intWidget "fyne.io/fyne/v2/internal/widget"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type titleBarButtonMode int
|
||||
|
||||
const (
|
||||
modeClose titleBarButtonMode = iota
|
||||
modeMinimize
|
||||
modeMaximize
|
||||
modeIcon
|
||||
)
|
||||
|
||||
var _ fyne.Widget = (*InnerWindow)(nil)
|
||||
|
||||
// InnerWindow defines a container that wraps content in a window border - that can then be placed inside
|
||||
// a regular container/canvas.
|
||||
//
|
||||
// Since: 2.5
|
||||
type InnerWindow struct {
|
||||
widget.BaseWidget
|
||||
|
||||
CloseIntercept func() `json:"-"`
|
||||
OnDragged, OnResized func(*fyne.DragEvent) `json:"-"`
|
||||
OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func() `json:"-"`
|
||||
Icon fyne.Resource
|
||||
|
||||
// Alignment allows an inner window to specify if the buttons should be on the left
|
||||
// (`ButtonAlignLeading`) or right of the window border.
|
||||
//
|
||||
// Since: 2.6
|
||||
Alignment widget.ButtonAlign
|
||||
|
||||
title string
|
||||
content *fyne.Container
|
||||
maximized bool
|
||||
}
|
||||
|
||||
// NewInnerWindow creates a new window border around the given `content`, displaying the `title` along the top.
|
||||
// This will behave like a normal contain and will probably want to be added to a `MultipleWindows` parent.
|
||||
//
|
||||
// Since: 2.5
|
||||
func NewInnerWindow(title string, content fyne.CanvasObject) *InnerWindow {
|
||||
w := &InnerWindow{title: title, content: NewPadded(content)}
|
||||
w.ExtendBaseWidget(w)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *InnerWindow) Close() {
|
||||
w.Hide()
|
||||
}
|
||||
|
||||
func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
|
||||
w.ExtendBaseWidget(w)
|
||||
th := w.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
min := newBorderButton(theme.WindowMinimizeIcon(), modeMinimize, th, w.OnMinimized)
|
||||
if w.OnMinimized == nil {
|
||||
min.Disable()
|
||||
}
|
||||
max := newBorderButton(theme.WindowMaximizeIcon(), modeMaximize, th, w.OnMaximized)
|
||||
if w.OnMaximized == nil {
|
||||
max.Disable()
|
||||
}
|
||||
|
||||
close := newBorderButton(theme.WindowCloseIcon(), modeClose, th, func() {
|
||||
if f := w.CloseIntercept; f != nil {
|
||||
f()
|
||||
} else {
|
||||
w.Close()
|
||||
}
|
||||
})
|
||||
buttons := NewCenter(NewHBox(close, min, max))
|
||||
|
||||
borderIcon := newBorderButton(w.Icon, modeIcon, th, func() {
|
||||
if f := w.OnTappedIcon; f != nil {
|
||||
f()
|
||||
}
|
||||
})
|
||||
if w.OnTappedIcon == nil {
|
||||
borderIcon.Disable()
|
||||
}
|
||||
|
||||
if w.Icon == nil {
|
||||
borderIcon.Hide()
|
||||
}
|
||||
title := newDraggableLabel(w.title, w)
|
||||
title.Truncation = fyne.TextTruncateEllipsis
|
||||
|
||||
height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight)
|
||||
off := (height - title.labelMinSize().Height) / 2
|
||||
barMid := New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title)
|
||||
if w.buttonPosition() == widget.ButtonAlignTrailing {
|
||||
buttons = NewCenter(NewHBox(min, max, close))
|
||||
}
|
||||
|
||||
bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
|
||||
contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v))
|
||||
corner := newDraggableCorner(w)
|
||||
bar := New(&titleBarLayout{buttons: buttons, icon: borderIcon, title: barMid, win: w},
|
||||
buttons, borderIcon, barMid)
|
||||
|
||||
if w.content == nil {
|
||||
w.content = NewPadded(canvas.NewRectangle(color.Transparent))
|
||||
}
|
||||
objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner}
|
||||
r := &innerWindowRenderer{ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel),
|
||||
win: w, bar: bar, buttonBox: buttons, buttons: []*borderButton{close, min, max}, bg: bg,
|
||||
corner: corner, contentBG: contentBG, icon: borderIcon}
|
||||
r.Layout(w.Size())
|
||||
return r
|
||||
}
|
||||
|
||||
func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
|
||||
w.content.Objects[0] = obj
|
||||
|
||||
w.content.Refresh()
|
||||
}
|
||||
|
||||
// SetMaximized tells the window if the maximized state should be set or not.
|
||||
//
|
||||
// Since: 2.6
|
||||
func (w *InnerWindow) SetMaximized(max bool) {
|
||||
w.maximized = max
|
||||
w.Refresh()
|
||||
}
|
||||
|
||||
func (w *InnerWindow) SetPadded(pad bool) {
|
||||
if pad {
|
||||
w.content.Layout = layout.NewPaddedLayout()
|
||||
} else {
|
||||
w.content.Layout = layout.NewStackLayout()
|
||||
}
|
||||
w.content.Refresh()
|
||||
}
|
||||
|
||||
func (w *InnerWindow) SetTitle(title string) {
|
||||
w.title = title
|
||||
w.Refresh()
|
||||
}
|
||||
|
||||
func (w *InnerWindow) buttonPosition() widget.ButtonAlign {
|
||||
if w.Alignment != widget.ButtonAlignCenter {
|
||||
return w.Alignment
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "linux" || strings.Contains(runtime.GOOS, "bsd") {
|
||||
return widget.ButtonAlignTrailing
|
||||
}
|
||||
// macOS
|
||||
return widget.ButtonAlignLeading
|
||||
}
|
||||
|
||||
var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil)
|
||||
|
||||
type innerWindowRenderer struct {
|
||||
*intWidget.ShadowingRenderer
|
||||
|
||||
win *InnerWindow
|
||||
bar, buttonBox *fyne.Container
|
||||
buttons []*borderButton
|
||||
icon *borderButton
|
||||
bg, contentBG *canvas.Rectangle
|
||||
corner fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (i *innerWindowRenderer) Layout(size fyne.Size) {
|
||||
th := i.win.Theme()
|
||||
pad := th.Size(theme.SizeNamePadding)
|
||||
|
||||
i.LayoutShadow(size, fyne.Position{})
|
||||
i.bg.Resize(size)
|
||||
|
||||
barHeight := i.win.Theme().Size(theme.SizeNameWindowTitleBarHeight)
|
||||
i.bar.Move(fyne.NewPos(pad, 0))
|
||||
i.bar.Resize(fyne.NewSize(size.Width-pad*2, barHeight))
|
||||
|
||||
innerPos := fyne.NewPos(pad, barHeight)
|
||||
innerSize := fyne.NewSize(size.Width-pad*2, size.Height-pad-barHeight)
|
||||
i.contentBG.Move(innerPos)
|
||||
i.contentBG.Resize(innerSize)
|
||||
i.win.content.Move(innerPos)
|
||||
i.win.content.Resize(innerSize)
|
||||
|
||||
cornerSize := i.corner.MinSize()
|
||||
i.corner.Move(fyne.NewPos(size.Components()).Subtract(cornerSize).AddXY(1, 1))
|
||||
i.corner.Resize(cornerSize)
|
||||
}
|
||||
|
||||
func (i *innerWindowRenderer) MinSize() fyne.Size {
|
||||
th := i.win.Theme()
|
||||
pad := th.Size(theme.SizeNamePadding)
|
||||
contentMin := i.win.content.MinSize()
|
||||
barHeight := th.Size(theme.SizeNameWindowTitleBarHeight)
|
||||
|
||||
innerWidth := fyne.Max(i.bar.MinSize().Width, contentMin.Width)
|
||||
|
||||
return fyne.NewSize(innerWidth+pad*2, contentMin.Height+pad+barHeight)
|
||||
}
|
||||
|
||||
func (i *innerWindowRenderer) Refresh() {
|
||||
th := i.win.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
i.bg.FillColor = th.Color(theme.ColorNameOverlayBackground, v)
|
||||
i.bg.Refresh()
|
||||
i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v)
|
||||
i.contentBG.Refresh()
|
||||
|
||||
if i.win.buttonPosition() == widget.ButtonAlignTrailing {
|
||||
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[1], i.buttons[2], i.buttons[0]}
|
||||
} else {
|
||||
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[0], i.buttons[1], i.buttons[2]}
|
||||
}
|
||||
for _, b := range i.buttons {
|
||||
b.setTheme(th)
|
||||
}
|
||||
i.bar.Refresh()
|
||||
|
||||
if i.win.OnMinimized == nil {
|
||||
i.buttons[1].Disable()
|
||||
} else {
|
||||
i.buttons[1].SetOnTapped(i.win.OnMinimized)
|
||||
i.buttons[1].Enable()
|
||||
}
|
||||
|
||||
max := i.buttons[2]
|
||||
if i.win.OnMaximized == nil {
|
||||
i.buttons[2].Disable()
|
||||
} else {
|
||||
max.SetOnTapped(i.win.OnMaximized)
|
||||
max.Enable()
|
||||
}
|
||||
if i.win.maximized {
|
||||
max.b.SetIcon(theme.ViewRestoreIcon())
|
||||
} else {
|
||||
max.b.SetIcon(theme.WindowMaximizeIcon())
|
||||
}
|
||||
|
||||
title := i.bar.Objects[2].(*fyne.Container).Objects[0].(*draggableLabel)
|
||||
title.SetText(i.win.title)
|
||||
i.ShadowingRenderer.RefreshShadow()
|
||||
if i.win.OnTappedIcon == nil {
|
||||
i.icon.Disable()
|
||||
} else {
|
||||
i.icon.Enable()
|
||||
}
|
||||
if i.win.Icon != nil {
|
||||
i.icon.b.SetIcon(i.win.Icon)
|
||||
i.icon.Show()
|
||||
} else {
|
||||
i.icon.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
type draggableLabel struct {
|
||||
widget.Label
|
||||
win *InnerWindow
|
||||
}
|
||||
|
||||
func newDraggableLabel(title string, win *InnerWindow) *draggableLabel {
|
||||
d := &draggableLabel{win: win}
|
||||
d.ExtendBaseWidget(d)
|
||||
d.Text = title
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *draggableLabel) Dragged(ev *fyne.DragEvent) {
|
||||
if f := d.win.OnDragged; f != nil {
|
||||
f(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *draggableLabel) DragEnd() {
|
||||
}
|
||||
|
||||
func (d *draggableLabel) MinSize() fyne.Size {
|
||||
width := d.Label.MinSize().Width
|
||||
height := d.Label.Theme().Size(theme.SizeNameWindowButtonHeight)
|
||||
return fyne.NewSize(width, height)
|
||||
}
|
||||
|
||||
func (d *draggableLabel) Tapped(_ *fyne.PointEvent) {
|
||||
if f := d.win.OnTappedBar; f != nil {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *draggableLabel) labelMinSize() fyne.Size {
|
||||
return d.Label.MinSize()
|
||||
}
|
||||
|
||||
type draggableCorner struct {
|
||||
widget.BaseWidget
|
||||
win *InnerWindow
|
||||
}
|
||||
|
||||
func newDraggableCorner(w *InnerWindow) *draggableCorner {
|
||||
d := &draggableCorner{win: w}
|
||||
d.ExtendBaseWidget(d)
|
||||
return d
|
||||
}
|
||||
|
||||
func (c *draggableCorner) CreateRenderer() fyne.WidgetRenderer {
|
||||
prop := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon(theme.IconNameDragCornerIndicator))
|
||||
prop.SetMinSize(fyne.NewSquareSize(16))
|
||||
return widget.NewSimpleRenderer(prop)
|
||||
}
|
||||
|
||||
func (c *draggableCorner) Dragged(ev *fyne.DragEvent) {
|
||||
if f := c.win.OnResized; f != nil {
|
||||
c.win.OnResized(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *draggableCorner) DragEnd() {
|
||||
}
|
||||
|
||||
type borderButton struct {
|
||||
widget.BaseWidget
|
||||
|
||||
b *widget.Button
|
||||
c *ThemeOverride
|
||||
mode titleBarButtonMode
|
||||
}
|
||||
|
||||
func newBorderButton(icon fyne.Resource, mode titleBarButtonMode, th fyne.Theme, fn func()) *borderButton {
|
||||
buttonImportance := widget.MediumImportance
|
||||
if mode == modeIcon {
|
||||
buttonImportance = widget.LowImportance
|
||||
}
|
||||
b := &widget.Button{Icon: icon, Importance: buttonImportance, OnTapped: fn}
|
||||
c := NewThemeOverride(b, &buttonTheme{Theme: th, mode: mode})
|
||||
|
||||
ret := &borderButton{b: b, c: c, mode: mode}
|
||||
ret.ExtendBaseWidget(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *borderButton) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(b.c)
|
||||
}
|
||||
|
||||
func (b *borderButton) Disable() {
|
||||
b.b.Disable()
|
||||
}
|
||||
|
||||
func (b *borderButton) Enable() {
|
||||
b.b.Enable()
|
||||
}
|
||||
|
||||
func (b *borderButton) SetOnTapped(fn func()) {
|
||||
b.b.OnTapped = fn
|
||||
}
|
||||
|
||||
func (b *borderButton) MinSize() fyne.Size {
|
||||
height := b.Theme().Size(theme.SizeNameWindowButtonHeight)
|
||||
return fyne.NewSquareSize(height)
|
||||
}
|
||||
|
||||
func (b *borderButton) setTheme(th fyne.Theme) {
|
||||
b.c.Theme = &buttonTheme{Theme: th, mode: b.mode}
|
||||
}
|
||||
|
||||
type buttonTheme struct {
|
||||
fyne.Theme
|
||||
mode titleBarButtonMode
|
||||
}
|
||||
|
||||
func (b *buttonTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
|
||||
switch n {
|
||||
case theme.ColorNameHover:
|
||||
if b.mode == modeClose {
|
||||
n = theme.ColorNameError
|
||||
}
|
||||
}
|
||||
return b.Theme.Color(n, v)
|
||||
}
|
||||
|
||||
func (b *buttonTheme) Size(n fyne.ThemeSizeName) float32 {
|
||||
switch n {
|
||||
case theme.SizeNameInputRadius:
|
||||
if b.mode == modeIcon {
|
||||
return 0
|
||||
}
|
||||
n = theme.SizeNameWindowButtonRadius
|
||||
case theme.SizeNameInlineIcon:
|
||||
n = theme.SizeNameWindowButtonIcon
|
||||
}
|
||||
|
||||
return b.Theme.Size(n)
|
||||
}
|
||||
|
||||
type titleBarLayout struct {
|
||||
win *InnerWindow
|
||||
buttons, icon, title fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (t *titleBarLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
|
||||
buttonMinWidth := t.buttons.MinSize().Width
|
||||
t.buttons.Resize(fyne.NewSize(buttonMinWidth, s.Height))
|
||||
t.icon.Resize(fyne.NewSquareSize(s.Height))
|
||||
usedWidth := buttonMinWidth
|
||||
if t.icon.Visible() {
|
||||
usedWidth += s.Height
|
||||
}
|
||||
t.title.Resize(fyne.NewSize(s.Width-usedWidth, s.Height))
|
||||
|
||||
if t.win.buttonPosition() == widget.ButtonAlignTrailing {
|
||||
t.buttons.Move(fyne.NewPos(s.Width-buttonMinWidth, 0))
|
||||
t.icon.Move(fyne.Position{})
|
||||
if t.icon.Visible() {
|
||||
t.title.Move(fyne.NewPos(s.Height, 0))
|
||||
} else {
|
||||
t.title.Move(fyne.Position{})
|
||||
}
|
||||
} else {
|
||||
t.buttons.Move(fyne.NewPos(0, 0))
|
||||
t.icon.Move(fyne.NewPos(s.Width-s.Height, 0))
|
||||
t.title.Move(fyne.NewPos(buttonMinWidth, 0))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *titleBarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
|
||||
buttonMin := t.buttons.MinSize()
|
||||
iconMin := t.icon.MinSize()
|
||||
titleMin := t.title.MinSize() // can truncate
|
||||
|
||||
return fyne.NewSize(buttonMin.Width+iconMin.Width+titleMin.Width,
|
||||
fyne.Max(fyne.Max(buttonMin.Height, iconMin.Height), titleMin.Height))
|
||||
}
|
||||
124
vendor/fyne.io/fyne/v2/container/layouts.go
generated
vendored
Normal file
124
vendor/fyne.io/fyne/v2/container/layouts.go
generated
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
package container // import "fyne.io/fyne/v2/container"
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
)
|
||||
|
||||
// NewAdaptiveGrid creates a new container with the specified objects and using the grid layout.
|
||||
// When in a horizontal arrangement the rowcols parameter will specify the column count, when in vertical
|
||||
// it will specify the rows. On mobile this will dynamically refresh when device is rotated.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewAdaptiveGrid(rowcols int, objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewAdaptiveGridLayout(rowcols), objects...)
|
||||
}
|
||||
|
||||
// NewBorder creates a new container with the specified objects and using the border layout.
|
||||
// The top, bottom, left and right parameters specify the items that should be placed around edges.
|
||||
// Nil can be used to an edge if it should not be filled.
|
||||
// Passed objects not assigned to any edge (parameters 5 onwards) will be used to fill the space
|
||||
// remaining in the middle.
|
||||
// Parameters 6 onwards will be stacked over the middle content in the specified order as a Stack container.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewBorder(top, bottom, left, right fyne.CanvasObject, objects ...fyne.CanvasObject) *fyne.Container {
|
||||
all := objects
|
||||
if top != nil {
|
||||
all = append(all, top)
|
||||
}
|
||||
if bottom != nil {
|
||||
all = append(all, bottom)
|
||||
}
|
||||
if left != nil {
|
||||
all = append(all, left)
|
||||
}
|
||||
if right != nil {
|
||||
all = append(all, right)
|
||||
}
|
||||
|
||||
if len(objects) == 1 && objects[0] == nil {
|
||||
internal.LogHint("Border layout requires only 4 parameters, optional items cannot be nil")
|
||||
all = all[1:]
|
||||
}
|
||||
return New(layout.NewBorderLayout(top, bottom, left, right), all...)
|
||||
}
|
||||
|
||||
// NewCenter creates a new container with the specified objects centered in the available space.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewCenter(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewCenterLayout(), objects...)
|
||||
}
|
||||
|
||||
// NewGridWithColumns creates a new container with the specified objects and using the grid layout with
|
||||
// a specified number of columns. The number of rows will depend on how many children are in the container.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewGridWithColumns(cols int, objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewGridLayoutWithColumns(cols), objects...)
|
||||
}
|
||||
|
||||
// NewGridWithRows creates a new container with the specified objects and using the grid layout with
|
||||
// a specified number of rows. The number of columns will depend on how many children are in the container.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewGridWithRows(rows int, objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewGridLayoutWithRows(rows), objects...)
|
||||
}
|
||||
|
||||
// NewGridWrap creates a new container with the specified objects and using the gridwrap layout.
|
||||
// Every element will be resized to the size parameter and the content will arrange along a row and flow to a
|
||||
// new row if the elements don't fit.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewGridWrap(size fyne.Size, objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewGridWrapLayout(size), objects...)
|
||||
}
|
||||
|
||||
// NewHBox creates a new container with the specified objects and using the HBox layout.
|
||||
// The objects will be placed in the container from left to right and always displayed
|
||||
// at their horizontal MinSize. Use a different layout if the objects are intended
|
||||
// to be larger then their horizontal MinSize.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewHBox(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewHBoxLayout(), objects...)
|
||||
}
|
||||
|
||||
// NewMax creates a new container with the specified objects filling the available space.
|
||||
//
|
||||
// Since: 1.4
|
||||
//
|
||||
// Deprecated: Use container.NewStack() instead.
|
||||
func NewMax(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return NewStack(objects...)
|
||||
}
|
||||
|
||||
// NewPadded creates a new container with the specified objects inset by standard padding size.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewPadded(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewPaddedLayout(), objects...)
|
||||
}
|
||||
|
||||
// NewStack returns a new container that stacks objects on top of each other.
|
||||
// Objects at the end of the container will be stacked on top of objects before.
|
||||
// Having only a single object has no impact as CanvasObjects will
|
||||
// fill the available space even without a Stack.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewStack(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewStackLayout(), objects...)
|
||||
}
|
||||
|
||||
// NewVBox creates a new container with the specified objects and using the VBox layout.
|
||||
// The objects will be stacked in the container from top to bottom and always displayed
|
||||
// at their vertical MinSize. Use a different layout if the objects are intended
|
||||
// to be larger then their vertical MinSize.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewVBox(objects ...fyne.CanvasObject) *fyne.Container {
|
||||
return New(layout.NewVBoxLayout(), objects...)
|
||||
}
|
||||
105
vendor/fyne.io/fyne/v2/container/multiplewindows.go
generated
vendored
Normal file
105
vendor/fyne.io/fyne/v2/container/multiplewindows.go
generated
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
intWidget "fyne.io/fyne/v2/internal/widget"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// MultipleWindows is a container that handles multiple `InnerWindow` containers.
|
||||
// Each inner window can be dragged, resized and the stacking will change when the title bar is tapped.
|
||||
//
|
||||
// Since: 2.5
|
||||
type MultipleWindows struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Windows []*InnerWindow
|
||||
|
||||
content *fyne.Container
|
||||
}
|
||||
|
||||
// NewMultipleWindows creates a new `MultipleWindows` container to manage many inner windows.
|
||||
// The initial window list is passed optionally to this constructor function.
|
||||
// You can add new more windows to this container by calling `Add` or updating the `Windows`
|
||||
// field and calling `Refresh`.
|
||||
//
|
||||
// Since: 2.5
|
||||
func NewMultipleWindows(wins ...*InnerWindow) *MultipleWindows {
|
||||
m := &MultipleWindows{Windows: wins}
|
||||
m.ExtendBaseWidget(m)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultipleWindows) Add(w *InnerWindow) {
|
||||
m.Windows = append(m.Windows, w)
|
||||
m.refreshChildren()
|
||||
}
|
||||
|
||||
func (m *MultipleWindows) CreateRenderer() fyne.WidgetRenderer {
|
||||
m.content = New(&multiWinLayout{})
|
||||
m.refreshChildren()
|
||||
return widget.NewSimpleRenderer(intWidget.NewScroll(m.content))
|
||||
}
|
||||
|
||||
func (m *MultipleWindows) Refresh() {
|
||||
m.refreshChildren()
|
||||
// m.BaseWidget.Refresh()
|
||||
}
|
||||
|
||||
func (m *MultipleWindows) raise(w *InnerWindow) {
|
||||
id := -1
|
||||
for i, ww := range m.Windows {
|
||||
if ww == w {
|
||||
id = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if id == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
windows := append(m.Windows[:id], m.Windows[id+1:]...)
|
||||
m.Windows = append(windows, w)
|
||||
m.refreshChildren()
|
||||
}
|
||||
|
||||
func (m *MultipleWindows) refreshChildren() {
|
||||
if m.content == nil {
|
||||
return
|
||||
}
|
||||
|
||||
objs := make([]fyne.CanvasObject, len(m.Windows))
|
||||
for i, w := range m.Windows {
|
||||
objs[i] = w
|
||||
|
||||
m.setupChild(w)
|
||||
}
|
||||
m.content.Objects = objs
|
||||
m.content.Refresh()
|
||||
}
|
||||
|
||||
func (m *MultipleWindows) setupChild(w *InnerWindow) {
|
||||
w.OnDragged = func(ev *fyne.DragEvent) {
|
||||
w.Move(w.Position().Add(ev.Dragged))
|
||||
}
|
||||
w.OnResized = func(ev *fyne.DragEvent) {
|
||||
size := w.Size().Add(ev.Dragged)
|
||||
w.Resize(size.Max(w.MinSize()))
|
||||
}
|
||||
w.OnTappedBar = func() {
|
||||
m.raise(w)
|
||||
}
|
||||
}
|
||||
|
||||
type multiWinLayout struct {
|
||||
}
|
||||
|
||||
func (m *multiWinLayout) Layout(objects []fyne.CanvasObject, _ fyne.Size) {
|
||||
for _, w := range objects { // update the windows so they have real size
|
||||
w.Resize(w.MinSize().Max(w.Size()))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *multiWinLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
|
||||
return fyne.Size{}
|
||||
}
|
||||
55
vendor/fyne.io/fyne/v2/container/scroll.go
generated
vendored
Normal file
55
vendor/fyne.io/fyne/v2/container/scroll.go
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/widget"
|
||||
)
|
||||
|
||||
// Scroll defines a container that is smaller than the Content.
|
||||
// The Offset is used to determine the position of the child widgets within the container.
|
||||
//
|
||||
// Since: 1.4
|
||||
type Scroll = widget.Scroll
|
||||
|
||||
// ScrollDirection represents the directions in which a Scroll container can scroll its child content.
|
||||
//
|
||||
// Since: 1.4
|
||||
type ScrollDirection = fyne.ScrollDirection
|
||||
|
||||
// Constants for valid values of ScrollDirection.
|
||||
const (
|
||||
// ScrollBoth supports horizontal and vertical scrolling.
|
||||
ScrollBoth ScrollDirection = fyne.ScrollBoth
|
||||
// ScrollHorizontalOnly specifies the scrolling should only happen left to right.
|
||||
ScrollHorizontalOnly = fyne.ScrollHorizontalOnly
|
||||
// ScrollVerticalOnly specifies the scrolling should only happen top to bottom.
|
||||
ScrollVerticalOnly = fyne.ScrollVerticalOnly
|
||||
// ScrollNone turns off scrolling for this container.
|
||||
//
|
||||
// Since: 2.1
|
||||
ScrollNone = fyne.ScrollNone
|
||||
)
|
||||
|
||||
// NewScroll creates a scrollable parent wrapping the specified content.
|
||||
// Note that this may cause the MinSize to be smaller than that of the passed object.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewScroll(content fyne.CanvasObject) *Scroll {
|
||||
return widget.NewScroll(content)
|
||||
}
|
||||
|
||||
// NewHScroll create a scrollable parent wrapping the specified content.
|
||||
// Note that this may cause the MinSize.Width to be smaller than that of the passed object.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewHScroll(content fyne.CanvasObject) *Scroll {
|
||||
return widget.NewHScroll(content)
|
||||
}
|
||||
|
||||
// NewVScroll a scrollable parent wrapping the specified content.
|
||||
// Note that this may cause the MinSize.Height to be smaller than that of the passed object.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewVScroll(content fyne.CanvasObject) *Scroll {
|
||||
return widget.NewVScroll(content)
|
||||
}
|
||||
404
vendor/fyne.io/fyne/v2/container/split.go
generated
vendored
Normal file
404
vendor/fyne.io/fyne/v2/container/split.go
generated
vendored
Normal file
@ -0,0 +1,404 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// Declare conformity with CanvasObject interface
|
||||
var _ fyne.CanvasObject = (*Split)(nil)
|
||||
|
||||
// Split defines a container whose size is split between two children.
|
||||
//
|
||||
// Since: 1.4
|
||||
type Split struct {
|
||||
widget.BaseWidget
|
||||
Offset float64
|
||||
Horizontal bool
|
||||
Leading fyne.CanvasObject
|
||||
Trailing fyne.CanvasObject
|
||||
|
||||
// to communicate to the renderer that the next refresh
|
||||
// is just an offset update (ie a resize and move only)
|
||||
// cleared by renderer in Refresh()
|
||||
offsetUpdated bool
|
||||
}
|
||||
|
||||
// NewHSplit creates a horizontally arranged container with the specified leading and trailing elements.
|
||||
// A vertical split bar that can be dragged will be added between the elements.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewHSplit(leading, trailing fyne.CanvasObject) *Split {
|
||||
return newSplitContainer(true, leading, trailing)
|
||||
}
|
||||
|
||||
// NewVSplit creates a vertically arranged container with the specified top and bottom elements.
|
||||
// A horizontal split bar that can be dragged will be added between the elements.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewVSplit(top, bottom fyne.CanvasObject) *Split {
|
||||
return newSplitContainer(false, top, bottom)
|
||||
}
|
||||
|
||||
func newSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject) *Split {
|
||||
s := &Split{
|
||||
Offset: 0.5, // Sensible default, can be overridden with SetOffset
|
||||
Horizontal: horizontal,
|
||||
Leading: leading,
|
||||
Trailing: trailing,
|
||||
}
|
||||
s.BaseWidget.ExtendBaseWidget(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||||
func (s *Split) CreateRenderer() fyne.WidgetRenderer {
|
||||
s.BaseWidget.ExtendBaseWidget(s)
|
||||
d := newDivider(s)
|
||||
return &splitContainerRenderer{
|
||||
split: s,
|
||||
divider: d,
|
||||
objects: []fyne.CanvasObject{s.Leading, d, s.Trailing},
|
||||
}
|
||||
}
|
||||
|
||||
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
|
||||
//
|
||||
// Deprecated: Support for extending containers is being removed
|
||||
func (s *Split) ExtendBaseWidget(wid fyne.Widget) {
|
||||
s.BaseWidget.ExtendBaseWidget(wid)
|
||||
}
|
||||
|
||||
// SetOffset sets the offset (0.0 to 1.0) of the Split divider.
|
||||
// 0.0 - Leading is min size, Trailing uses all remaining space.
|
||||
// 0.5 - Leading & Trailing equally share the available space.
|
||||
// 1.0 - Trailing is min size, Leading uses all remaining space.
|
||||
func (s *Split) SetOffset(offset float64) {
|
||||
if s.Offset == offset {
|
||||
return
|
||||
}
|
||||
s.Offset = offset
|
||||
s.offsetUpdated = true
|
||||
s.Refresh()
|
||||
}
|
||||
|
||||
var _ fyne.WidgetRenderer = (*splitContainerRenderer)(nil)
|
||||
|
||||
type splitContainerRenderer struct {
|
||||
split *Split
|
||||
divider *divider
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) Layout(size fyne.Size) {
|
||||
var dividerPos, leadingPos, trailingPos fyne.Position
|
||||
var dividerSize, leadingSize, trailingSize fyne.Size
|
||||
|
||||
if r.split.Horizontal {
|
||||
lw, tw := r.computeSplitLengths(size.Width, r.minLeadingWidth(), r.minTrailingWidth())
|
||||
leadingPos.X = 0
|
||||
leadingSize.Width = lw
|
||||
leadingSize.Height = size.Height
|
||||
dividerPos.X = lw
|
||||
dividerSize.Width = dividerThickness(r.divider)
|
||||
dividerSize.Height = size.Height
|
||||
trailingPos.X = lw + dividerSize.Width
|
||||
trailingSize.Width = tw
|
||||
trailingSize.Height = size.Height
|
||||
} else {
|
||||
lh, th := r.computeSplitLengths(size.Height, r.minLeadingHeight(), r.minTrailingHeight())
|
||||
leadingPos.Y = 0
|
||||
leadingSize.Width = size.Width
|
||||
leadingSize.Height = lh
|
||||
dividerPos.Y = lh
|
||||
dividerSize.Width = size.Width
|
||||
dividerSize.Height = dividerThickness(r.divider)
|
||||
trailingPos.Y = lh + dividerSize.Height
|
||||
trailingSize.Width = size.Width
|
||||
trailingSize.Height = th
|
||||
}
|
||||
|
||||
r.divider.Move(dividerPos)
|
||||
r.divider.Resize(dividerSize)
|
||||
r.split.Leading.Move(leadingPos)
|
||||
r.split.Leading.Resize(leadingSize)
|
||||
r.split.Trailing.Move(trailingPos)
|
||||
r.split.Trailing.Resize(trailingSize)
|
||||
canvas.Refresh(r.divider)
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) MinSize() fyne.Size {
|
||||
s := fyne.NewSize(0, 0)
|
||||
for _, o := range r.objects {
|
||||
min := o.MinSize()
|
||||
if r.split.Horizontal {
|
||||
s.Width += min.Width
|
||||
s.Height = fyne.Max(s.Height, min.Height)
|
||||
} else {
|
||||
s.Width = fyne.Max(s.Width, min.Width)
|
||||
s.Height += min.Height
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) Refresh() {
|
||||
if r.split.offsetUpdated {
|
||||
r.Layout(r.split.Size())
|
||||
r.split.offsetUpdated = false
|
||||
return
|
||||
}
|
||||
|
||||
r.objects[0] = r.split.Leading
|
||||
// [1] is divider which doesn't change
|
||||
r.objects[2] = r.split.Trailing
|
||||
r.Layout(r.split.Size())
|
||||
|
||||
r.split.Leading.Refresh()
|
||||
r.divider.Refresh()
|
||||
r.split.Trailing.Refresh()
|
||||
canvas.Refresh(r.split)
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) {
|
||||
available := float64(total - dividerThickness(r.divider))
|
||||
if available <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
ld := float64(lMin)
|
||||
tr := float64(tMin)
|
||||
offset := r.split.Offset
|
||||
|
||||
min := ld / available
|
||||
max := 1 - tr/available
|
||||
if min <= max {
|
||||
if offset < min {
|
||||
offset = min
|
||||
}
|
||||
if offset > max {
|
||||
offset = max
|
||||
}
|
||||
} else {
|
||||
offset = ld / (ld + tr)
|
||||
}
|
||||
|
||||
ld = offset * available
|
||||
tr = available - ld
|
||||
return float32(ld), float32(tr)
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) minLeadingWidth() float32 {
|
||||
if r.split.Leading.Visible() {
|
||||
return r.split.Leading.MinSize().Width
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) minLeadingHeight() float32 {
|
||||
if r.split.Leading.Visible() {
|
||||
return r.split.Leading.MinSize().Height
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) minTrailingWidth() float32 {
|
||||
if r.split.Trailing.Visible() {
|
||||
return r.split.Trailing.MinSize().Width
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *splitContainerRenderer) minTrailingHeight() float32 {
|
||||
if r.split.Trailing.Visible() {
|
||||
return r.split.Trailing.MinSize().Height
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Declare conformity with interfaces
|
||||
var _ fyne.CanvasObject = (*divider)(nil)
|
||||
var _ fyne.Draggable = (*divider)(nil)
|
||||
var _ desktop.Cursorable = (*divider)(nil)
|
||||
var _ desktop.Hoverable = (*divider)(nil)
|
||||
|
||||
type divider struct {
|
||||
widget.BaseWidget
|
||||
split *Split
|
||||
hovered bool
|
||||
startDragOff *fyne.Position
|
||||
currentDragPos fyne.Position
|
||||
}
|
||||
|
||||
func newDivider(split *Split) *divider {
|
||||
d := ÷r{
|
||||
split: split,
|
||||
}
|
||||
d.ExtendBaseWidget(d)
|
||||
return d
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||||
func (d *divider) CreateRenderer() fyne.WidgetRenderer {
|
||||
d.ExtendBaseWidget(d)
|
||||
th := d.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
background := canvas.NewRectangle(th.Color(theme.ColorNameShadow, v))
|
||||
foreground := canvas.NewRectangle(th.Color(theme.ColorNameForeground, v))
|
||||
return ÷rRenderer{
|
||||
divider: d,
|
||||
background: background,
|
||||
foreground: foreground,
|
||||
objects: []fyne.CanvasObject{background, foreground},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *divider) Cursor() desktop.Cursor {
|
||||
if d.split.Horizontal {
|
||||
return desktop.HResizeCursor
|
||||
}
|
||||
return desktop.VResizeCursor
|
||||
}
|
||||
|
||||
func (d *divider) DragEnd() {
|
||||
d.startDragOff = nil
|
||||
}
|
||||
|
||||
func (d *divider) Dragged(e *fyne.DragEvent) {
|
||||
if d.startDragOff == nil {
|
||||
d.currentDragPos = d.Position().Add(e.Position)
|
||||
start := e.Position.Subtract(e.Dragged)
|
||||
d.startDragOff = &start
|
||||
} else {
|
||||
d.currentDragPos = d.currentDragPos.Add(e.Dragged)
|
||||
}
|
||||
|
||||
x, y := d.currentDragPos.Components()
|
||||
var offset, leadingRatio, trailingRatio float64
|
||||
if d.split.Horizontal {
|
||||
widthFree := float64(d.split.Size().Width - dividerThickness(d))
|
||||
leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree
|
||||
trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree)
|
||||
offset = float64(x-d.startDragOff.X) / widthFree
|
||||
} else {
|
||||
heightFree := float64(d.split.Size().Height - dividerThickness(d))
|
||||
leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree
|
||||
trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree)
|
||||
offset = float64(y-d.startDragOff.Y) / heightFree
|
||||
}
|
||||
|
||||
if offset < leadingRatio {
|
||||
offset = leadingRatio
|
||||
}
|
||||
if offset > trailingRatio {
|
||||
offset = trailingRatio
|
||||
}
|
||||
d.split.SetOffset(offset)
|
||||
}
|
||||
|
||||
func (d *divider) MouseIn(event *desktop.MouseEvent) {
|
||||
d.hovered = true
|
||||
d.split.Refresh()
|
||||
}
|
||||
|
||||
func (d *divider) MouseMoved(event *desktop.MouseEvent) {}
|
||||
|
||||
func (d *divider) MouseOut() {
|
||||
d.hovered = false
|
||||
d.split.Refresh()
|
||||
}
|
||||
|
||||
var _ fyne.WidgetRenderer = (*dividerRenderer)(nil)
|
||||
|
||||
type dividerRenderer struct {
|
||||
divider *divider
|
||||
background *canvas.Rectangle
|
||||
foreground *canvas.Rectangle
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *dividerRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (r *dividerRenderer) Layout(size fyne.Size) {
|
||||
r.background.Resize(size)
|
||||
var x, y, w, h float32
|
||||
if r.divider.split.Horizontal {
|
||||
x = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2
|
||||
y = (size.Height - handleLength(r.divider)) / 2
|
||||
w = handleThickness(r.divider)
|
||||
h = handleLength(r.divider)
|
||||
} else {
|
||||
x = (size.Width - handleLength(r.divider)) / 2
|
||||
y = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2
|
||||
w = handleLength(r.divider)
|
||||
h = handleThickness(r.divider)
|
||||
}
|
||||
r.foreground.Move(fyne.NewPos(x, y))
|
||||
r.foreground.Resize(fyne.NewSize(w, h))
|
||||
}
|
||||
|
||||
func (r *dividerRenderer) MinSize() fyne.Size {
|
||||
if r.divider.split.Horizontal {
|
||||
return fyne.NewSize(dividerThickness(r.divider), dividerLength(r.divider))
|
||||
}
|
||||
return fyne.NewSize(dividerLength(r.divider), dividerThickness(r.divider))
|
||||
}
|
||||
|
||||
func (r *dividerRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
func (r *dividerRenderer) Refresh() {
|
||||
th := r.divider.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
if r.divider.hovered {
|
||||
r.background.FillColor = th.Color(theme.ColorNameHover, v)
|
||||
} else {
|
||||
r.background.FillColor = th.Color(theme.ColorNameShadow, v)
|
||||
}
|
||||
r.background.Refresh()
|
||||
r.foreground.FillColor = th.Color(theme.ColorNameForeground, v)
|
||||
r.foreground.Refresh()
|
||||
r.Layout(r.divider.Size())
|
||||
}
|
||||
|
||||
func dividerTheme(d *divider) fyne.Theme {
|
||||
if d == nil {
|
||||
return theme.Current()
|
||||
}
|
||||
|
||||
return d.Theme()
|
||||
}
|
||||
|
||||
func dividerThickness(d *divider) float32 {
|
||||
th := dividerTheme(d)
|
||||
return th.Size(theme.SizeNamePadding) * 2
|
||||
}
|
||||
|
||||
func dividerLength(d *divider) float32 {
|
||||
th := dividerTheme(d)
|
||||
return th.Size(theme.SizeNamePadding) * 6
|
||||
}
|
||||
|
||||
func handleThickness(d *divider) float32 {
|
||||
th := dividerTheme(d)
|
||||
return th.Size(theme.SizeNamePadding) / 2
|
||||
|
||||
}
|
||||
|
||||
func handleLength(d *divider) float32 {
|
||||
th := dividerTheme(d)
|
||||
return th.Size(theme.SizeNamePadding) * 4
|
||||
}
|
||||
876
vendor/fyne.io/fyne/v2/container/tabs.go
generated
vendored
Normal file
876
vendor/fyne.io/fyne/v2/container/tabs.go
generated
vendored
Normal file
@ -0,0 +1,876 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/internal"
|
||||
"fyne.io/fyne/v2/internal/build"
|
||||
intTheme "fyne.io/fyne/v2/internal/theme"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// TabItem represents a single view in a tab view.
|
||||
// The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active.
|
||||
//
|
||||
// Since: 1.4
|
||||
type TabItem struct {
|
||||
Text string
|
||||
Icon fyne.Resource
|
||||
Content fyne.CanvasObject
|
||||
|
||||
button *tabButton
|
||||
}
|
||||
|
||||
// Disabled returns whether or not the TabItem is disabled.
|
||||
//
|
||||
// Since: 2.3
|
||||
func (ti *TabItem) Disabled() bool {
|
||||
if ti.button != nil {
|
||||
return ti.button.Disabled()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ti *TabItem) disable() {
|
||||
if ti.button != nil {
|
||||
ti.button.Disable()
|
||||
}
|
||||
}
|
||||
|
||||
func (ti *TabItem) enable() {
|
||||
if ti.button != nil {
|
||||
ti.button.Enable()
|
||||
}
|
||||
}
|
||||
|
||||
// TabLocation is the location where the tabs of a tab container should be rendered
|
||||
//
|
||||
// Since: 1.4
|
||||
type TabLocation int
|
||||
|
||||
// TabLocation values
|
||||
const (
|
||||
TabLocationTop TabLocation = iota
|
||||
TabLocationLeading
|
||||
TabLocationBottom
|
||||
TabLocationTrailing
|
||||
)
|
||||
|
||||
// NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewTabItem(text string, content fyne.CanvasObject) *TabItem {
|
||||
return &TabItem{Text: text, Content: content}
|
||||
}
|
||||
|
||||
// NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem {
|
||||
return &TabItem{Text: text, Icon: icon, Content: content}
|
||||
}
|
||||
|
||||
type baseTabs interface {
|
||||
fyne.Widget
|
||||
|
||||
onUnselected() func(*TabItem)
|
||||
onSelected() func(*TabItem)
|
||||
|
||||
items() []*TabItem
|
||||
setItems([]*TabItem)
|
||||
|
||||
selected() int
|
||||
setSelected(int)
|
||||
|
||||
tabLocation() TabLocation
|
||||
|
||||
transitioning() bool
|
||||
setTransitioning(bool)
|
||||
}
|
||||
|
||||
func isMobile(b baseTabs) bool {
|
||||
d := fyne.CurrentDevice()
|
||||
mobile := intTheme.FeatureForWidget(intTheme.FeatureNameDeviceIsMobile, b)
|
||||
if is, ok := mobile.(bool); ok {
|
||||
return is
|
||||
}
|
||||
|
||||
return d.IsMobile()
|
||||
}
|
||||
|
||||
func tabsAdjustedLocation(l TabLocation, b baseTabs) TabLocation {
|
||||
// Mobile has limited screen space, so don't put app tab bar on long edges
|
||||
if isMobile(b) {
|
||||
if o := fyne.CurrentDevice().Orientation(); fyne.IsVertical(o) {
|
||||
if l == TabLocationLeading {
|
||||
return TabLocationTop
|
||||
} else if l == TabLocationTrailing {
|
||||
return TabLocationBottom
|
||||
}
|
||||
} else {
|
||||
if l == TabLocationTop {
|
||||
return TabLocationLeading
|
||||
} else if l == TabLocationBottom {
|
||||
return TabLocationTrailing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func buildPopUpMenu(t baseTabs, button *widget.Button, items []*fyne.MenuItem) *widget.PopUpMenu {
|
||||
d := fyne.CurrentApp().Driver()
|
||||
c := d.CanvasForObject(button)
|
||||
popUpMenu := widget.NewPopUpMenu(fyne.NewMenu("", items...), c)
|
||||
buttonPos := d.AbsolutePositionForObject(button)
|
||||
buttonSize := button.Size()
|
||||
popUpMin := popUpMenu.MinSize()
|
||||
var popUpPos fyne.Position
|
||||
switch t.tabLocation() {
|
||||
case TabLocationLeading:
|
||||
popUpPos.X = buttonPos.X + buttonSize.Width
|
||||
popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
|
||||
case TabLocationTrailing:
|
||||
popUpPos.X = buttonPos.X - popUpMin.Width
|
||||
popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
|
||||
case TabLocationTop:
|
||||
popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
|
||||
popUpPos.Y = buttonPos.Y + buttonSize.Height
|
||||
case TabLocationBottom:
|
||||
popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
|
||||
popUpPos.Y = buttonPos.Y - popUpMin.Height
|
||||
}
|
||||
if popUpPos.X < 0 {
|
||||
popUpPos.X = 0
|
||||
}
|
||||
if popUpPos.Y < 0 {
|
||||
popUpPos.Y = 0
|
||||
}
|
||||
popUpMenu.ShowAtPosition(popUpPos)
|
||||
return popUpMenu
|
||||
}
|
||||
|
||||
func removeIndex(t baseTabs, index int) {
|
||||
items := t.items()
|
||||
if index < 0 || index >= len(items) {
|
||||
return
|
||||
}
|
||||
setItems(t, append(items[:index], items[index+1:]...))
|
||||
if s := t.selected(); index < s {
|
||||
t.setSelected(s - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func removeItem(t baseTabs, item *TabItem) {
|
||||
for index, existingItem := range t.items() {
|
||||
if existingItem == item {
|
||||
removeIndex(t, index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func selected(t baseTabs) *TabItem {
|
||||
selected := t.selected()
|
||||
items := t.items()
|
||||
if selected < 0 || selected >= len(items) {
|
||||
return nil
|
||||
}
|
||||
return items[selected]
|
||||
}
|
||||
|
||||
func selectIndex(t baseTabs, index int) {
|
||||
selected := t.selected()
|
||||
|
||||
if selected == index {
|
||||
// No change, so do nothing
|
||||
return
|
||||
}
|
||||
|
||||
items := t.items()
|
||||
|
||||
if f := t.onUnselected(); f != nil && selected >= 0 && selected < len(items) {
|
||||
// Notification of unselected
|
||||
f(items[selected])
|
||||
}
|
||||
|
||||
if index < 0 || index >= len(items) {
|
||||
// Out of bounds, so do nothing
|
||||
return
|
||||
}
|
||||
|
||||
t.setTransitioning(true)
|
||||
t.setSelected(index)
|
||||
t.Refresh()
|
||||
|
||||
if f := t.onSelected(); f != nil {
|
||||
// Notification of selected
|
||||
f(items[index])
|
||||
}
|
||||
}
|
||||
|
||||
func selectItem(t baseTabs, item *TabItem) {
|
||||
for i, child := range t.items() {
|
||||
if child == item {
|
||||
selectIndex(t, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setItems(t baseTabs, items []*TabItem) {
|
||||
if build.HasHints && mismatchedTabItems(items) {
|
||||
internal.LogHint("Tab items should all have the same type of content (text, icons or both)")
|
||||
}
|
||||
t.setItems(items)
|
||||
selected := t.selected()
|
||||
count := len(items)
|
||||
switch {
|
||||
case count == 0:
|
||||
// No items available to be selected
|
||||
selectIndex(t, -1) // Unsure OnUnselected gets called if applicable
|
||||
t.setSelected(-1)
|
||||
case selected < 0:
|
||||
// Current is first tab item
|
||||
selectIndex(t, 0)
|
||||
case selected >= count:
|
||||
// Current doesn't exist, select last tab
|
||||
selectIndex(t, count-1)
|
||||
}
|
||||
}
|
||||
|
||||
func disableIndex(t baseTabs, index int) {
|
||||
items := t.items()
|
||||
if index < 0 || index >= len(items) {
|
||||
return
|
||||
}
|
||||
|
||||
item := items[index]
|
||||
item.disable()
|
||||
|
||||
if selected(t) == item {
|
||||
// the disabled tab is currently selected, so select the first enabled tab
|
||||
for i, it := range items {
|
||||
if !it.Disabled() {
|
||||
selectIndex(t, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selected(t) == item {
|
||||
selectIndex(t, -1) // no other tab is able to be selected
|
||||
}
|
||||
}
|
||||
|
||||
func disableItem(t baseTabs, item *TabItem) {
|
||||
for i, it := range t.items() {
|
||||
if it == item {
|
||||
disableIndex(t, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableIndex(t baseTabs, index int) {
|
||||
items := t.items()
|
||||
if index < 0 || index >= len(items) {
|
||||
return
|
||||
}
|
||||
|
||||
item := items[index]
|
||||
item.enable()
|
||||
}
|
||||
|
||||
func enableItem(t baseTabs, item *TabItem) {
|
||||
for i, it := range t.items() {
|
||||
if it == item {
|
||||
enableIndex(t, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type baseTabsRenderer struct {
|
||||
positionAnimation, sizeAnimation *fyne.Animation
|
||||
|
||||
lastIndicatorPos fyne.Position
|
||||
lastIndicatorSize fyne.Size
|
||||
lastIndicatorHidden bool
|
||||
|
||||
action *widget.Button
|
||||
bar *fyne.Container
|
||||
divider, indicator *canvas.Rectangle
|
||||
|
||||
tabs baseTabs
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) applyTheme(t baseTabs) {
|
||||
if r.action != nil {
|
||||
r.action.SetIcon(moreIcon(t))
|
||||
}
|
||||
th := theme.CurrentForWidget(t)
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
r.divider.FillColor = th.Color(theme.ColorNameShadow, v)
|
||||
r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v)
|
||||
r.indicator.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||||
|
||||
for _, tab := range r.tabs.items() {
|
||||
tab.Content.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
|
||||
var (
|
||||
barPos, dividerPos, contentPos fyne.Position
|
||||
barSize, dividerSize, contentSize fyne.Size
|
||||
)
|
||||
|
||||
barMin := r.bar.MinSize()
|
||||
|
||||
th := theme.CurrentForWidget(t)
|
||||
padding := th.Size(theme.SizeNamePadding)
|
||||
switch t.tabLocation() {
|
||||
case TabLocationTop:
|
||||
barHeight := barMin.Height
|
||||
barPos = fyne.NewPos(0, 0)
|
||||
barSize = fyne.NewSize(size.Width, barHeight)
|
||||
dividerPos = fyne.NewPos(0, barHeight)
|
||||
dividerSize = fyne.NewSize(size.Width, padding)
|
||||
contentPos = fyne.NewPos(0, barHeight+padding)
|
||||
contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
|
||||
case TabLocationLeading:
|
||||
barWidth := barMin.Width
|
||||
barPos = fyne.NewPos(0, 0)
|
||||
barSize = fyne.NewSize(barWidth, size.Height)
|
||||
dividerPos = fyne.NewPos(barWidth, 0)
|
||||
dividerSize = fyne.NewSize(padding, size.Height)
|
||||
contentPos = fyne.NewPos(barWidth+padding, 0)
|
||||
contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
|
||||
case TabLocationBottom:
|
||||
barHeight := barMin.Height
|
||||
barPos = fyne.NewPos(0, size.Height-barHeight)
|
||||
barSize = fyne.NewSize(size.Width, barHeight)
|
||||
dividerPos = fyne.NewPos(0, size.Height-barHeight-padding)
|
||||
dividerSize = fyne.NewSize(size.Width, padding)
|
||||
contentPos = fyne.NewPos(0, 0)
|
||||
contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
|
||||
case TabLocationTrailing:
|
||||
barWidth := barMin.Width
|
||||
barPos = fyne.NewPos(size.Width-barWidth, 0)
|
||||
barSize = fyne.NewSize(barWidth, size.Height)
|
||||
dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0)
|
||||
dividerSize = fyne.NewSize(padding, size.Height)
|
||||
contentPos = fyne.NewPos(0, 0)
|
||||
contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
|
||||
}
|
||||
|
||||
r.bar.Move(barPos)
|
||||
r.bar.Resize(barSize)
|
||||
r.divider.Move(dividerPos)
|
||||
r.divider.Resize(dividerSize)
|
||||
selected := t.selected()
|
||||
for i, ti := range t.items() {
|
||||
if i == selected {
|
||||
ti.Content.Move(contentPos)
|
||||
ti.Content.Resize(contentSize)
|
||||
ti.Content.Show()
|
||||
} else {
|
||||
ti.Content.Hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size {
|
||||
th := theme.CurrentForWidget(t)
|
||||
pad := th.Size(theme.SizeNamePadding)
|
||||
buttonPad := pad
|
||||
barMin := r.bar.MinSize()
|
||||
tabsMin := r.bar.Objects[0].MinSize()
|
||||
accessory := r.bar.Objects[1]
|
||||
accessoryMin := accessory.MinSize()
|
||||
if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 {
|
||||
tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space
|
||||
buttonPad = 0
|
||||
} else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 {
|
||||
tabsMin = group.Objects[0].MinSize()
|
||||
buttonPad = 0
|
||||
}
|
||||
if !accessory.Visible() || accessoryMin.Width == 0 {
|
||||
buttonPad = 0
|
||||
accessoryMin = fyne.Size{}
|
||||
}
|
||||
|
||||
contentMin := fyne.NewSize(0, 0)
|
||||
for _, content := range t.items() {
|
||||
contentMin = contentMin.Max(content.Content.MinSize())
|
||||
}
|
||||
|
||||
switch t.tabLocation() {
|
||||
case TabLocationLeading, TabLocationTrailing:
|
||||
return fyne.NewSize(barMin.Width+contentMin.Width+pad,
|
||||
fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height))
|
||||
default:
|
||||
return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width),
|
||||
barMin.Height+contentMin.Height+pad)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, th fyne.Theme, animate bool) {
|
||||
isSameState := r.lastIndicatorPos.Subtract(pos).IsZero() && r.lastIndicatorSize.Subtract(siz).IsZero() &&
|
||||
r.lastIndicatorHidden == r.indicator.Hidden
|
||||
if isSameState {
|
||||
return
|
||||
}
|
||||
|
||||
if r.positionAnimation != nil {
|
||||
r.positionAnimation.Stop()
|
||||
r.positionAnimation = nil
|
||||
}
|
||||
if r.sizeAnimation != nil {
|
||||
r.sizeAnimation.Stop()
|
||||
r.sizeAnimation = nil
|
||||
}
|
||||
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v)
|
||||
if r.indicator.Position().IsZero() {
|
||||
r.indicator.Move(pos)
|
||||
r.indicator.Resize(siz)
|
||||
r.indicator.Refresh()
|
||||
return
|
||||
}
|
||||
|
||||
r.lastIndicatorPos = pos
|
||||
r.lastIndicatorSize = siz
|
||||
r.lastIndicatorHidden = r.indicator.Hidden
|
||||
|
||||
if animate && fyne.CurrentApp().Settings().ShowAnimations() {
|
||||
r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
|
||||
r.indicator.Move(p)
|
||||
r.indicator.Refresh()
|
||||
if pos == p {
|
||||
r.positionAnimation.Stop()
|
||||
r.positionAnimation = nil
|
||||
}
|
||||
})
|
||||
r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) {
|
||||
r.indicator.Resize(s)
|
||||
r.indicator.Refresh()
|
||||
if siz == s {
|
||||
r.sizeAnimation.Stop()
|
||||
r.sizeAnimation = nil
|
||||
}
|
||||
})
|
||||
|
||||
r.positionAnimation.Start()
|
||||
r.sizeAnimation.Start()
|
||||
} else {
|
||||
r.indicator.Move(pos)
|
||||
r.indicator.Resize(siz)
|
||||
r.indicator.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject {
|
||||
objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator}
|
||||
if i, is := t.selected(), t.items(); i >= 0 && i < len(is) {
|
||||
objects = append(objects, is[i].Content)
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
func (r *baseTabsRenderer) refresh(t baseTabs) {
|
||||
r.applyTheme(t)
|
||||
|
||||
r.bar.Refresh()
|
||||
r.divider.Refresh()
|
||||
r.indicator.Refresh()
|
||||
}
|
||||
|
||||
type buttonIconPosition int
|
||||
|
||||
const (
|
||||
buttonIconInline buttonIconPosition = iota
|
||||
buttonIconTop
|
||||
)
|
||||
|
||||
var _ fyne.Widget = (*tabButton)(nil)
|
||||
var _ fyne.Tappable = (*tabButton)(nil)
|
||||
var _ desktop.Hoverable = (*tabButton)(nil)
|
||||
|
||||
type tabButton struct {
|
||||
widget.DisableableWidget
|
||||
hovered bool
|
||||
icon fyne.Resource
|
||||
iconPosition buttonIconPosition
|
||||
importance widget.Importance
|
||||
onTapped func()
|
||||
onClosed func()
|
||||
text string
|
||||
textAlignment fyne.TextAlign
|
||||
|
||||
tabs baseTabs
|
||||
}
|
||||
|
||||
func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
|
||||
b.ExtendBaseWidget(b)
|
||||
th := b.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
|
||||
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||||
background.Hide()
|
||||
icon := canvas.NewImageFromResource(b.icon)
|
||||
if b.icon == nil {
|
||||
icon.Hide()
|
||||
}
|
||||
|
||||
label := canvas.NewText(b.text, th.Color(theme.ColorNameForeground, v))
|
||||
label.TextStyle.Bold = true
|
||||
|
||||
close := &tabCloseButton{
|
||||
parent: b,
|
||||
onTapped: func() {
|
||||
if f := b.onClosed; f != nil {
|
||||
f()
|
||||
}
|
||||
},
|
||||
}
|
||||
close.ExtendBaseWidget(close)
|
||||
close.Hide()
|
||||
|
||||
objects := []fyne.CanvasObject{background, label, close, icon}
|
||||
return &tabButtonRenderer{
|
||||
button: b,
|
||||
background: background,
|
||||
icon: icon,
|
||||
label: label,
|
||||
close: close,
|
||||
objects: objects,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tabButton) MinSize() fyne.Size {
|
||||
b.ExtendBaseWidget(b)
|
||||
return b.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
func (b *tabButton) MouseIn(*desktop.MouseEvent) {
|
||||
b.hovered = true
|
||||
b.Refresh()
|
||||
}
|
||||
|
||||
func (b *tabButton) MouseMoved(*desktop.MouseEvent) {
|
||||
}
|
||||
|
||||
func (b *tabButton) MouseOut() {
|
||||
b.hovered = false
|
||||
b.Refresh()
|
||||
}
|
||||
|
||||
func (b *tabButton) Tapped(*fyne.PointEvent) {
|
||||
if b.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
b.onTapped()
|
||||
}
|
||||
|
||||
type tabButtonRenderer struct {
|
||||
button *tabButton
|
||||
background *canvas.Rectangle
|
||||
icon *canvas.Image
|
||||
label *canvas.Text
|
||||
close *tabCloseButton
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) Layout(size fyne.Size) {
|
||||
th := r.button.Theme()
|
||||
pad := th.Size(theme.SizeNamePadding)
|
||||
r.background.Resize(size)
|
||||
padding := r.padding()
|
||||
innerSize := size.Subtract(padding)
|
||||
innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
|
||||
labelShift := float32(0)
|
||||
if r.icon.Visible() {
|
||||
iconSize := r.iconSize()
|
||||
var iconOffset fyne.Position
|
||||
if r.button.iconPosition == buttonIconTop {
|
||||
iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0)
|
||||
} else {
|
||||
iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2)
|
||||
}
|
||||
r.icon.Resize(fyne.NewSquareSize(iconSize))
|
||||
r.icon.Move(innerOffset.Add(iconOffset))
|
||||
labelShift = iconSize + pad
|
||||
}
|
||||
if r.label.Text != "" {
|
||||
var labelOffset fyne.Position
|
||||
var labelSize fyne.Size
|
||||
if r.button.iconPosition == buttonIconTop {
|
||||
labelOffset = fyne.NewPos(0, labelShift)
|
||||
labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height)
|
||||
} else {
|
||||
labelOffset = fyne.NewPos(labelShift, 0)
|
||||
labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height)
|
||||
}
|
||||
r.label.Resize(labelSize)
|
||||
r.label.Move(innerOffset.Add(labelOffset))
|
||||
}
|
||||
inlineIconSize := th.Size(theme.SizeNameInlineIcon)
|
||||
r.close.Move(fyne.NewPos(size.Width-inlineIconSize-pad, (size.Height-inlineIconSize)/2))
|
||||
r.close.Resize(fyne.NewSquareSize(inlineIconSize))
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) MinSize() fyne.Size {
|
||||
th := r.button.Theme()
|
||||
var contentWidth, contentHeight float32
|
||||
textSize := r.label.MinSize()
|
||||
iconSize := r.iconSize()
|
||||
padding := th.Size(theme.SizeNamePadding)
|
||||
if r.button.iconPosition == buttonIconTop {
|
||||
contentWidth = fyne.Max(textSize.Width, iconSize)
|
||||
if r.icon.Visible() {
|
||||
contentHeight += iconSize
|
||||
}
|
||||
if r.label.Text != "" {
|
||||
if r.icon.Visible() {
|
||||
contentHeight += padding
|
||||
}
|
||||
contentHeight += textSize.Height
|
||||
}
|
||||
} else {
|
||||
contentHeight = fyne.Max(textSize.Height, iconSize)
|
||||
if r.icon.Visible() {
|
||||
contentWidth += iconSize
|
||||
}
|
||||
if r.label.Text != "" {
|
||||
if r.icon.Visible() {
|
||||
contentWidth += padding
|
||||
}
|
||||
contentWidth += textSize.Width
|
||||
}
|
||||
}
|
||||
if r.button.onClosed != nil {
|
||||
inlineIconSize := th.Size(theme.SizeNameInlineIcon)
|
||||
contentWidth += inlineIconSize + padding
|
||||
contentHeight = fyne.Max(contentHeight, inlineIconSize)
|
||||
}
|
||||
return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) Refresh() {
|
||||
th := r.button.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
if r.button.hovered && !r.button.Disabled() {
|
||||
r.background.FillColor = th.Color(theme.ColorNameHover, v)
|
||||
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||||
r.background.Show()
|
||||
} else {
|
||||
r.background.Hide()
|
||||
}
|
||||
r.background.Refresh()
|
||||
|
||||
r.label.Text = r.button.text
|
||||
r.label.Alignment = r.button.textAlignment
|
||||
if !r.button.Disabled() {
|
||||
if r.button.importance == widget.HighImportance {
|
||||
r.label.Color = th.Color(theme.ColorNamePrimary, v)
|
||||
} else {
|
||||
r.label.Color = th.Color(theme.ColorNameForeground, v)
|
||||
}
|
||||
} else {
|
||||
r.label.Color = th.Color(theme.ColorNameDisabled, v)
|
||||
}
|
||||
r.label.TextSize = th.Size(theme.SizeNameText)
|
||||
if r.button.text == "" {
|
||||
r.label.Hide()
|
||||
} else {
|
||||
r.label.Show()
|
||||
}
|
||||
|
||||
r.icon.Resource = r.button.icon
|
||||
if r.icon.Resource != nil {
|
||||
r.icon.Show()
|
||||
switch res := r.icon.Resource.(type) {
|
||||
case *theme.ThemedResource:
|
||||
if r.button.importance == widget.HighImportance {
|
||||
r.icon.Resource = theme.NewPrimaryThemedResource(res)
|
||||
}
|
||||
case *theme.PrimaryThemedResource:
|
||||
if r.button.importance != widget.HighImportance {
|
||||
r.icon.Resource = res.Original()
|
||||
}
|
||||
}
|
||||
r.icon.Refresh()
|
||||
} else {
|
||||
r.icon.Hide()
|
||||
}
|
||||
|
||||
if r.button.onClosed != nil && (isMobile(r.button.tabs) || r.button.hovered || r.close.hovered) {
|
||||
r.close.Show()
|
||||
} else {
|
||||
r.close.Hide()
|
||||
}
|
||||
r.close.Refresh()
|
||||
|
||||
canvas.Refresh(r.button)
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) iconSize() float32 {
|
||||
iconSize := r.button.Theme().Size(theme.SizeNameInlineIcon)
|
||||
if r.button.iconPosition == buttonIconTop {
|
||||
return 2 * iconSize
|
||||
}
|
||||
|
||||
return iconSize
|
||||
}
|
||||
|
||||
func (r *tabButtonRenderer) padding() fyne.Size {
|
||||
padding := r.button.Theme().Size(theme.SizeNameInnerPadding)
|
||||
if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
|
||||
return fyne.NewSquareSize(padding * 2)
|
||||
}
|
||||
return fyne.NewSize(padding, padding*2)
|
||||
}
|
||||
|
||||
var _ fyne.Widget = (*tabCloseButton)(nil)
|
||||
var _ fyne.Tappable = (*tabCloseButton)(nil)
|
||||
var _ desktop.Hoverable = (*tabCloseButton)(nil)
|
||||
|
||||
type tabCloseButton struct {
|
||||
widget.BaseWidget
|
||||
parent *tabButton
|
||||
hovered bool
|
||||
onTapped func()
|
||||
}
|
||||
|
||||
func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
|
||||
b.ExtendBaseWidget(b)
|
||||
th := b.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
|
||||
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||||
background.Hide()
|
||||
icon := canvas.NewImageFromResource(theme.CancelIcon())
|
||||
|
||||
return &tabCloseButtonRenderer{
|
||||
button: b,
|
||||
background: background,
|
||||
icon: icon,
|
||||
objects: []fyne.CanvasObject{background, icon},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tabCloseButton) MinSize() fyne.Size {
|
||||
b.ExtendBaseWidget(b)
|
||||
return b.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) {
|
||||
b.hovered = true
|
||||
b.parent.Refresh()
|
||||
}
|
||||
|
||||
func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) {
|
||||
}
|
||||
|
||||
func (b *tabCloseButton) MouseOut() {
|
||||
b.hovered = false
|
||||
b.parent.Refresh()
|
||||
}
|
||||
|
||||
func (b *tabCloseButton) Tapped(*fyne.PointEvent) {
|
||||
b.onTapped()
|
||||
}
|
||||
|
||||
type tabCloseButtonRenderer struct {
|
||||
button *tabCloseButton
|
||||
background *canvas.Rectangle
|
||||
icon *canvas.Image
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *tabCloseButtonRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
|
||||
r.background.Resize(size)
|
||||
r.icon.Resize(size)
|
||||
}
|
||||
|
||||
func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
|
||||
return fyne.NewSquareSize(r.button.Theme().Size(theme.SizeNameInlineIcon))
|
||||
}
|
||||
|
||||
func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
func (r *tabCloseButtonRenderer) Refresh() {
|
||||
th := r.button.Theme()
|
||||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||||
|
||||
if r.button.hovered {
|
||||
r.background.FillColor = th.Color(theme.ColorNameHover, v)
|
||||
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||||
r.background.Show()
|
||||
} else {
|
||||
r.background.Hide()
|
||||
}
|
||||
r.background.Refresh()
|
||||
switch res := r.icon.Resource.(type) {
|
||||
case *theme.ThemedResource:
|
||||
if r.button.parent.importance == widget.HighImportance {
|
||||
r.icon.Resource = theme.NewPrimaryThemedResource(res)
|
||||
}
|
||||
case *theme.PrimaryThemedResource:
|
||||
if r.button.parent.importance != widget.HighImportance {
|
||||
r.icon.Resource = res.Original()
|
||||
}
|
||||
}
|
||||
r.icon.Refresh()
|
||||
}
|
||||
|
||||
func mismatchedTabItems(items []*TabItem) bool {
|
||||
var hasText, hasIcon bool
|
||||
for _, tab := range items {
|
||||
hasText = hasText || tab.Text != ""
|
||||
hasIcon = hasIcon || tab.Icon != nil
|
||||
}
|
||||
|
||||
mismatch := false
|
||||
for _, tab := range items {
|
||||
if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) {
|
||||
mismatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return mismatch
|
||||
}
|
||||
|
||||
func moreIcon(t baseTabs) fyne.Resource {
|
||||
if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing {
|
||||
return theme.MoreVerticalIcon()
|
||||
}
|
||||
return theme.MoreHorizontalIcon()
|
||||
}
|
||||
116
vendor/fyne.io/fyne/v2/container/theme.go
generated
vendored
Normal file
116
vendor/fyne.io/fyne/v2/container/theme.go
generated
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/cache"
|
||||
intTheme "fyne.io/fyne/v2/internal/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// ThemeOverride is a container where the child widgets are themed by the specified theme.
|
||||
// Containers will be traversed and all child widgets will reflect the theme in this container.
|
||||
// This should be used sparingly to avoid a jarring user experience.
|
||||
//
|
||||
// Since: 2.5
|
||||
type ThemeOverride struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Content fyne.CanvasObject
|
||||
Theme fyne.Theme
|
||||
|
||||
holder *fyne.Container
|
||||
|
||||
mobile bool
|
||||
}
|
||||
|
||||
// NewThemeOverride provides a container where the child widgets are themed by the specified theme.
|
||||
// Containers will be traversed and all child widgets will reflect the theme in this container.
|
||||
// This should be used sparingly to avoid a jarring user experience.
|
||||
//
|
||||
// If the content `obj` of this theme override is a container and items are later added to the container or any
|
||||
// sub-containers ensure that you call `Refresh()` on this `ThemeOverride` to ensure the new items match the theme.
|
||||
//
|
||||
// Since: 2.5
|
||||
func NewThemeOverride(obj fyne.CanvasObject, th fyne.Theme) *ThemeOverride {
|
||||
t := &ThemeOverride{Content: obj, Theme: th, holder: NewStack(obj)}
|
||||
t.ExtendBaseWidget(t)
|
||||
|
||||
cache.OverrideTheme(obj, addFeatures(th, t))
|
||||
obj.Refresh() // required as the widgets passed in could have been initially rendered with default theme
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *ThemeOverride) CreateRenderer() fyne.WidgetRenderer {
|
||||
cache.OverrideTheme(t.Content, addFeatures(t.Theme, t))
|
||||
|
||||
return &overrideRenderer{parent: t, objs: []fyne.CanvasObject{t.holder}}
|
||||
}
|
||||
|
||||
func (t *ThemeOverride) Refresh() {
|
||||
if t.holder.Objects[0] != t.Content {
|
||||
t.holder.Objects[0] = t.Content
|
||||
t.holder.Refresh()
|
||||
}
|
||||
|
||||
cache.OverrideTheme(t.Content, addFeatures(t.Theme, t))
|
||||
t.Content.Refresh()
|
||||
t.BaseWidget.Refresh()
|
||||
}
|
||||
|
||||
// SetDeviceIsMobile allows a ThemeOverride container to shape the contained widgets as a mobile device.
|
||||
// This will impact containers such as AppTabs and DocTabs, and more in the future, to display a layout
|
||||
// that would automatically be used for a mobile device runtime.
|
||||
//
|
||||
// Since: 2.6
|
||||
func (t *ThemeOverride) SetDeviceIsMobile(on bool) {
|
||||
t.mobile = on
|
||||
t.BaseWidget.Refresh()
|
||||
}
|
||||
|
||||
type featureTheme struct {
|
||||
fyne.Theme
|
||||
|
||||
over *ThemeOverride
|
||||
}
|
||||
|
||||
func addFeatures(th fyne.Theme, o *ThemeOverride) fyne.Theme {
|
||||
return &featureTheme{Theme: th, over: o}
|
||||
}
|
||||
|
||||
func (f *featureTheme) Feature(n intTheme.FeatureName) any {
|
||||
if n == intTheme.FeatureNameDeviceIsMobile {
|
||||
return f.over.mobile
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type overrideRenderer struct {
|
||||
parent *ThemeOverride
|
||||
|
||||
objs []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *overrideRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (r *overrideRenderer) Layout(s fyne.Size) {
|
||||
intTheme.PushRenderingTheme(r.parent.Theme)
|
||||
defer intTheme.PopRenderingTheme()
|
||||
|
||||
r.parent.holder.Resize(s)
|
||||
}
|
||||
|
||||
func (r *overrideRenderer) MinSize() fyne.Size {
|
||||
intTheme.PushRenderingTheme(r.parent.Theme)
|
||||
defer intTheme.PopRenderingTheme()
|
||||
|
||||
return r.parent.Content.MinSize()
|
||||
}
|
||||
|
||||
func (r *overrideRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objs
|
||||
}
|
||||
|
||||
func (r *overrideRenderer) Refresh() {
|
||||
}
|
||||
169
vendor/fyne.io/fyne/v2/data/binding/binding.go
generated
vendored
Normal file
169
vendor/fyne.io/fyne/v2/data/binding/binding.go
generated
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
//go:generate go run gen.go
|
||||
|
||||
// Package binding provides support for binding data to widgets.
|
||||
// All APIs in the binding package are safe to invoke directly from any goroutine.
|
||||
package binding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
errKeyNotFound = errors.New("key not found")
|
||||
errOutOfBounds = errors.New("index out of bounds")
|
||||
errParseFailed = errors.New("format did not match 1 value")
|
||||
|
||||
// As an optimisation we connect any listeners asking for the same key, so that there is only 1 per preference item.
|
||||
prefBinds = newPreferencesMap()
|
||||
)
|
||||
|
||||
// DataItem is the base interface for all bindable data items.
|
||||
// All APIs on bindable data items are safe to invoke directly fron any goroutine.
|
||||
//
|
||||
// Since: 2.0
|
||||
type DataItem interface {
|
||||
// AddListener attaches a new change listener to this DataItem.
|
||||
// Listeners are called each time the data inside this DataItem changes.
|
||||
// Additionally, the listener will be triggered upon successful connection to get the current value.
|
||||
AddListener(DataListener)
|
||||
// RemoveListener will detach the specified change listener from the DataItem.
|
||||
// Disconnected listener will no longer be triggered when changes occur.
|
||||
RemoveListener(DataListener)
|
||||
}
|
||||
|
||||
// DataListener is any object that can register for changes in a bindable DataItem.
|
||||
// See NewDataListener to define a new listener using just an inline function.
|
||||
//
|
||||
// Since: 2.0
|
||||
type DataListener interface {
|
||||
DataChanged()
|
||||
}
|
||||
|
||||
// NewDataListener is a helper function that creates a new listener type from a simple callback function.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewDataListener(fn func()) DataListener {
|
||||
return &listener{fn}
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
callback func()
|
||||
}
|
||||
|
||||
func (l *listener) DataChanged() {
|
||||
l.callback()
|
||||
}
|
||||
|
||||
type base struct {
|
||||
listeners []DataListener
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// AddListener allows a data listener to be informed of changes to this item.
|
||||
func (b *base) AddListener(l DataListener) {
|
||||
fyne.Do(func() {
|
||||
b.listeners = append(b.listeners, l)
|
||||
l.DataChanged()
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveListener should be called if the listener is no longer interested in being informed of data change events.
|
||||
func (b *base) RemoveListener(l DataListener) {
|
||||
fyne.Do(func() {
|
||||
for i, listener := range b.listeners {
|
||||
if listener == l {
|
||||
// Delete without preserving order:
|
||||
lastIndex := len(b.listeners) - 1
|
||||
b.listeners[i] = b.listeners[lastIndex]
|
||||
b.listeners[lastIndex] = nil
|
||||
b.listeners = b.listeners[:lastIndex]
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (b *base) trigger() {
|
||||
fyne.Do(b.triggerFromMain)
|
||||
}
|
||||
|
||||
func (b *base) triggerFromMain() {
|
||||
for _, listen := range b.listeners {
|
||||
listen.DataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Untyped supports binding an any value.
|
||||
//
|
||||
// Since: 2.1
|
||||
type Untyped = Item[any]
|
||||
|
||||
// NewUntyped returns a bindable any value that is managed internally.
|
||||
//
|
||||
// Since: 2.1
|
||||
func NewUntyped() Untyped {
|
||||
return NewItem(func(a1, a2 any) bool { return a1 == a2 })
|
||||
}
|
||||
|
||||
// ExternalUntyped supports binding a any value to an external value.
|
||||
//
|
||||
// Since: 2.1
|
||||
type ExternalUntyped = ExternalItem[any]
|
||||
|
||||
// BindUntyped returns a bindable any value that is bound to an external type.
|
||||
// The parameter must be a pointer to the type you wish to bind.
|
||||
//
|
||||
// Since: 2.1
|
||||
func BindUntyped(v any) ExternalUntyped {
|
||||
t := reflect.TypeOf(v)
|
||||
if t.Kind() != reflect.Ptr {
|
||||
fyne.LogError("Invalid type passed to BindUntyped, must be a pointer", nil)
|
||||
v = nil
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
v = new(any) // never allow a nil value pointer
|
||||
}
|
||||
|
||||
b := &boundExternalUntyped{}
|
||||
b.val = reflect.ValueOf(v).Elem()
|
||||
b.old = b.val.Interface()
|
||||
return b
|
||||
}
|
||||
|
||||
type boundExternalUntyped struct {
|
||||
base
|
||||
|
||||
val reflect.Value
|
||||
old any
|
||||
}
|
||||
|
||||
func (b *boundExternalUntyped) Get() (any, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
return b.val.Interface(), nil
|
||||
}
|
||||
|
||||
func (b *boundExternalUntyped) Set(val any) error {
|
||||
b.lock.Lock()
|
||||
if b.old == val {
|
||||
b.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.val.Set(reflect.ValueOf(val))
|
||||
b.old = val
|
||||
b.lock.Unlock()
|
||||
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *boundExternalUntyped) Reload() error {
|
||||
return b.Set(b.val.Interface())
|
||||
}
|
||||
118
vendor/fyne.io/fyne/v2/data/binding/bool.go
generated
vendored
Normal file
118
vendor/fyne.io/fyne/v2/data/binding/bool.go
generated
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
package binding
|
||||
|
||||
type not struct {
|
||||
Bool
|
||||
}
|
||||
|
||||
var _ Bool = (*not)(nil)
|
||||
|
||||
// Not returns a Bool binding that invert the value of the given data binding.
|
||||
// This is providing the logical Not boolean operation as a data binding.
|
||||
//
|
||||
// Since 2.4
|
||||
func Not(data Bool) Bool {
|
||||
return ¬{Bool: data}
|
||||
}
|
||||
|
||||
func (n *not) Get() (bool, error) {
|
||||
v, err := n.Bool.Get()
|
||||
return !v, err
|
||||
}
|
||||
|
||||
func (n *not) Set(value bool) error {
|
||||
return n.Bool.Set(!value)
|
||||
}
|
||||
|
||||
type and struct {
|
||||
booleans
|
||||
}
|
||||
|
||||
var _ Bool = (*and)(nil)
|
||||
|
||||
// And returns a Bool binding that return true when all the passed Bool binding are
|
||||
// true and false otherwise. It does apply a logical and boolean operation on all passed
|
||||
// Bool bindings. This binding is two way. In case of a Set, it will propagate the value
|
||||
// identically to all the Bool bindings used for its construction.
|
||||
//
|
||||
// Since 2.4
|
||||
func And(data ...Bool) Bool {
|
||||
return &and{booleans: booleans{data: data}}
|
||||
}
|
||||
|
||||
func (a *and) Get() (bool, error) {
|
||||
for _, d := range a.data {
|
||||
v, err := d.Get()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !v {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *and) Set(value bool) error {
|
||||
for _, d := range a.data {
|
||||
err := d.Set(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type or struct {
|
||||
booleans
|
||||
}
|
||||
|
||||
var _ Bool = (*or)(nil)
|
||||
|
||||
// Or returns a Bool binding that return true when at least one of the passed Bool binding
|
||||
// is true and false otherwise. It does apply a logical or boolean operation on all passed
|
||||
// Bool bindings. This binding is two way. In case of a Set, it will propagate the value
|
||||
// identically to all the Bool bindings used for its construction.
|
||||
//
|
||||
// Since 2.4
|
||||
func Or(data ...Bool) Bool {
|
||||
return &or{booleans: booleans{data: data}}
|
||||
}
|
||||
|
||||
func (o *or) Get() (bool, error) {
|
||||
for _, d := range o.data {
|
||||
v, err := d.Get()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if v {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (o *or) Set(value bool) error {
|
||||
for _, d := range o.data {
|
||||
err := d.Set(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type booleans struct {
|
||||
data []Bool
|
||||
}
|
||||
|
||||
func (g *booleans) AddListener(listener DataListener) {
|
||||
for _, d := range g.data {
|
||||
d.AddListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *booleans) RemoveListener(listener DataListener) {
|
||||
for _, d := range g.data {
|
||||
d.RemoveListener(listener)
|
||||
}
|
||||
}
|
||||
409
vendor/fyne.io/fyne/v2/data/binding/convert.go
generated
vendored
Normal file
409
vendor/fyne.io/fyne/v2/data/binding/convert.go
generated
vendored
Normal file
@ -0,0 +1,409 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
// BoolToString creates a binding that connects a Bool data item to a String.
|
||||
// Changes to the Bool will be pushed to the String and setting the string will parse and set the
|
||||
// Bool if the parse was successful.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BoolToString(v Bool) String {
|
||||
return toStringComparable[bool](v, formatBool, parseBool)
|
||||
}
|
||||
|
||||
// BoolToStringWithFormat creates a binding that connects a Bool data item to a String and is
|
||||
// presented using the specified format. Changes to the Bool will be pushed to the String and setting
|
||||
// the string will parse and set the Bool if the string matches the format and its parse was successful.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BoolToStringWithFormat(v Bool, format string) String {
|
||||
return toStringWithFormatComparable[bool](v, format, "%t", formatBool, parseBool)
|
||||
}
|
||||
|
||||
// FloatToString creates a binding that connects a Float data item to a String.
|
||||
// Changes to the Float will be pushed to the String and setting the string will parse and set the
|
||||
// Float if the parse was successful.
|
||||
//
|
||||
// Since: 2.0
|
||||
func FloatToString(v Float) String {
|
||||
return toStringComparable[float64](v, formatFloat, parseFloat)
|
||||
}
|
||||
|
||||
// FloatToStringWithFormat creates a binding that connects a Float data item to a String and is
|
||||
// presented using the specified format. Changes to the Float will be pushed to the String and setting
|
||||
// the string will parse and set the Float if the string matches the format and its parse was successful.
|
||||
//
|
||||
// Since: 2.0
|
||||
func FloatToStringWithFormat(v Float, format string) String {
|
||||
return toStringWithFormatComparable[float64](v, format, "%f", formatFloat, parseFloat)
|
||||
}
|
||||
|
||||
// IntToFloat creates a binding that connects an Int data item to a Float.
|
||||
//
|
||||
// Since: 2.5
|
||||
func IntToFloat(val Int) Float {
|
||||
v := &fromIntTo[float64]{from: val, parser: internalFloatToInt, formatter: internalIntToFloat}
|
||||
val.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// FloatToInt creates a binding that connects a Float data item to an Int.
|
||||
//
|
||||
// Since: 2.5
|
||||
func FloatToInt(v Float) Int {
|
||||
i := &toInt[float64]{from: v, parser: internalFloatToInt, formatter: internalIntToFloat}
|
||||
v.AddListener(i)
|
||||
return i
|
||||
}
|
||||
|
||||
// IntToString creates a binding that connects a Int data item to a String.
|
||||
// Changes to the Int will be pushed to the String and setting the string will parse and set the
|
||||
// Int if the parse was successful.
|
||||
//
|
||||
// Since: 2.0
|
||||
func IntToString(v Int) String {
|
||||
return toStringComparable[int](v, formatInt, parseInt)
|
||||
}
|
||||
|
||||
// IntToStringWithFormat creates a binding that connects a Int data item to a String and is
|
||||
// presented using the specified format. Changes to the Int will be pushed to the String and setting
|
||||
// the string will parse and set the Int if the string matches the format and its parse was successful.
|
||||
//
|
||||
// Since: 2.0
|
||||
func IntToStringWithFormat(v Int, format string) String {
|
||||
return toStringWithFormatComparable[int](v, format, "%d", formatInt, parseInt)
|
||||
}
|
||||
|
||||
// URIToString creates a binding that connects a URI data item to a String.
|
||||
// Changes to the URI will be pushed to the String and setting the string will parse and set the
|
||||
// URI if the parse was successful.
|
||||
//
|
||||
// Since: 2.1
|
||||
func URIToString(v URI) String {
|
||||
return toString[fyne.URI](v, uriToString, storage.EqualURI, uriFromString)
|
||||
}
|
||||
|
||||
// StringToBool creates a binding that connects a String data item to a Bool.
|
||||
// Changes to the String will be parsed and pushed to the Bool if the parse was successful, and setting
|
||||
// the Bool update the String binding.
|
||||
//
|
||||
// Since: 2.0
|
||||
func StringToBool(str String) Bool {
|
||||
v := &fromStringTo[bool]{from: str, formatter: parseBool, parser: formatBool}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// StringToBoolWithFormat creates a binding that connects a String data item to a Bool and is
|
||||
// presented using the specified format. Changes to the Bool will be parsed and if the format matches and
|
||||
// the parse is successful it will be pushed to the String. Setting the Bool will push a formatted value
|
||||
// into the String.
|
||||
//
|
||||
// Since: 2.0
|
||||
func StringToBoolWithFormat(str String, format string) Bool {
|
||||
if format == "%t" { // Same as not using custom format.
|
||||
return StringToBool(str)
|
||||
}
|
||||
|
||||
v := &fromStringTo[bool]{from: str, format: format}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// StringToFloat creates a binding that connects a String data item to a Float.
|
||||
// Changes to the String will be parsed and pushed to the Float if the parse was successful, and setting
|
||||
// the Float update the String binding.
|
||||
//
|
||||
// Since: 2.0
|
||||
func StringToFloat(str String) Float {
|
||||
v := &fromStringTo[float64]{from: str, formatter: parseFloat, parser: formatFloat}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// StringToFloatWithFormat creates a binding that connects a String data item to a Float and is
|
||||
// presented using the specified format. Changes to the Float will be parsed and if the format matches and
|
||||
// the parse is successful it will be pushed to the String. Setting the Float will push a formatted value
|
||||
// into the String.
|
||||
//
|
||||
// Since: 2.0
|
||||
func StringToFloatWithFormat(str String, format string) Float {
|
||||
if format == "%f" { // Same as not using custom format.
|
||||
return StringToFloat(str)
|
||||
}
|
||||
|
||||
v := &fromStringTo[float64]{from: str, format: format}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// StringToInt creates a binding that connects a String data item to a Int.
|
||||
// Changes to the String will be parsed and pushed to the Int if the parse was successful, and setting
|
||||
// the Int update the String binding.
|
||||
//
|
||||
// Since: 2.0
|
||||
func StringToInt(str String) Int {
|
||||
v := &fromStringTo[int]{from: str, parser: formatInt, formatter: parseInt}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// StringToIntWithFormat creates a binding that connects a String data item to a Int and is
|
||||
// presented using the specified format. Changes to the Int will be parsed and if the format matches and
|
||||
// the parse is successful it will be pushed to the String. Setting the Int will push a formatted value
|
||||
// into the String.
|
||||
//
|
||||
// Since: 2.0
|
||||
func StringToIntWithFormat(str String, format string) Int {
|
||||
if format == "%d" { // Same as not using custom format.
|
||||
return StringToInt(str)
|
||||
}
|
||||
|
||||
v := &fromStringTo[int]{from: str, format: format}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// StringToURI creates a binding that connects a String data item to a URI.
|
||||
// Changes to the String will be parsed and pushed to the URI if the parse was successful, and setting
|
||||
// the URI update the String binding.
|
||||
//
|
||||
// Since: 2.1
|
||||
func StringToURI(str String) URI {
|
||||
v := &fromStringTo[fyne.URI]{from: str, parser: uriToString, formatter: uriFromString}
|
||||
str.AddListener(v)
|
||||
return v
|
||||
}
|
||||
|
||||
func toString[T any](v Item[T], formatter func(T) (string, error), comparator func(T, T) bool, parser func(string) (T, error)) *toStringFrom[T] {
|
||||
str := &toStringFrom[T]{from: v, formatter: formatter, comparator: comparator, parser: parser}
|
||||
v.AddListener(str)
|
||||
return str
|
||||
}
|
||||
|
||||
func toStringComparable[T bool | float64 | int](v Item[T], formatter func(T) (string, error), parser func(string) (T, error)) *toStringFrom[T] {
|
||||
return toString(v, formatter, func(t1, t2 T) bool { return t1 == t2 }, parser)
|
||||
}
|
||||
|
||||
func toStringWithFormat[T any](v Item[T], format, defaultFormat string, formatter func(T) (string, error), comparator func(T, T) bool, parser func(string) (T, error)) String {
|
||||
str := toString(v, formatter, comparator, parser)
|
||||
if format != defaultFormat { // Same as not using custom formatting.
|
||||
str.format = format
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func toStringWithFormatComparable[T bool | float64 | int](v Item[T], format, defaultFormat string, formatter func(T) (string, error), parser func(string) (T, error)) String {
|
||||
return toStringWithFormat(v, format, defaultFormat, formatter, func(t1, t2 T) bool { return t1 == t2 }, parser)
|
||||
}
|
||||
|
||||
type convertBaseItem struct {
|
||||
base
|
||||
}
|
||||
|
||||
func (s *convertBaseItem) DataChanged() {
|
||||
s.triggerFromMain()
|
||||
}
|
||||
|
||||
type toStringFrom[T any] struct {
|
||||
convertBaseItem
|
||||
|
||||
format string
|
||||
|
||||
formatter func(T) (string, error)
|
||||
comparator func(T, T) bool
|
||||
parser func(string) (T, error)
|
||||
|
||||
from Item[T]
|
||||
}
|
||||
|
||||
func (s *toStringFrom[T]) Get() (string, error) {
|
||||
val, err := s.from.Get()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if s.format != "" {
|
||||
return fmt.Sprintf(s.format, val), nil
|
||||
}
|
||||
|
||||
return s.formatter(val)
|
||||
}
|
||||
|
||||
func (s *toStringFrom[T]) Set(str string) error {
|
||||
var val T
|
||||
if s.format != "" {
|
||||
safe := stripFormatPrecision(s.format)
|
||||
n, err := fmt.Sscanf(str, safe+" ", &val) // " " denotes match to end of string
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errParseFailed
|
||||
}
|
||||
} else {
|
||||
new, err := s.parser(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val = new
|
||||
}
|
||||
|
||||
old, err := s.from.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.comparator(val, old) {
|
||||
return nil
|
||||
}
|
||||
if err = s.from.Set(val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type fromStringTo[T any] struct {
|
||||
convertBaseItem
|
||||
|
||||
format string
|
||||
formatter func(string) (T, error)
|
||||
parser func(T) (string, error)
|
||||
|
||||
from String
|
||||
}
|
||||
|
||||
func (s *fromStringTo[T]) Get() (T, error) {
|
||||
str, err := s.from.Get()
|
||||
if str == "" || err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
var val T
|
||||
if s.format != "" {
|
||||
n, err := fmt.Sscanf(str, s.format+" ", &val) // " " denotes match to end of string
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
if n != 1 {
|
||||
return *new(T), errParseFailed
|
||||
}
|
||||
} else {
|
||||
formatted, err := s.formatter(str)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
val = formatted
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (s *fromStringTo[T]) Set(val T) error {
|
||||
var str string
|
||||
if s.format != "" {
|
||||
str = fmt.Sprintf(s.format, val)
|
||||
} else {
|
||||
parsed, err := s.parser(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str = parsed
|
||||
}
|
||||
|
||||
old, err := s.from.Get()
|
||||
if str == old {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.from.Set(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type toInt[T float64] struct {
|
||||
convertBaseItem
|
||||
|
||||
formatter func(int) (T, error)
|
||||
parser func(T) (int, error)
|
||||
|
||||
from Item[T]
|
||||
}
|
||||
|
||||
func (s *toInt[T]) Get() (int, error) {
|
||||
val, err := s.from.Get()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.parser(val)
|
||||
}
|
||||
|
||||
func (s *toInt[T]) Set(v int) error {
|
||||
val, err := s.formatter(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
old, err := s.from.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val == old {
|
||||
return nil
|
||||
}
|
||||
err = s.from.Set(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type fromIntTo[T float64] struct {
|
||||
convertBaseItem
|
||||
|
||||
formatter func(int) (T, error)
|
||||
parser func(T) (int, error)
|
||||
from Item[int]
|
||||
}
|
||||
|
||||
func (s *fromIntTo[T]) Get() (T, error) {
|
||||
val, err := s.from.Get()
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
return s.formatter(val)
|
||||
}
|
||||
|
||||
func (s *fromIntTo[T]) Set(val T) error {
|
||||
i, err := s.parser(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
old, err := s.from.Get()
|
||||
if i == old {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.from.Set(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.trigger()
|
||||
return nil
|
||||
}
|
||||
98
vendor/fyne.io/fyne/v2/data/binding/convert_helper.go
generated
vendored
Normal file
98
vendor/fyne.io/fyne/v2/data/binding/convert_helper.go
generated
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
func stripFormatPrecision(in string) string {
|
||||
// quick exit if certainly not float
|
||||
if !strings.ContainsRune(in, 'f') {
|
||||
return in
|
||||
}
|
||||
|
||||
start := -1
|
||||
end := -1
|
||||
runes := []rune(in)
|
||||
for i, r := range runes {
|
||||
switch r {
|
||||
case '%':
|
||||
if i > 0 && start == i-1 { // ignore %%
|
||||
start = -1
|
||||
} else {
|
||||
start = i
|
||||
}
|
||||
case 'f':
|
||||
if start == -1 { // not part of format
|
||||
continue
|
||||
}
|
||||
end = i
|
||||
}
|
||||
|
||||
if end > -1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if end == start+1 { // no width/precision
|
||||
return in
|
||||
}
|
||||
|
||||
sizeRunes := runes[start+1 : end]
|
||||
width, err := parseFloat(string(sizeRunes))
|
||||
if err != nil {
|
||||
return string(runes[:start+1]) + string(runes[:end])
|
||||
}
|
||||
|
||||
if sizeRunes[0] == '.' { // formats like %.2f
|
||||
return string(runes[:start+1]) + string(runes[end:])
|
||||
}
|
||||
return string(runes[:start+1]) + strconv.Itoa(int(width)) + string(runes[end:])
|
||||
}
|
||||
|
||||
func uriFromString(in string) (fyne.URI, error) {
|
||||
return storage.ParseURI(in)
|
||||
}
|
||||
|
||||
func uriToString(in fyne.URI) (string, error) {
|
||||
if in == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return in.String(), nil
|
||||
}
|
||||
|
||||
func parseBool(in string) (bool, error) {
|
||||
return strconv.ParseBool(in)
|
||||
}
|
||||
|
||||
func parseFloat(in string) (float64, error) {
|
||||
return strconv.ParseFloat(in, 64)
|
||||
}
|
||||
|
||||
func parseInt(in string) (int, error) {
|
||||
out, err := strconv.ParseInt(in, 0, 64)
|
||||
return int(out), err
|
||||
}
|
||||
|
||||
func formatBool(in bool) (string, error) {
|
||||
return strconv.FormatBool(in), nil
|
||||
}
|
||||
|
||||
func formatFloat(in float64) (string, error) {
|
||||
return strconv.FormatFloat(in, 'f', 6, 64), nil
|
||||
}
|
||||
|
||||
func formatInt(in int) (string, error) {
|
||||
return strconv.FormatInt(int64(in), 10), nil
|
||||
}
|
||||
|
||||
func internalFloatToInt(val float64) (int, error) {
|
||||
return int(val), nil
|
||||
}
|
||||
|
||||
func internalIntToFloat(val int) (float64, error) {
|
||||
return float64(val), nil
|
||||
}
|
||||
284
vendor/fyne.io/fyne/v2/data/binding/items.go
generated
vendored
Normal file
284
vendor/fyne.io/fyne/v2/data/binding/items.go
generated
vendored
Normal file
@ -0,0 +1,284 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
// Item supports binding any type T generically.
|
||||
//
|
||||
// Since: 2.6
|
||||
type Item[T any] interface {
|
||||
DataItem
|
||||
Get() (T, error)
|
||||
Set(T) error
|
||||
}
|
||||
|
||||
// ExternalItem supports binding any external value of type T.
|
||||
//
|
||||
// Since: 2.6
|
||||
type ExternalItem[T any] interface {
|
||||
Item[T]
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewItem returns a bindable value of type T that is managed internally.
|
||||
//
|
||||
// Since: 2.6
|
||||
func NewItem[T any](comparator func(T, T) bool) Item[T] {
|
||||
return &item[T]{val: new(T), comparator: comparator}
|
||||
}
|
||||
|
||||
// BindItem returns a new bindable value that controls the contents of the provided variable of type T.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.6
|
||||
func BindItem[T any](val *T, comparator func(T, T) bool) ExternalItem[T] {
|
||||
if val == nil {
|
||||
val = new(T) // never allow a nil value pointer
|
||||
}
|
||||
b := &externalItem[T]{}
|
||||
b.comparator = comparator
|
||||
b.val = val
|
||||
b.old = *val
|
||||
return b
|
||||
}
|
||||
|
||||
// Bool supports binding a bool value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type Bool = Item[bool]
|
||||
|
||||
// ExternalBool supports binding a bool value to an external value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalBool = ExternalItem[bool]
|
||||
|
||||
// NewBool returns a bindable bool value that is managed internally.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewBool() Bool {
|
||||
return newItemComparable[bool]()
|
||||
}
|
||||
|
||||
// BindBool returns a new bindable value that controls the contents of the provided bool variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindBool(v *bool) ExternalBool {
|
||||
return bindExternalComparable(v)
|
||||
}
|
||||
|
||||
// Bytes supports binding a []byte value.
|
||||
//
|
||||
// Since: 2.2
|
||||
type Bytes = Item[[]byte]
|
||||
|
||||
// ExternalBytes supports binding a []byte value to an external value.
|
||||
//
|
||||
// Since: 2.2
|
||||
type ExternalBytes = ExternalItem[[]byte]
|
||||
|
||||
// NewBytes returns a bindable []byte value that is managed internally.
|
||||
//
|
||||
// Since: 2.2
|
||||
func NewBytes() Bytes {
|
||||
return NewItem(bytes.Equal)
|
||||
}
|
||||
|
||||
// BindBytes returns a new bindable value that controls the contents of the provided []byte variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.2
|
||||
func BindBytes(v *[]byte) ExternalBytes {
|
||||
return BindItem(v, bytes.Equal)
|
||||
}
|
||||
|
||||
// Float supports binding a float64 value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type Float = Item[float64]
|
||||
|
||||
// ExternalFloat supports binding a float64 value to an external value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalFloat = ExternalItem[float64]
|
||||
|
||||
// NewFloat returns a bindable float64 value that is managed internally.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewFloat() Float {
|
||||
return newItemComparable[float64]()
|
||||
}
|
||||
|
||||
// BindFloat returns a new bindable value that controls the contents of the provided float64 variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindFloat(v *float64) ExternalFloat {
|
||||
return bindExternalComparable(v)
|
||||
}
|
||||
|
||||
// Int supports binding a int value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type Int = Item[int]
|
||||
|
||||
// ExternalInt supports binding a int value to an external value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalInt = ExternalItem[int]
|
||||
|
||||
// NewInt returns a bindable int value that is managed internally.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewInt() Int {
|
||||
return newItemComparable[int]()
|
||||
}
|
||||
|
||||
// BindInt returns a new bindable value that controls the contents of the provided int variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindInt(v *int) ExternalInt {
|
||||
return bindExternalComparable(v)
|
||||
}
|
||||
|
||||
// Rune supports binding a rune value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type Rune = Item[rune]
|
||||
|
||||
// ExternalRune supports binding a rune value to an external value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalRune = ExternalItem[rune]
|
||||
|
||||
// NewRune returns a bindable rune value that is managed internally.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewRune() Rune {
|
||||
return newItemComparable[rune]()
|
||||
}
|
||||
|
||||
// BindRune returns a new bindable value that controls the contents of the provided rune variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindRune(v *rune) ExternalRune {
|
||||
return bindExternalComparable(v)
|
||||
}
|
||||
|
||||
// String supports binding a string value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type String = Item[string]
|
||||
|
||||
// ExternalString supports binding a string value to an external value.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalString = ExternalItem[string]
|
||||
|
||||
// NewString returns a bindable string value that is managed internally.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewString() String {
|
||||
return newItemComparable[string]()
|
||||
}
|
||||
|
||||
// BindString returns a new bindable value that controls the contents of the provided string variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindString(v *string) ExternalString {
|
||||
return bindExternalComparable(v)
|
||||
}
|
||||
|
||||
// URI supports binding a fyne.URI value.
|
||||
//
|
||||
// Since: 2.1
|
||||
type URI = Item[fyne.URI]
|
||||
|
||||
// ExternalURI supports binding a fyne.URI value to an external value.
|
||||
//
|
||||
// Since: 2.1
|
||||
type ExternalURI = ExternalItem[fyne.URI]
|
||||
|
||||
// NewURI returns a bindable fyne.URI value that is managed internally.
|
||||
//
|
||||
// Since: 2.1
|
||||
func NewURI() URI {
|
||||
return NewItem(storage.EqualURI)
|
||||
}
|
||||
|
||||
// BindURI returns a new bindable value that controls the contents of the provided fyne.URI variable.
|
||||
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.1
|
||||
func BindURI(v *fyne.URI) ExternalURI {
|
||||
return BindItem(v, storage.EqualURI)
|
||||
}
|
||||
|
||||
func newItemComparable[T bool | float64 | int | rune | string]() Item[T] {
|
||||
return NewItem[T](func(a, b T) bool { return a == b })
|
||||
}
|
||||
|
||||
type item[T any] struct {
|
||||
base
|
||||
|
||||
comparator func(T, T) bool
|
||||
val *T
|
||||
}
|
||||
|
||||
func (b *item[T]) Get() (T, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
if b.val == nil {
|
||||
return *new(T), nil
|
||||
}
|
||||
return *b.val, nil
|
||||
}
|
||||
|
||||
func (b *item[T]) Set(val T) error {
|
||||
b.lock.Lock()
|
||||
equal := b.comparator(*b.val, val)
|
||||
*b.val = val
|
||||
b.lock.Unlock()
|
||||
|
||||
if !equal {
|
||||
b.trigger()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindExternalComparable[T bool | float64 | int | rune | string](val *T) ExternalItem[T] {
|
||||
return BindItem(val, func(t1, t2 T) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
type externalItem[T any] struct {
|
||||
item[T]
|
||||
|
||||
old T
|
||||
}
|
||||
|
||||
func (b *externalItem[T]) Set(val T) error {
|
||||
b.lock.Lock()
|
||||
if b.comparator(b.old, val) {
|
||||
b.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
*b.val = val
|
||||
b.old = val
|
||||
b.lock.Unlock()
|
||||
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *externalItem[T]) Reload() error {
|
||||
return b.Set(*b.val)
|
||||
}
|
||||
662
vendor/fyne.io/fyne/v2/data/binding/lists.go
generated
vendored
Normal file
662
vendor/fyne.io/fyne/v2/data/binding/lists.go
generated
vendored
Normal file
@ -0,0 +1,662 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
// DataList is the base interface for all bindable data lists.
|
||||
//
|
||||
// Since: 2.0
|
||||
type DataList interface {
|
||||
DataItem
|
||||
GetItem(index int) (DataItem, error)
|
||||
Length() int
|
||||
}
|
||||
|
||||
// BoolList supports binding a list of bool values.
|
||||
//
|
||||
// Since: 2.0
|
||||
type BoolList interface {
|
||||
DataList
|
||||
|
||||
Append(value bool) error
|
||||
Get() ([]bool, error)
|
||||
GetValue(index int) (bool, error)
|
||||
Prepend(value bool) error
|
||||
Remove(value bool) error
|
||||
Set(list []bool) error
|
||||
SetValue(index int, value bool) error
|
||||
}
|
||||
|
||||
// ExternalBoolList supports binding a list of bool values from an external variable.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalBoolList interface {
|
||||
BoolList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewBoolList returns a bindable list of bool values.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewBoolList() BoolList {
|
||||
return newListComparable[bool]()
|
||||
}
|
||||
|
||||
// BindBoolList returns a bound list of bool values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindBoolList(v *[]bool) ExternalBoolList {
|
||||
return bindListComparable(v)
|
||||
}
|
||||
|
||||
// BytesList supports binding a list of []byte values.
|
||||
//
|
||||
// Since: 2.2
|
||||
type BytesList interface {
|
||||
DataList
|
||||
|
||||
Append(value []byte) error
|
||||
Get() ([][]byte, error)
|
||||
GetValue(index int) ([]byte, error)
|
||||
Prepend(value []byte) error
|
||||
Remove(value []byte) error
|
||||
Set(list [][]byte) error
|
||||
SetValue(index int, value []byte) error
|
||||
}
|
||||
|
||||
// ExternalBytesList supports binding a list of []byte values from an external variable.
|
||||
//
|
||||
// Since: 2.2
|
||||
type ExternalBytesList interface {
|
||||
BytesList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewBytesList returns a bindable list of []byte values.
|
||||
//
|
||||
// Since: 2.2
|
||||
func NewBytesList() BytesList {
|
||||
return newList(bytes.Equal)
|
||||
}
|
||||
|
||||
// BindBytesList returns a bound list of []byte values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.2
|
||||
func BindBytesList(v *[][]byte) ExternalBytesList {
|
||||
return bindList(v, bytes.Equal)
|
||||
}
|
||||
|
||||
// FloatList supports binding a list of float64 values.
|
||||
//
|
||||
// Since: 2.0
|
||||
type FloatList interface {
|
||||
DataList
|
||||
|
||||
Append(value float64) error
|
||||
Get() ([]float64, error)
|
||||
GetValue(index int) (float64, error)
|
||||
Prepend(value float64) error
|
||||
Remove(value float64) error
|
||||
Set(list []float64) error
|
||||
SetValue(index int, value float64) error
|
||||
}
|
||||
|
||||
// ExternalFloatList supports binding a list of float64 values from an external variable.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalFloatList interface {
|
||||
FloatList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewFloatList returns a bindable list of float64 values.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewFloatList() FloatList {
|
||||
return newListComparable[float64]()
|
||||
}
|
||||
|
||||
// BindFloatList returns a bound list of float64 values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindFloatList(v *[]float64) ExternalFloatList {
|
||||
return bindListComparable(v)
|
||||
}
|
||||
|
||||
// IntList supports binding a list of int values.
|
||||
//
|
||||
// Since: 2.0
|
||||
type IntList interface {
|
||||
DataList
|
||||
|
||||
Append(value int) error
|
||||
Get() ([]int, error)
|
||||
GetValue(index int) (int, error)
|
||||
Prepend(value int) error
|
||||
Remove(value int) error
|
||||
Set(list []int) error
|
||||
SetValue(index int, value int) error
|
||||
}
|
||||
|
||||
// ExternalIntList supports binding a list of int values from an external variable.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalIntList interface {
|
||||
IntList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewIntList returns a bindable list of int values.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewIntList() IntList {
|
||||
return newListComparable[int]()
|
||||
}
|
||||
|
||||
// BindIntList returns a bound list of int values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindIntList(v *[]int) ExternalIntList {
|
||||
return bindListComparable(v)
|
||||
}
|
||||
|
||||
// RuneList supports binding a list of rune values.
|
||||
//
|
||||
// Since: 2.0
|
||||
type RuneList interface {
|
||||
DataList
|
||||
|
||||
Append(value rune) error
|
||||
Get() ([]rune, error)
|
||||
GetValue(index int) (rune, error)
|
||||
Prepend(value rune) error
|
||||
Remove(value rune) error
|
||||
Set(list []rune) error
|
||||
SetValue(index int, value rune) error
|
||||
}
|
||||
|
||||
// ExternalRuneList supports binding a list of rune values from an external variable.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalRuneList interface {
|
||||
RuneList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewRuneList returns a bindable list of rune values.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewRuneList() RuneList {
|
||||
return newListComparable[rune]()
|
||||
}
|
||||
|
||||
// BindRuneList returns a bound list of rune values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindRuneList(v *[]rune) ExternalRuneList {
|
||||
if v == nil {
|
||||
return NewRuneList().(ExternalRuneList)
|
||||
}
|
||||
|
||||
b := newListComparable[rune]()
|
||||
b.val = v
|
||||
b.updateExternal = true
|
||||
|
||||
for i := range *v {
|
||||
b.appendItem(bindListItemComparable(v, i, b.updateExternal))
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// StringList supports binding a list of string values.
|
||||
//
|
||||
// Since: 2.0
|
||||
type StringList interface {
|
||||
DataList
|
||||
|
||||
Append(value string) error
|
||||
Get() ([]string, error)
|
||||
GetValue(index int) (string, error)
|
||||
Prepend(value string) error
|
||||
Remove(value string) error
|
||||
Set(list []string) error
|
||||
SetValue(index int, value string) error
|
||||
}
|
||||
|
||||
// ExternalStringList supports binding a list of string values from an external variable.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalStringList interface {
|
||||
StringList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewStringList returns a bindable list of string values.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewStringList() StringList {
|
||||
return newListComparable[string]()
|
||||
}
|
||||
|
||||
// BindStringList returns a bound list of string values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindStringList(v *[]string) ExternalStringList {
|
||||
return bindListComparable(v)
|
||||
}
|
||||
|
||||
// UntypedList supports binding a list of any values.
|
||||
//
|
||||
// Since: 2.1
|
||||
type UntypedList interface {
|
||||
DataList
|
||||
|
||||
Append(value any) error
|
||||
Get() ([]any, error)
|
||||
GetValue(index int) (any, error)
|
||||
Prepend(value any) error
|
||||
Remove(value any) error
|
||||
Set(list []any) error
|
||||
SetValue(index int, value any) error
|
||||
}
|
||||
|
||||
// ExternalUntypedList supports binding a list of any values from an external variable.
|
||||
//
|
||||
// Since: 2.1
|
||||
type ExternalUntypedList interface {
|
||||
UntypedList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewUntypedList returns a bindable list of any values.
|
||||
//
|
||||
// Since: 2.1
|
||||
func NewUntypedList() UntypedList {
|
||||
return newList(func(t1, t2 any) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
// BindUntypedList returns a bound list of any values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.1
|
||||
func BindUntypedList(v *[]any) ExternalUntypedList {
|
||||
if v == nil {
|
||||
return NewUntypedList().(ExternalUntypedList)
|
||||
}
|
||||
|
||||
comparator := func(t1, t2 any) bool { return t1 == t2 }
|
||||
b := newExternalList(v, comparator)
|
||||
for i := range *v {
|
||||
b.appendItem(bindListItem(v, i, b.updateExternal, comparator))
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// URIList supports binding a list of fyne.URI values.
|
||||
//
|
||||
// Since: 2.1
|
||||
type URIList interface {
|
||||
DataList
|
||||
|
||||
Append(value fyne.URI) error
|
||||
Get() ([]fyne.URI, error)
|
||||
GetValue(index int) (fyne.URI, error)
|
||||
Prepend(value fyne.URI) error
|
||||
Remove(value fyne.URI) error
|
||||
Set(list []fyne.URI) error
|
||||
SetValue(index int, value fyne.URI) error
|
||||
}
|
||||
|
||||
// ExternalURIList supports binding a list of fyne.URI values from an external variable.
|
||||
//
|
||||
// Since: 2.1
|
||||
type ExternalURIList interface {
|
||||
URIList
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewURIList returns a bindable list of fyne.URI values.
|
||||
//
|
||||
// Since: 2.1
|
||||
func NewURIList() URIList {
|
||||
return newList(storage.EqualURI)
|
||||
}
|
||||
|
||||
// BindURIList returns a bound list of fyne.URI values, based on the contents of the passed slice.
|
||||
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.1
|
||||
func BindURIList(v *[]fyne.URI) ExternalURIList {
|
||||
return bindList(v, storage.EqualURI)
|
||||
}
|
||||
|
||||
type listBase struct {
|
||||
base
|
||||
items []DataItem
|
||||
}
|
||||
|
||||
// GetItem returns the DataItem at the specified index.
|
||||
func (b *listBase) GetItem(i int) (DataItem, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
if i < 0 || i >= len(b.items) {
|
||||
return nil, errOutOfBounds
|
||||
}
|
||||
|
||||
return b.items[i], nil
|
||||
}
|
||||
|
||||
// Length returns the number of items in this data list.
|
||||
func (b *listBase) Length() int {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
return len(b.items)
|
||||
}
|
||||
|
||||
func (b *listBase) appendItem(i DataItem) {
|
||||
b.items = append(b.items, i)
|
||||
}
|
||||
|
||||
func (b *listBase) deleteItem(i int) {
|
||||
b.items = append(b.items[:i], b.items[i+1:]...)
|
||||
}
|
||||
|
||||
func newList[T any](comparator func(T, T) bool) *boundList[T] {
|
||||
return &boundList[T]{val: new([]T), comparator: comparator}
|
||||
}
|
||||
|
||||
func newListComparable[T bool | float64 | int | rune | string]() *boundList[T] {
|
||||
return newList(func(t1, t2 T) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
func newExternalList[T any](v *[]T, comparator func(T, T) bool) *boundList[T] {
|
||||
return &boundList[T]{val: v, comparator: comparator, updateExternal: true}
|
||||
}
|
||||
|
||||
func bindList[T any](v *[]T, comparator func(T, T) bool) *boundList[T] {
|
||||
if v == nil {
|
||||
return newList(comparator)
|
||||
}
|
||||
|
||||
l := newExternalList(v, comparator)
|
||||
for i := range *v {
|
||||
l.appendItem(bindListItem(v, i, l.updateExternal, comparator))
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func bindListComparable[T bool | float64 | int | rune | string](v *[]T) *boundList[T] {
|
||||
return bindList(v, func(t1, t2 T) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
type boundList[T any] struct {
|
||||
listBase
|
||||
|
||||
comparator func(T, T) bool
|
||||
updateExternal bool
|
||||
val *[]T
|
||||
|
||||
parentListener func(int)
|
||||
}
|
||||
|
||||
func (l *boundList[T]) Append(val T) error {
|
||||
l.lock.Lock()
|
||||
*l.val = append(*l.val, val)
|
||||
|
||||
trigger, err := l.doReload()
|
||||
l.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
l.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *boundList[T]) Get() ([]T, error) {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
return *l.val, nil
|
||||
}
|
||||
|
||||
func (l *boundList[T]) GetValue(i int) (T, error) {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
if i < 0 || i >= l.Length() {
|
||||
return *new(T), errOutOfBounds
|
||||
}
|
||||
|
||||
return (*l.val)[i], nil
|
||||
}
|
||||
|
||||
func (l *boundList[T]) Prepend(val T) error {
|
||||
l.lock.Lock()
|
||||
*l.val = append([]T{val}, *l.val...)
|
||||
|
||||
trigger, err := l.doReload()
|
||||
l.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
l.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *boundList[T]) Reload() error {
|
||||
l.lock.Lock()
|
||||
trigger, err := l.doReload()
|
||||
l.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
l.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *boundList[T]) Remove(val T) error {
|
||||
l.lock.Lock()
|
||||
|
||||
v := *l.val
|
||||
if len(v) == 0 {
|
||||
l.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
if l.comparator(v[0], val) {
|
||||
*l.val = v[1:]
|
||||
} else if l.comparator(v[len(v)-1], val) {
|
||||
*l.val = v[:len(v)-1]
|
||||
} else {
|
||||
id := -1
|
||||
for i, v := range v {
|
||||
if l.comparator(v, val) {
|
||||
id = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if id == -1 {
|
||||
l.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
*l.val = append(v[:id], v[id+1:]...)
|
||||
}
|
||||
|
||||
trigger, err := l.doReload()
|
||||
l.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
l.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *boundList[T]) Set(v []T) error {
|
||||
l.lock.Lock()
|
||||
*l.val = v
|
||||
trigger, err := l.doReload()
|
||||
l.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
l.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *boundList[T]) doReload() (trigger bool, retErr error) {
|
||||
oldLen := len(l.items)
|
||||
newLen := len(*l.val)
|
||||
if oldLen > newLen {
|
||||
for i := oldLen - 1; i >= newLen; i-- {
|
||||
l.deleteItem(i)
|
||||
}
|
||||
trigger = true
|
||||
} else if oldLen < newLen {
|
||||
for i := oldLen; i < newLen; i++ {
|
||||
item := bindListItem(l.val, i, l.updateExternal, l.comparator)
|
||||
|
||||
if l.parentListener != nil {
|
||||
index := i
|
||||
item.AddListener(NewDataListener(func() {
|
||||
l.parentListener(index)
|
||||
}))
|
||||
}
|
||||
|
||||
l.appendItem(item)
|
||||
}
|
||||
trigger = true
|
||||
}
|
||||
|
||||
for i, item := range l.items {
|
||||
if i > oldLen || i > newLen {
|
||||
break
|
||||
}
|
||||
|
||||
var err error
|
||||
if l.updateExternal {
|
||||
err = item.(*boundExternalListItem[T]).setIfChanged((*l.val)[i])
|
||||
} else {
|
||||
err = item.(*boundListItem[T]).doSet((*l.val)[i])
|
||||
}
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *boundList[T]) SetValue(i int, v T) error {
|
||||
l.lock.RLock()
|
||||
len := l.Length()
|
||||
l.lock.RUnlock()
|
||||
|
||||
if i < 0 || i >= len {
|
||||
return errOutOfBounds
|
||||
}
|
||||
|
||||
l.lock.Lock()
|
||||
(*l.val)[i] = v
|
||||
l.lock.Unlock()
|
||||
|
||||
item, err := l.GetItem(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return item.(Item[T]).Set(v)
|
||||
}
|
||||
|
||||
func bindListItem[T any](v *[]T, i int, external bool, comparator func(T, T) bool) Item[T] {
|
||||
if external {
|
||||
ret := &boundExternalListItem[T]{old: (*v)[i]}
|
||||
ret.val = v
|
||||
ret.index = i
|
||||
ret.comparator = comparator
|
||||
return ret
|
||||
}
|
||||
|
||||
return &boundListItem[T]{val: v, index: i, comparator: comparator}
|
||||
}
|
||||
|
||||
func bindListItemComparable[T bool | float64 | int | rune | string](v *[]T, i int, external bool) Item[T] {
|
||||
return bindListItem(v, i, external, func(t1, t2 T) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
type boundListItem[T any] struct {
|
||||
base
|
||||
|
||||
comparator func(T, T) bool
|
||||
val *[]T
|
||||
index int
|
||||
}
|
||||
|
||||
func (b *boundListItem[T]) Get() (T, error) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
if b.index < 0 || b.index >= len(*b.val) {
|
||||
return *new(T), errOutOfBounds
|
||||
}
|
||||
|
||||
return (*b.val)[b.index], nil
|
||||
}
|
||||
|
||||
func (b *boundListItem[T]) Set(val T) error {
|
||||
return b.doSet(val)
|
||||
}
|
||||
|
||||
func (b *boundListItem[T]) doSet(val T) error {
|
||||
b.lock.Lock()
|
||||
(*b.val)[b.index] = val
|
||||
b.lock.Unlock()
|
||||
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type boundExternalListItem[T any] struct {
|
||||
boundListItem[T]
|
||||
|
||||
old T
|
||||
}
|
||||
|
||||
func (b *boundExternalListItem[T]) setIfChanged(val T) error {
|
||||
b.lock.Lock()
|
||||
if b.comparator(val, b.old) {
|
||||
b.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
(*b.val)[b.index] = val
|
||||
b.old = val
|
||||
|
||||
b.lock.Unlock()
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
522
vendor/fyne.io/fyne/v2/data/binding/maps.go
generated
vendored
Normal file
522
vendor/fyne.io/fyne/v2/data/binding/maps.go
generated
vendored
Normal file
@ -0,0 +1,522 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// DataMap is the base interface for all bindable data maps.
|
||||
//
|
||||
// Since: 2.0
|
||||
type DataMap interface {
|
||||
DataItem
|
||||
GetItem(string) (DataItem, error)
|
||||
Keys() []string
|
||||
}
|
||||
|
||||
// ExternalUntypedMap is a map data binding with all values untyped (any), connected to an external data source.
|
||||
//
|
||||
// Since: 2.0
|
||||
type ExternalUntypedMap interface {
|
||||
UntypedMap
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// UntypedMap is a map data binding with all values Untyped (any).
|
||||
//
|
||||
// Since: 2.0
|
||||
type UntypedMap interface {
|
||||
DataMap
|
||||
Delete(string)
|
||||
Get() (map[string]any, error)
|
||||
GetValue(string) (any, error)
|
||||
Set(map[string]any) error
|
||||
SetValue(string, any) error
|
||||
}
|
||||
|
||||
// NewUntypedMap creates a new, empty map binding of string to any.
|
||||
//
|
||||
// Since: 2.0
|
||||
func NewUntypedMap() UntypedMap {
|
||||
return &mapBase{items: make(map[string]reflectUntyped), val: &map[string]any{}}
|
||||
}
|
||||
|
||||
// BindUntypedMap creates a new map binding of string to any based on the data passed.
|
||||
// If your code changes the content of the map this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindUntypedMap(d *map[string]any) ExternalUntypedMap {
|
||||
if d == nil {
|
||||
return NewUntypedMap().(ExternalUntypedMap)
|
||||
}
|
||||
m := &mapBase{items: make(map[string]reflectUntyped), val: d, updateExternal: true}
|
||||
|
||||
for k := range *d {
|
||||
m.setItem(k, bindUntypedMapValue(d, k, m.updateExternal))
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Struct is the base interface for a bound struct type.
|
||||
//
|
||||
// Since: 2.0
|
||||
type Struct interface {
|
||||
DataMap
|
||||
GetValue(string) (any, error)
|
||||
SetValue(string, any) error
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// BindStruct creates a new map binding of string to any using the struct passed as data.
|
||||
// The key for each item is a string representation of each exported field with the value set as an any.
|
||||
// Only exported fields are included.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindStruct(i any) Struct {
|
||||
if i == nil {
|
||||
return NewUntypedMap().(Struct)
|
||||
}
|
||||
t := reflect.TypeOf(i)
|
||||
if t.Kind() != reflect.Ptr ||
|
||||
(reflect.TypeOf(reflect.ValueOf(i).Elem()).Kind() != reflect.Struct) {
|
||||
fyne.LogError("Invalid type passed to BindStruct, must be pointer to struct", nil)
|
||||
return NewUntypedMap().(Struct)
|
||||
}
|
||||
|
||||
s := &boundStruct{orig: i}
|
||||
s.items = make(map[string]reflectUntyped)
|
||||
s.val = &map[string]any{}
|
||||
s.updateExternal = true
|
||||
|
||||
v := reflect.ValueOf(i).Elem()
|
||||
t = v.Type()
|
||||
for j := 0; j < v.NumField(); j++ {
|
||||
f := v.Field(j)
|
||||
if !f.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
key := t.Field(j).Name
|
||||
s.items[key] = bindReflect(f)
|
||||
(*s.val)[key] = f.Interface()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type reflectUntyped interface {
|
||||
DataItem
|
||||
get() (any, error)
|
||||
set(any) error
|
||||
}
|
||||
|
||||
type mapBase struct {
|
||||
base
|
||||
|
||||
updateExternal bool
|
||||
items map[string]reflectUntyped
|
||||
val *map[string]any
|
||||
}
|
||||
|
||||
func (b *mapBase) GetItem(key string) (DataItem, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
if v, ok := b.items[key]; ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, errKeyNotFound
|
||||
}
|
||||
|
||||
func (b *mapBase) Keys() []string {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
ret := make([]string, len(b.items))
|
||||
i := 0
|
||||
for k := range b.items {
|
||||
ret[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *mapBase) Delete(key string) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
delete(b.items, key)
|
||||
|
||||
b.trigger()
|
||||
}
|
||||
|
||||
func (b *mapBase) Get() (map[string]any, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
if b.val == nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
return *b.val, nil
|
||||
}
|
||||
|
||||
func (b *mapBase) GetValue(key string) (any, error) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
if i, ok := b.items[key]; ok {
|
||||
return i.get()
|
||||
}
|
||||
|
||||
return nil, errKeyNotFound
|
||||
}
|
||||
|
||||
func (b *mapBase) Reload() error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
return b.doReload()
|
||||
}
|
||||
|
||||
func (b *mapBase) Set(v map[string]any) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
if b.val == nil { // was not initialized with a blank value, recover
|
||||
b.val = &v
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
*b.val = v
|
||||
return b.doReload()
|
||||
}
|
||||
|
||||
func (b *mapBase) SetValue(key string, d any) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
if i, ok := b.items[key]; ok {
|
||||
return i.set(d)
|
||||
}
|
||||
|
||||
(*b.val)[key] = d
|
||||
item := bindUntypedMapValue(b.val, key, b.updateExternal)
|
||||
b.setItem(key, item)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *mapBase) doReload() (retErr error) {
|
||||
changed := false
|
||||
// add new
|
||||
for key := range *b.val {
|
||||
_, found := b.items[key]
|
||||
if !found {
|
||||
b.setItem(key, bindUntypedMapValue(b.val, key, b.updateExternal))
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// remove old
|
||||
for key := range b.items {
|
||||
_, found := (*b.val)[key]
|
||||
if !found {
|
||||
delete(b.items, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
b.trigger()
|
||||
}
|
||||
|
||||
for k, item := range b.items {
|
||||
var err error
|
||||
|
||||
if b.updateExternal {
|
||||
err = item.(*boundExternalMapValue).setIfChanged((*b.val)[k])
|
||||
} else {
|
||||
err = item.(*boundMapValue).set((*b.val)[k])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *mapBase) setItem(key string, d reflectUntyped) {
|
||||
b.items[key] = d
|
||||
|
||||
b.trigger()
|
||||
}
|
||||
|
||||
type boundStruct struct {
|
||||
mapBase
|
||||
|
||||
orig any
|
||||
}
|
||||
|
||||
func (b *boundStruct) Reload() (retErr error) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
v := reflect.ValueOf(b.orig).Elem()
|
||||
t := v.Type()
|
||||
for j := 0; j < v.NumField(); j++ {
|
||||
f := v.Field(j)
|
||||
if !f.CanSet() {
|
||||
continue
|
||||
}
|
||||
kind := f.Kind()
|
||||
if kind == reflect.Slice || kind == reflect.Struct {
|
||||
fyne.LogError("Data binding does not yet support slice or struct elements in a struct", nil)
|
||||
continue
|
||||
}
|
||||
|
||||
key := t.Field(j).Name
|
||||
old := (*b.val)[key]
|
||||
if f.Interface() == old {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
err = b.items[key].(*reflectBool).Set(f.Bool())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
err = b.items[key].(*reflectFloat).Set(f.Float())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
err = b.items[key].(*reflectInt).Set(int(f.Int()))
|
||||
case reflect.String:
|
||||
err = b.items[key].(*reflectString).Set(f.String())
|
||||
}
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
(*b.val)[key] = f.Interface()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func bindUntypedMapValue(m *map[string]any, k string, external bool) reflectUntyped {
|
||||
if external {
|
||||
ret := &boundExternalMapValue{old: (*m)[k]}
|
||||
ret.val = m
|
||||
ret.key = k
|
||||
return ret
|
||||
}
|
||||
|
||||
return &boundMapValue{val: m, key: k}
|
||||
}
|
||||
|
||||
type boundMapValue struct {
|
||||
base
|
||||
|
||||
val *map[string]any
|
||||
key string
|
||||
}
|
||||
|
||||
func (b *boundMapValue) get() (any, error) {
|
||||
if v, ok := (*b.val)[b.key]; ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, errKeyNotFound
|
||||
}
|
||||
|
||||
func (b *boundMapValue) set(val any) error {
|
||||
(*b.val)[b.key] = val
|
||||
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type boundExternalMapValue struct {
|
||||
boundMapValue
|
||||
|
||||
old any
|
||||
}
|
||||
|
||||
func (b *boundExternalMapValue) setIfChanged(val any) error {
|
||||
if val == b.old {
|
||||
return nil
|
||||
}
|
||||
b.old = val
|
||||
|
||||
return b.set(val)
|
||||
}
|
||||
|
||||
type boundReflect struct {
|
||||
base
|
||||
|
||||
val reflect.Value
|
||||
}
|
||||
|
||||
func (b *boundReflect) get() (any, error) {
|
||||
return b.val.Interface(), nil
|
||||
}
|
||||
|
||||
func (b *boundReflect) set(val any) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("unable to set bool in data binding")
|
||||
}
|
||||
}()
|
||||
b.val.Set(reflect.ValueOf(val))
|
||||
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type reflectBool struct {
|
||||
boundReflect
|
||||
}
|
||||
|
||||
func (r *reflectBool) Get() (val bool, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("invalid bool value in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
val = r.val.Bool()
|
||||
return
|
||||
}
|
||||
|
||||
func (r *reflectBool) Set(b bool) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("unable to set bool in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
r.val.SetBool(b)
|
||||
r.trigger()
|
||||
return
|
||||
}
|
||||
|
||||
func bindReflectBool(f reflect.Value) reflectUntyped {
|
||||
r := &reflectBool{}
|
||||
r.val = f
|
||||
return r
|
||||
}
|
||||
|
||||
type reflectFloat struct {
|
||||
boundReflect
|
||||
}
|
||||
|
||||
func (r *reflectFloat) Get() (val float64, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("invalid float64 value in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
val = r.val.Float()
|
||||
return
|
||||
}
|
||||
|
||||
func (r *reflectFloat) Set(f float64) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("unable to set float64 in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
r.val.SetFloat(f)
|
||||
r.trigger()
|
||||
return
|
||||
}
|
||||
|
||||
func bindReflectFloat(f reflect.Value) reflectUntyped {
|
||||
r := &reflectFloat{}
|
||||
r.val = f
|
||||
return r
|
||||
}
|
||||
|
||||
type reflectInt struct {
|
||||
boundReflect
|
||||
}
|
||||
|
||||
func (r *reflectInt) Get() (val int, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("invalid int value in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
val = int(r.val.Int())
|
||||
return
|
||||
}
|
||||
|
||||
func (r *reflectInt) Set(i int) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("unable to set int in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
r.val.SetInt(int64(i))
|
||||
r.trigger()
|
||||
return
|
||||
}
|
||||
|
||||
func bindReflectInt(f reflect.Value) reflectUntyped {
|
||||
r := &reflectInt{}
|
||||
r.val = f
|
||||
return r
|
||||
}
|
||||
|
||||
type reflectString struct {
|
||||
boundReflect
|
||||
}
|
||||
|
||||
func (r *reflectString) Get() (val string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("invalid string value in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
val = r.val.String()
|
||||
return
|
||||
}
|
||||
|
||||
func (r *reflectString) Set(s string) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("unable to set string in data binding")
|
||||
}
|
||||
}()
|
||||
|
||||
r.val.SetString(s)
|
||||
r.trigger()
|
||||
return
|
||||
}
|
||||
|
||||
func bindReflectString(f reflect.Value) reflectUntyped {
|
||||
r := &reflectString{}
|
||||
r.val = f
|
||||
return r
|
||||
}
|
||||
|
||||
func bindReflect(field reflect.Value) reflectUntyped {
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
return bindReflectBool(field)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return bindReflectFloat(field)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return bindReflectInt(field)
|
||||
case reflect.String:
|
||||
return bindReflectString(field)
|
||||
}
|
||||
return &boundReflect{val: field}
|
||||
}
|
||||
97
vendor/fyne.io/fyne/v2/data/binding/pref_helper.go
generated
vendored
Normal file
97
vendor/fyne.io/fyne/v2/data/binding/pref_helper.go
generated
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/internal/async"
|
||||
)
|
||||
|
||||
type preferenceItem interface {
|
||||
checkForChange()
|
||||
}
|
||||
|
||||
type preferenceBindings struct {
|
||||
async.Map[string, preferenceItem]
|
||||
}
|
||||
|
||||
func (b *preferenceBindings) list() []preferenceItem {
|
||||
ret := []preferenceItem{}
|
||||
b.Range(func(_ string, item preferenceItem) bool {
|
||||
ret = append(ret, item)
|
||||
return true
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
type preferencesMap struct {
|
||||
prefs async.Map[fyne.Preferences, *preferenceBindings]
|
||||
|
||||
appPrefs fyne.Preferences // the main application prefs, to check if it changed...
|
||||
appLock sync.Mutex
|
||||
}
|
||||
|
||||
func newPreferencesMap() *preferencesMap {
|
||||
return &preferencesMap{}
|
||||
}
|
||||
|
||||
func (m *preferencesMap) ensurePreferencesAttached(p fyne.Preferences) *preferenceBindings {
|
||||
binds, loaded := m.prefs.LoadOrStore(p, &preferenceBindings{})
|
||||
if loaded {
|
||||
return binds
|
||||
}
|
||||
|
||||
p.AddChangeListener(func() { m.preferencesChanged(p) })
|
||||
return binds
|
||||
}
|
||||
|
||||
func (m *preferencesMap) getBindings(p fyne.Preferences) *preferenceBindings {
|
||||
if p == fyne.CurrentApp().Preferences() {
|
||||
m.appLock.Lock()
|
||||
prefs := m.appPrefs
|
||||
if m.appPrefs == nil {
|
||||
m.appPrefs = p
|
||||
}
|
||||
m.appLock.Unlock()
|
||||
if prefs != p {
|
||||
m.migratePreferences(prefs, p)
|
||||
}
|
||||
}
|
||||
binds, _ := m.prefs.Load(p)
|
||||
return binds
|
||||
}
|
||||
|
||||
func (m *preferencesMap) preferencesChanged(p fyne.Preferences) {
|
||||
binds := m.getBindings(p)
|
||||
if binds == nil {
|
||||
return
|
||||
}
|
||||
for _, item := range binds.list() {
|
||||
item.checkForChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *preferencesMap) migratePreferences(src, dst fyne.Preferences) {
|
||||
old, loaded := m.prefs.Load(src)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
|
||||
m.prefs.Store(dst, old)
|
||||
m.prefs.Delete(src)
|
||||
m.appLock.Lock()
|
||||
m.appPrefs = dst
|
||||
m.appLock.Unlock()
|
||||
|
||||
binds := m.getBindings(dst)
|
||||
if binds == nil {
|
||||
return
|
||||
}
|
||||
for _, b := range binds.list() {
|
||||
if backed, ok := b.(interface{ replaceProvider(fyne.Preferences) }); ok {
|
||||
backed.replaceProvider(dst)
|
||||
}
|
||||
}
|
||||
|
||||
m.preferencesChanged(dst)
|
||||
}
|
||||
261
vendor/fyne.io/fyne/v2/data/binding/preference.go
generated
vendored
Normal file
261
vendor/fyne.io/fyne/v2/data/binding/preference.go
generated
vendored
Normal file
@ -0,0 +1,261 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// Work around Go not supporting generic methods on non-generic types:
|
||||
type preferenceLookupSetter[T any] func(fyne.Preferences) (func(string) T, func(string, T))
|
||||
|
||||
const keyTypeMismatchError = "A previous preference binding exists with different type for key: "
|
||||
|
||||
// BindPreferenceBool returns a bindable bool value that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindPreferenceBool(key string, p fyne.Preferences) Bool {
|
||||
return bindPreferenceItem(key, p,
|
||||
func(p fyne.Preferences) (func(string) bool, func(string, bool)) {
|
||||
return p.Bool, p.SetBool
|
||||
})
|
||||
}
|
||||
|
||||
// BindPreferenceBoolList returns a bound list of bool values that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.6
|
||||
func BindPreferenceBoolList(key string, p fyne.Preferences) BoolList {
|
||||
return bindPreferenceListComparable[bool](key, p,
|
||||
func(p fyne.Preferences) (func(string) []bool, func(string, []bool)) {
|
||||
return p.BoolList, p.SetBoolList
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// BindPreferenceFloat returns a bindable float64 value that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindPreferenceFloat(key string, p fyne.Preferences) Float {
|
||||
return bindPreferenceItem(key, p,
|
||||
func(p fyne.Preferences) (func(string) float64, func(string, float64)) {
|
||||
return p.Float, p.SetFloat
|
||||
})
|
||||
}
|
||||
|
||||
// BindPreferenceFloatList returns a bound list of float64 values that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.6
|
||||
func BindPreferenceFloatList(key string, p fyne.Preferences) FloatList {
|
||||
return bindPreferenceListComparable[float64](key, p,
|
||||
func(p fyne.Preferences) (func(string) []float64, func(string, []float64)) {
|
||||
return p.FloatList, p.SetFloatList
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// BindPreferenceInt returns a bindable int value that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindPreferenceInt(key string, p fyne.Preferences) Int {
|
||||
return bindPreferenceItem(key, p,
|
||||
func(p fyne.Preferences) (func(string) int, func(string, int)) {
|
||||
return p.Int, p.SetInt
|
||||
})
|
||||
}
|
||||
|
||||
// BindPreferenceIntList returns a bound list of int values that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.6
|
||||
func BindPreferenceIntList(key string, p fyne.Preferences) IntList {
|
||||
return bindPreferenceListComparable[int](key, p,
|
||||
func(p fyne.Preferences) (func(string) []int, func(string, []int)) {
|
||||
return p.IntList, p.SetIntList
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// BindPreferenceString returns a bindable string value that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.0
|
||||
func BindPreferenceString(key string, p fyne.Preferences) String {
|
||||
return bindPreferenceItem(key, p,
|
||||
func(p fyne.Preferences) (func(string) string, func(string, string)) {
|
||||
return p.String, p.SetString
|
||||
})
|
||||
}
|
||||
|
||||
// BindPreferenceStringList returns a bound list of string values that is managed by the application preferences.
|
||||
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
|
||||
//
|
||||
// Since: 2.6
|
||||
func BindPreferenceStringList(key string, p fyne.Preferences) StringList {
|
||||
return bindPreferenceListComparable[string](key, p,
|
||||
func(p fyne.Preferences) (func(string) []string, func(string, []string)) {
|
||||
return p.StringList, p.SetStringList
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func bindPreferenceItem[T bool | float64 | int | string](key string, p fyne.Preferences, setLookup preferenceLookupSetter[T]) Item[T] {
|
||||
if found, ok := lookupExistingBinding[T](key, p); ok {
|
||||
return found
|
||||
}
|
||||
|
||||
listen := &prefBoundBase[T]{key: key, setLookup: setLookup}
|
||||
listen.replaceProvider(p)
|
||||
binds := prefBinds.ensurePreferencesAttached(p)
|
||||
binds.Store(key, listen)
|
||||
return listen
|
||||
}
|
||||
|
||||
func lookupExistingBinding[T any](key string, p fyne.Preferences) (Item[T], bool) {
|
||||
binds := prefBinds.getBindings(p)
|
||||
if binds == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if listen, ok := binds.Load(key); listen != nil && ok {
|
||||
if l, ok := listen.(Item[T]); ok {
|
||||
return l, ok
|
||||
}
|
||||
fyne.LogError(keyTypeMismatchError+key, nil)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func lookupExistingListBinding[T bool | float64 | int | string](key string, p fyne.Preferences) (*prefBoundList[T], bool) {
|
||||
binds := prefBinds.getBindings(p)
|
||||
if binds == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if listen, ok := binds.Load(key); listen != nil && ok {
|
||||
if l, ok := listen.(*prefBoundList[T]); ok {
|
||||
return l, ok
|
||||
}
|
||||
fyne.LogError(keyTypeMismatchError+key, nil)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type prefBoundBase[T bool | float64 | int | string] struct {
|
||||
base
|
||||
key string
|
||||
|
||||
get func(string) T
|
||||
set func(string, T)
|
||||
setLookup preferenceLookupSetter[T]
|
||||
cache atomic.Pointer[T]
|
||||
}
|
||||
|
||||
func (b *prefBoundBase[T]) Get() (T, error) {
|
||||
cache := b.get(b.key)
|
||||
b.cache.Store(&cache)
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
func (b *prefBoundBase[T]) Set(v T) error {
|
||||
b.set(b.key, v)
|
||||
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
b.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *prefBoundBase[T]) checkForChange() {
|
||||
val := b.cache.Load()
|
||||
if val != nil && b.get(b.key) == *val {
|
||||
return
|
||||
}
|
||||
b.trigger()
|
||||
}
|
||||
|
||||
func (b *prefBoundBase[T]) replaceProvider(p fyne.Preferences) {
|
||||
b.get, b.set = b.setLookup(p)
|
||||
}
|
||||
|
||||
type prefBoundList[T bool | float64 | int | string] struct {
|
||||
boundList[T]
|
||||
key string
|
||||
|
||||
get func(string) []T
|
||||
set func(string, []T)
|
||||
setLookup preferenceLookupSetter[[]T]
|
||||
}
|
||||
|
||||
func (b *prefBoundList[T]) checkForChange() {
|
||||
val := *b.val
|
||||
updated := b.get(b.key)
|
||||
if val == nil || len(updated) != len(val) {
|
||||
b.Set(updated)
|
||||
return
|
||||
}
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// incoming changes to a preference list are not at the child level
|
||||
for i, v := range val {
|
||||
if i >= len(updated) {
|
||||
break
|
||||
}
|
||||
|
||||
if !b.comparator(v, updated[i]) {
|
||||
_ = b.items[i].(Item[T]).Set(updated[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *prefBoundList[T]) replaceProvider(p fyne.Preferences) {
|
||||
b.get, b.set = b.setLookup(p)
|
||||
}
|
||||
|
||||
type internalPrefs = interface{ WriteValues(func(map[string]any)) }
|
||||
|
||||
func bindPreferenceListComparable[T bool | float64 | int | string](key string, p fyne.Preferences,
|
||||
setLookup preferenceLookupSetter[[]T]) *prefBoundList[T] {
|
||||
if found, ok := lookupExistingListBinding[T](key, p); ok {
|
||||
return found
|
||||
}
|
||||
|
||||
listen := &prefBoundList[T]{key: key, setLookup: setLookup}
|
||||
listen.replaceProvider(p)
|
||||
|
||||
items := listen.get(listen.key)
|
||||
listen.boundList = *bindList(nil, func(t1, t2 T) bool { return t1 == t2 })
|
||||
|
||||
listen.boundList.AddListener(NewDataListener(func() {
|
||||
cached := *listen.val
|
||||
replaced := listen.get(listen.key)
|
||||
if len(cached) == len(replaced) {
|
||||
return
|
||||
}
|
||||
|
||||
listen.set(listen.key, *listen.val)
|
||||
listen.trigger()
|
||||
}))
|
||||
|
||||
listen.boundList.parentListener = func(index int) {
|
||||
listen.set(listen.key, *listen.val)
|
||||
|
||||
// the child changes are not seen on the write end so force it
|
||||
if prefs, ok := p.(internalPrefs); ok {
|
||||
prefs.WriteValues(func(map[string]any) {})
|
||||
}
|
||||
}
|
||||
listen.boundList.Set(items)
|
||||
|
||||
binds := prefBinds.ensurePreferencesAttached(p)
|
||||
binds.Store(key, listen)
|
||||
return listen
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user