diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd3de7649..c9f4488539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,33 @@ This file lists the main changes with each version of the Fyne toolkit. More detailed release notes can be found on the [releases page](https://github.com/fyne-io/fyne/releases). +## 2.4.1 - 8 October 2023 + +### Fixed + +* Left key on tree now collapses open branch +* Avoid memory leak in Android driver code +* Entry Field on Android in Landscape Mode Shows "0" (#4036) +* DocTabs Indicator remains visible after last tab is removed (#4220) +* Some SVG resources don't update appearance correctly with the theme (#3900) +* Fix mobile simulation builds on OpenBSD +* Fix alignment of menu button on mobile +* Fix Compilation with Android NDK r26 +* Clicking table headers causes high CPU consumption (#4264) +* Frequent clicking on table may cause the program to not respond (#4210) +* Application stops responding when scrolling a table (#4263) +* Possible crash parsing malformed JSON color (#4270) +* NewFolderOpen: incomplete filenames (#2165) +* Resolve issue where storage.List could crash with short URI (#4271) +* TextTruncateEllipsis abnormally truncates strings with multi-byte UTF-8 characters (#4283) +* Last character doesn't appear in Select when there is a special character (#4293) +* Resolve random crash in DocTab (#3909) +* Selecting items from a list caused the keyboard to popup on Android (#4236) + + ## 2.4.0 - 1 September 2023 -## Added +### Added * Rounded corners in rectangle (#1090) * Support for emoji in text @@ -43,7 +67,7 @@ More detailed release notes can be found on the [releases page](https://github.c * Add `--pprof` option to fyne build commands to enable profiling * Support compiling from Android (termux) -## Changed +### Changed * Go 1.17 or later is now required. * Theme updated for rounded corners on buttons and input widgets @@ -60,7 +84,7 @@ More detailed release notes can be found on the [releases page](https://github.c * Improving performance of lookup for theme data * Improved application startup time -## Fixed +### Fixed * Rendering performance enhancements * `dialog.NewProgressInfinite` is deprecated, but dialog.NewCustom isn't equivalent diff --git a/app/app_mobile_and.c b/app/app_mobile_and.c index 4f5aecdade..cbe6dc2336 100644 --- a/app/app_mobile_and.c +++ b/app/app_mobile_and.c @@ -42,10 +42,10 @@ 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, ctx); + 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, ctx, getSystemService, serviceStr); + return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, serviceStr); } int nextId = 1; @@ -81,7 +81,7 @@ void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url) { jclass contextClass = find_class(env, "android/content/Context"); jmethodID start = find_method(env, contextClass, "startActivity", "(Landroid/content/Intent;)V"); - (*env)->CallVoidMethod(env, ctx, start, intent); + (*env)->CallVoidMethod(env, (jobject)ctx, start, intent); } void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *body) { @@ -94,7 +94,7 @@ void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char jobject builder = (*env)->NewObject(env, cls, constructor, ctx); jclass mgrCls = find_class(env, "android/app/NotificationManager"); - jobject mgr = getSystemService(env, ctx, "notification"); + jobject mgr = getSystemService((uintptr_t)env, ctx, "notification"); if (isOreoOrLater(env)) { jstring channelId = (*env)->NewStringUTF(env, "fyne-notif"); @@ -128,4 +128,4 @@ void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char jmethodID notify = find_method(env, mgrCls, "notify", "(ILandroid/app/Notification;)V"); (*env)->CallVoidMethod(env, mgr, notify, nextId, notif); nextId++; -} \ No newline at end of file +} diff --git a/cmd/fyne/commands/command.go b/cmd/fyne/commands/command.go index fc1f3e2832..43a34197af 100644 --- a/cmd/fyne/commands/command.go +++ b/cmd/fyne/commands/command.go @@ -17,35 +17,35 @@ type Command interface { type Getter = commands.Getter // NewGetter returns a command that can handle the download and install of GUI apps built using Fyne. -// It depends on a Go and C compiler installed at this stage and takes a single, package, parameter to identify the app. +// It depends on a Go and C compiler installed. func NewGetter() *Getter { - return &Getter{} + return commands.NewGetter() } // NewBundler returns a command that can bundle resources into Go code. // // Deprecated: A better version will be exposed in the future. func NewBundler() Command { - return &commands.Bundler{} + return commands.NewBundler() } // NewInstaller returns an install command that can install locally built Fyne apps. // // Deprecated: A better version will be exposed in the future. func NewInstaller() Command { - return &commands.Installer{} + return commands.NewInstaller() } // NewPackager returns a packager command that can wrap executables into full GUI app packages. // // Deprecated: A better version will be exposed in the future. func NewPackager() Command { - return &commands.Packager{} + return commands.NewPackager() } // NewReleaser returns a command that can adapt app packages for distribution. // // Deprecated: A better version will be exposed in the future. func NewReleaser() Command { - return &commands.Releaser{} + return commands.NewReleaser() } diff --git a/cmd/fyne/commands/command_test.go b/cmd/fyne/commands/command_test.go new file mode 100644 index 0000000000..f01b566ea7 --- /dev/null +++ b/cmd/fyne/commands/command_test.go @@ -0,0 +1,8 @@ +package commands + +import "testing" + +func TestNewGetter(t *testing.T) { + g := NewGetter() + g.SetAppID("io.fyne.text") // would crash if not set up internally correctly +} diff --git a/cmd/fyne/internal/commands/build.go b/cmd/fyne/internal/commands/build.go index 5cc55c59ec..83881cf6ad 100644 --- a/cmd/fyne/internal/commands/build.go +++ b/cmd/fyne/internal/commands/build.go @@ -33,9 +33,14 @@ type Builder struct { runner runner } +// NewBuilder returns a command that can handle the build of GUI apps built using Fyne. +func NewBuilder() *Builder { + return &Builder{appData: &appData{}} +} + // Build returns the cli command for building fyne applications func Build() *cli.Command { - b := &Builder{appData: &appData{}} + b := NewBuilder() return &cli.Command{ Name: "build", @@ -137,7 +142,7 @@ func checkVersion(output string, versionConstraint *version.ConstraintGroup) err } func isWeb(goos string) bool { - return goos == "gopherjs" || goos == "wasm" + return goos == "js" || goos == "wasm" } func checkGoVersion(runner runner, versionConstraint *version.ConstraintGroup) error { @@ -197,7 +202,7 @@ func (b *Builder) build() error { goos = targetOS() } - if goos == "gopherjs" && runtime.GOOS == "windows" { + if goos == "js" && runtime.GOOS == "windows" { return errors.New("gopherjs doesn't support Windows. Only wasm target is supported for the web output. You can also use fyne-cross to solve this") } @@ -262,7 +267,7 @@ func (b *Builder) build() error { tags = append(tags, "release") } if len(tags) > 0 { - if goos == "gopherjs" { + if goos == "js" { args = append(args, "--tags") } else { args = append(args, "-tags") @@ -280,7 +285,7 @@ func (b *Builder) build() error { versionConstraint = version.NewConstrainGroupFromString(">=1.17") env = append(env, "GOARCH=wasm") env = append(env, "GOOS=js") - } else if goos == "gopherjs" { + } else if goos == "js" { _, err := b.runner.runOutput("version") if err != nil { fmt.Fprintf(os.Stderr, "Can not execute `gopherjs version`. Please do `go install github.com/gopherjs/gopherjs@latest`.\n") @@ -359,7 +364,7 @@ func (b *Builder) updateAndGetGoExecutable(goos string) runner { fyneGoModRunner = newCommand(goBin) b.runner = fyneGoModRunner } else { - if goos != "gopherjs" { + if goos != "js" { b.runner = newCommand("go") } else { b.runner = newCommand("gopherjs") diff --git a/cmd/fyne/internal/commands/build_test.go b/cmd/fyne/internal/commands/build_test.go index 42043f35a5..55646cf4e0 100644 --- a/cmd/fyne/internal/commands/build_test.go +++ b/cmd/fyne/internal/commands/build_test.go @@ -201,7 +201,7 @@ func Test_BuildGopherJSReleaseVersion(t *testing.T) { } gopherJSBuildTest := &testCommandRuns{runs: expected, t: t} - b := &Builder{appData: &appData{}, os: "gopherjs", srcdir: "myTest", release: true, runner: gopherJSBuildTest} + b := &Builder{appData: &appData{}, os: "js", srcdir: "myTest", release: true, runner: gopherJSBuildTest} err := b.build() if runtime.GOOS == "windows" { assert.NotNil(t, err) diff --git a/cmd/fyne/internal/commands/bundle.go b/cmd/fyne/internal/commands/bundle.go index e06399aa66..0c2feddabe 100644 --- a/cmd/fyne/internal/commands/bundle.go +++ b/cmd/fyne/internal/commands/bundle.go @@ -69,6 +69,11 @@ type Bundler struct { noheader bool } +// NewBundler returns a command that can handle the bundling assets into a GUI app binary. +func NewBundler() *Bundler { + return &Bundler{} +} + // AddFlags adds all the command line flags for passing to the Bundler. // // Deprecated: Access to the individual cli commands are being removed. diff --git a/cmd/fyne/internal/commands/get.go b/cmd/fyne/internal/commands/get.go index daaf52f40e..e7266adda8 100644 --- a/cmd/fyne/internal/commands/get.go +++ b/cmd/fyne/internal/commands/get.go @@ -50,7 +50,7 @@ type Getter struct { } // NewGetter returns a command that can handle the download and install of GUI apps built using Fyne. -// It depends on a Go and C compiler installed at this stage and takes a single, package, parameter to identify the app. +// It depends on a Go and C compiler installed at this stage. func NewGetter() *Getter { return &Getter{appData: &appData{}} } diff --git a/cmd/fyne/internal/commands/install.go b/cmd/fyne/internal/commands/install.go index 4dbe04d25c..d89e0c18a8 100644 --- a/cmd/fyne/internal/commands/install.go +++ b/cmd/fyne/internal/commands/install.go @@ -17,7 +17,7 @@ import ( // Install returns the cli command for installing fyne applications func Install() *cli.Command { - i := &Installer{appData: &appData{}} + i := NewInstaller() return &cli.Command{ Name: "install", @@ -73,6 +73,11 @@ type Installer struct { release bool } +// NewInstaller returns a command that can install a GUI apps built using Fyne from local source code. +func NewInstaller() *Installer { + return &Installer{appData: &appData{}} +} + // AddFlags adds the flags for interacting with the Installer. // // Deprecated: Access to the individual cli commands are being removed. diff --git a/cmd/fyne/internal/commands/package-web.go b/cmd/fyne/internal/commands/package-web.go index 4735da6dde..55ed8312ad 100644 --- a/cmd/fyne/internal/commands/package-web.go +++ b/cmd/fyne/internal/commands/package-web.go @@ -39,7 +39,7 @@ func (p *Packager) packageWasm() error { } func (p *Packager) packageGopherJS() error { - appDir := util.EnsureSubDir(p.dir, "gopherjs") + appDir := util.EnsureSubDir(p.dir, "js") tpl := webData{ AppName: p.Name, diff --git a/cmd/fyne/internal/commands/package.go b/cmd/fyne/internal/commands/package.go index cac8d91ecc..7bc5e209bd 100644 --- a/cmd/fyne/internal/commands/package.go +++ b/cmd/fyne/internal/commands/package.go @@ -31,7 +31,7 @@ const ( // Package returns the cli command for packaging fyne applications func Package() *cli.Command { - p := &Packager{appData: &appData{}} + p := NewPackager() return &cli.Command{ Name: "package", @@ -41,7 +41,7 @@ func Package() *cli.Command { &cli.StringFlag{ Name: "target", Aliases: []string{"os"}, - Usage: "The mobile platform to target (android, android/arm, android/arm64, android/amd64, android/386, ios, iossimulator, wasm, gopherjs, web).", + Usage: "The mobile platform to target (android, android/arm, android/arm64, android/amd64, android/386, ios, iossimulator, wasm, js, web).", Destination: &p.os, }, &cli.StringFlag{ @@ -140,6 +140,11 @@ type Packager struct { linuxAndBSDMetadata *metadata.LinuxAndBSD } +// NewPackager returns a command that can handle the packaging a GUI apps built using Fyne from local source code. +func NewPackager() *Packager { + return &Packager{appData: &appData{}} +} + // AddFlags adds the flags for interacting with the package command. // // Deprecated: Access to the individual cli commands are being removed. @@ -243,7 +248,7 @@ func (p *Packager) buildPackage(runner runner, tags []string) ([]string, error) } bGopherJS := &Builder{ - os: "gopherjs", + os: "js", srcdir: p.srcDir, target: p.exe + ".js", release: p.release, @@ -322,7 +327,7 @@ func (p *Packager) doPackage(runner runner) error { return p.packageIOS(p.os, tags) case "wasm": return p.packageWasm() - case "gopherjs": + case "js": return p.packageGopherJS() case "web": return p.packageWeb() diff --git a/cmd/fyne/internal/commands/package_test.go b/cmd/fyne/internal/commands/package_test.go index f9cd6464d6..a34348dc42 100644 --- a/cmd/fyne/internal/commands/package_test.go +++ b/cmd/fyne/internal/commands/package_test.go @@ -343,7 +343,7 @@ func Test_buildPackageGopherJS(t *testing.T) { p := &Packager{ appData: &appData{}, - os: "gopherjs", + os: "js", srcDir: "myTest", exe: "myTest.js", release: true, @@ -394,7 +394,7 @@ func Test_PackageGopherJS(t *testing.T) { Name: "myTest", icon: "myTest.png", }, - os: "gopherjs", + os: "js", srcDir: "myTest", dir: "myTestTarget", exe: "myTest.js", @@ -409,7 +409,7 @@ func Test_PackageGopherJS(t *testing.T) { expectedEnsureSubDirRuns := mockEnsureSubDirRuns{ expected: []mockEnsureSubDir{ - {"myTestTarget", "gopherjs", "myTestTarget/gopherjs"}, + {"myTestTarget", "js", "myTestTarget/js"}, }, } utilEnsureSubDirMock = func(parent, name string) string { @@ -428,12 +428,12 @@ func Test_PackageGopherJS(t *testing.T) { expectedWriteFileRuns := mockWriteFileRuns{ expected: []mockWriteFile{ - {filepath.Join("myTestTarget", "gopherjs", "index.html"), nil}, - {filepath.Join("myTestTarget", "gopherjs", "spinner_light.gif"), nil}, - {filepath.Join("myTestTarget", "gopherjs", "spinner_dark.gif"), nil}, - {filepath.Join("myTestTarget", "gopherjs", "light.css"), nil}, - {filepath.Join("myTestTarget", "gopherjs", "dark.css"), nil}, - {filepath.Join("myTestTarget", "gopherjs", "webgl-debug.js"), nil}, + {filepath.Join("myTestTarget", "js", "index.html"), nil}, + {filepath.Join("myTestTarget", "js", "spinner_light.gif"), nil}, + {filepath.Join("myTestTarget", "js", "spinner_dark.gif"), nil}, + {filepath.Join("myTestTarget", "js", "light.css"), nil}, + {filepath.Join("myTestTarget", "js", "dark.css"), nil}, + {filepath.Join("myTestTarget", "js", "webgl-debug.js"), nil}, }, } utilWriteFileMock = func(target string, _ []byte) error { @@ -442,8 +442,8 @@ func Test_PackageGopherJS(t *testing.T) { expectedCopyFileRuns := mockCopyFileRuns{ expected: []mockCopyFile{ - {source: "myTest.png", target: filepath.Join("myTestTarget", "gopherjs", "icon.png")}, - {source: "myTest.js", target: filepath.Join("myTestTarget", "gopherjs", "myTest.js")}, + {source: "myTest.png", target: filepath.Join("myTestTarget", "js", "icon.png")}, + {source: "myTest.js", target: filepath.Join("myTestTarget", "js", "myTest.js")}, }, } utilCopyFileMock = func(source, target string) error { diff --git a/cmd/fyne/internal/commands/release.go b/cmd/fyne/internal/commands/release.go index 58c35f9add..43fe05cf67 100644 --- a/cmd/fyne/internal/commands/release.go +++ b/cmd/fyne/internal/commands/release.go @@ -28,8 +28,7 @@ var macAppStoreCategories = []string{ // Release returns the cli command for bundling release builds of fyne applications func Release() *cli.Command { - r := &Releaser{} - r.appData = &appData{} + r := NewReleaser() return &cli.Command{ Name: "release", @@ -149,6 +148,13 @@ type Releaser struct { password string } +// NewReleaser returns a command that can handle the packaging a GUI apps for release from local Fyne source code. +func NewReleaser() *Releaser { + r := &Releaser{} + r.appData = &appData{} + return r +} + // AddFlags adds the flags for interacting with the release command. // // Deprecated: Access to the individual cli commands are being removed. diff --git a/cmd/fyne/internal/mobile/dex.go b/cmd/fyne/internal/mobile/dex.go index c6921ed5e0..22f52a756f 100644 --- a/cmd/fyne/internal/mobile/dex.go +++ b/cmd/fyne/internal/mobile/dex.go @@ -6,45 +6,45 @@ package mobile -var dexStr = `ZGV4CjAzNQDTYRTF/qrASEUxTObFaqwTSgBKmxVbTMLALQAAcAAAAHhWNBIAAAAAAAAAAP` + - `AsAADbAAAAcAAAADUAAADcAwAARQAAALAEAAAXAAAA7AcAAHgAAACkCAAACAAAAGQMAABc` + - `IAAAZA0AAKQaAACmGgAAqRoAAK4aAACzGgAAthoAAL4aAADSGgAA6RoAAPkaAAAJGwAADx` + - `sAACYbAAApGwAALhsAADQbAAA5GwAAPxsAAEIbAABGGwAASxsAAE8bAABUGwAAWRsAAHcb` + - `AACYGwAAsxsAAM0bAADwGwAAFRwAADocAABbHAAAdBwAAIccAACjHAAAuBwAAM4cAADnHA` + - `AAAx0AABcdAABMHQAAbB0AAIUdAACxHQAAxh0AAO0dAAAEHgAAIR4AAFAeAABrHgAAlh4A` + - `AMgeAADjHgAACB8AACgfAABHHwAAVx8AAHEfAACIHwAApx8AALsfAADRHwAA5R8AAPkfAA` + - `AQIAAANyAAAFwgAACBIAAApiAAAM0gAADyIAAAFyEAADohAABQIQAAWyEAAHMhAAB8IQAA` + - `liEAAKEhAACkIQAAqCEAAK0hAAC0IQAAuiEAAL4hAADDIQAAyiEAANYhAADbIQAA3yEAAO` + - `IhAADmIQAA6yEAAPEhAAD2IQAACyIAAA8iAAAbIgAAJyIAADMiAAA/IgAASyIAAFciAABj` + - `IgAAbyIAAHwiAACJIgAAmSIAAKMiAAC+IgAA1iIAAOgiAAD+IgAAJSMAAEojAAB0IwAAli` + - `MAALcjAADTIwAA7CMAAPkjAAAMJAAAGiQAACQkAAAzJAAAQyQAAFMkAABjJAAAcyQAAHYk` + - `AAB+JAAAoSQAALUkAADDJAAA0yQAANgkAADpJAAA+iQAAAclAAAVJQAAJyUAADAlAAA+JQ` + - `AASSUAAFQlAABnJQAAdSUAAIIlAACXJQAAoCUAAKslAAC9JQAA2SUAAPMlAAAOJgAAJyYA` + - `ADAmAAA7JgAARSYAAFAmAABgJgAAfiYAAJAmAACYJgAApiYAAL8mAADKJgAA2CYAAOcmAA` + - `D3JgAABicAAAwnAAAUJwAAGicAACcnAAA7JwAAZCcAAG8nAAB5JwAAfycAAJEnAACgJwAA` + - `uCcAAMInAADSJwAA4icAAPEnAAD7JwAACSgAAA4oAAAdKAAAKigAADkoAABHKAAAWCgAAH` + - `MoAACBKAAAiigAAJMoAACiKAAArigAALwoAADKKAAA2CgAAOcoAADuKAAABikAABMpAAAb` + - `KQAAIykAAC0pAAAyKQAAOikAAF4pAABsKQAAeSkAAIspAACSKQAAmSkAAAwAAAAXAAAAGA` + +var dexStr = `ZGV4CjAzNQDjZX63XYczplWvLvfMLlPbq7VDP34Wu73ELQAAcAAAAHhWNBIAAAAAAAAAAP` + + `QsAADbAAAAcAAAADUAAADcAwAARQAAALAEAAAXAAAA7AcAAHgAAACkCAAACAAAAGQMAABg` + + `IAAAZA0AAKgaAACqGgAArRoAALIaAAC3GgAAuhoAAMIaAADWGgAA7RoAAP0aAAANGwAAEx` + + `sAACobAAAtGwAAMhsAADgbAAA9GwAAQxsAAEYbAABKGwAATxsAAFMbAABYGwAAXRsAAHsb` + + `AACcGwAAtxsAANEbAAD0GwAAGRwAAD4cAABfHAAAeBwAAIscAACnHAAAvBwAANIcAADrHA` + + `AABx0AABsdAABQHQAAcB0AAIkdAAC1HQAAyh0AAPEdAAAIHgAAJR4AAFQeAABvHgAAmh4A` + + `AMweAADnHgAADB8AACwfAABLHwAAWx8AAHUfAACMHwAAqx8AAL8fAADVHwAA6R8AAP0fAA` + + `AUIAAAOyAAAGAgAACFIAAAqiAAANEgAAD2IAAAGyEAAD4hAABUIQAAXyEAAHchAACAIQAA` + + `miEAAKUhAACoIQAArCEAALEhAAC4IQAAviEAAMIhAADHIQAAziEAANohAADfIQAA4yEAAO` + + `YhAADqIQAA7yEAAPUhAAD6IQAADyIAABMiAAAfIgAAKyIAADciAABDIgAATyIAAFsiAABn` + + `IgAAcyIAAIAiAACNIgAAnSIAAKciAADCIgAA2iIAAOwiAAACIwAAKSMAAE4jAAB4IwAAmi` + + `MAALsjAADXIwAA8CMAAP0jAAAQJAAAHiQAACgkAAA3JAAARyQAAFckAABnJAAAdyQAAHok` + + `AACCJAAApSQAALkkAADHJAAA1yQAANwkAADtJAAA/iQAAAslAAAZJQAAKyUAADQlAABCJQ` + + `AATSUAAFglAABrJQAAeSUAAIYlAACbJQAApCUAAK8lAADBJQAA3SUAAPclAAASJgAAKyYA` + + `ADQmAAA/JgAASSYAAFQmAABkJgAAgiYAAJQmAACcJgAAqiYAAMMmAADOJgAA3CYAAOsmAA` + + `D7JgAACicAABAnAAAYJwAAHicAACsnAAA/JwAAaCcAAHMnAAB9JwAAgycAAJUnAACkJwAA` + + `vCcAAMYnAADWJwAA5icAAPUnAAD/JwAADSgAABIoAAAhKAAALigAAD0oAABLKAAAXCgAAH` + + `coAACFKAAAjigAAJcoAACmKAAAsigAAMAoAADOKAAA3CgAAOsoAADyKAAACikAABcpAAAf` + + `KQAAJykAADEpAAA2KQAAPikAAGIpAABwKQAAfSkAAI8pAACWKQAAnSkAAAwAAAAXAAAAGA` + `AAABkAAAAaAAAAGwAAABwAAAAdAAAAHgAAAB8AAAAgAAAAIQAAACIAAAAjAAAAJAAAACUA` + `AAAmAAAAJwAAACgAAAApAAAAKgAAACsAAAAsAAAALQAAAC4AAAAvAAAAMAAAADEAAAAyAA` + `AAMwAAADQAAAA1AAAANgAAADcAAAA4AAAAOQAAADoAAAA7AAAAPAAAAD0AAAA+AAAAPwAA` + `AEAAAABBAAAAQgAAAEMAAABEAAAARQAAAEYAAABHAAAATgAAAFkAAABeAAAADAAAAAAAAA` + - `AAAAAADQAAAAAAAABsGQAADgAAAAAAAAB0GQAADwAAAAAAAACAGQAAEAAAAAAAAACIGQAA` + - `EQAAAAIAAAAAAAAAEQAAAAQAAAAAAAAAEgAAAAQAAACUGQAAFgAAAAQAAACcGQAAFAAAAA` + - `QAAACkGQAAFgAAAAQAAACAGQAAFgAAAAQAAACsGQAAFQAAAAUAAAC0GQAAEQAAAAYAAAAA` + + `AAAAAADQAAAAAAAABwGQAADgAAAAAAAAB4GQAADwAAAAAAAACEGQAAEAAAAAAAAACMGQAA` + + `EQAAAAIAAAAAAAAAEQAAAAQAAAAAAAAAEgAAAAQAAACYGQAAFgAAAAQAAACgGQAAFAAAAA` + + `QAAACoGQAAFgAAAAQAAACEGQAAFgAAAAQAAACwGQAAFQAAAAUAAAC4GQAAEQAAAAYAAAAA` + `AAAAEQAAAAcAAAAAAAAAEQAAAAgAAAAAAAAAEQAAAAoAAAAAAAAAEQAAAA0AAAAAAAAAEQ` + - `AAAA4AAAAAAAAAEgAAABIAAACUGQAAEQAAABUAAAAAAAAAEgAAABUAAACUGQAAEQAAABcA` + - `AAAAAAAAEQAAABgAAAAAAAAAFAAAABoAAAC8GQAAFgAAABoAAADEGQAAEQAAACEAAAAAAA` + - `AAEwAAACIAAABsGQAAFAAAACUAAACkGQAAEQAAACcAAAAAAAAAFAAAACcAAACkGQAAEQAA` + - `ADEAAAAAAAAATgAAADIAAAAAAAAATwAAADIAAACUGQAAUAAAADIAAABsGQAAUQAAADIAAA` + - `DMGQAAUgAAADIAAADYGQAAUwAAADIAAADkGQAAVAAAADIAAADsGQAAUwAAADIAAAD0GQAA` + - `UwAAADIAAAD8GQAAUwAAADIAAAAEGgAAUwAAADIAAAAMGgAAUwAAADIAAABkGQAAUwAAAD` + - `IAAABcGQAAVgAAADIAAAAUGgAAVwAAADIAAAAsGgAAUwAAADIAAAA0GgAAUwAAADIAAABM` + - `GQAAUwAAADIAAAA8GgAAVQAAADIAAABEGgAAUwAAADIAAABUGQAAUwAAADIAAACkGQAAVw` + - `AAADIAAACAGQAAUwAAADIAAABQGgAAUwAAADIAAABYGgAAUwAAADIAAAC8GQAAVAAAADIA` + - `AABgGgAAVwAAADIAAABoGgAAWAAAADIAAABwGgAAWQAAADMAAAAAAAAAWwAAADMAAAB4Gg` + - `AAWwAAADMAAACAGgAAXAAAADMAAACIGgAAWgAAADMAAAA8GgAAWgAAADMAAACUGgAAWgAA` + - `ADMAAAC8GQAAXQAAADMAAACcGgAAFAAAADQAAACkGQAABQAMALEAAAAHAAAA0wAAAAkAAA` + + `AAAA4AAAAAAAAAEgAAABIAAACYGQAAEQAAABUAAAAAAAAAEgAAABUAAACYGQAAEQAAABcA` + + `AAAAAAAAEQAAABgAAAAAAAAAFAAAABoAAADAGQAAFgAAABoAAADIGQAAEQAAACEAAAAAAA` + + `AAEwAAACIAAABwGQAAFAAAACUAAACoGQAAEQAAACcAAAAAAAAAFAAAACcAAACoGQAAEQAA` + + `ADEAAAAAAAAATgAAADIAAAAAAAAATwAAADIAAACYGQAAUAAAADIAAABwGQAAUQAAADIAAA` + + `DQGQAAUgAAADIAAADcGQAAUwAAADIAAADoGQAAVAAAADIAAADwGQAAUwAAADIAAAD4GQAA` + + `UwAAADIAAAAAGgAAUwAAADIAAAAIGgAAUwAAADIAAAAQGgAAUwAAADIAAABoGQAAUwAAAD` + + `IAAABgGQAAVgAAADIAAAAYGgAAVwAAADIAAAAwGgAAUwAAADIAAAA4GgAAUwAAADIAAABQ` + + `GQAAUwAAADIAAABAGgAAVQAAADIAAABIGgAAUwAAADIAAABYGQAAUwAAADIAAACoGQAAVw` + + `AAADIAAACEGQAAUwAAADIAAABUGgAAUwAAADIAAABcGgAAUwAAADIAAADAGQAAVAAAADIA` + + `AABkGgAAVwAAADIAAABsGgAAWAAAADIAAAB0GgAAWQAAADMAAAAAAAAAWwAAADMAAAB8Gg` + + `AAWwAAADMAAACEGgAAXAAAADMAAACMGgAAWgAAADMAAABAGgAAWgAAADMAAACYGgAAWgAA` + + `ADMAAADAGQAAXQAAADMAAACgGgAAFAAAADQAAACoGQAABQAMALEAAAAHAAAA0wAAAAkAAA` + `CqAAAACQAAANIAAAALAAAASwAAACoAKwDQAAAAKwAxAM8AAAArAAAA1wAAACwAMQDPAAAA` + `LQAxAM8AAAAuAC8A0AAAAC8AMQDPAAAAMAAxAM8AAAAxAAAABgAAADEAAAAHAAAAMQAAAA` + `gAAAAxAAAACQAAADEAAABIAAAAMQAAAEoAAAAxAAAATAAAADEAMQChAAAAMQAzAKUAAAAx` + @@ -66,168 +66,168 @@ var dexStr = `ZGV4CjAzNQDTYRTF/qrASEUxTObFaqwTSgBKmxVbTMLALQAAcAAAAHhWNBIAAAAAAA `EAAAAxAAIAlAAAADEAHACWAAAAMQAdAJwAAAAxABYAngAAADEAIACjAAAAMQAjAKcAAAAx` + `ACAAqAAAADEANACpAAAAMQAgAKwAAAAxACQAswAAADEAIAC0AAAAMQAnALUAAAAxACkAtg` + `AAADEAMwC9AAAAMQA7AL4AAAAxACAAxwAAADEANADIAAAAMQA1AMkAAAAxACEAygAAADEA` + - `JgDNAAAAMQAgANUAAAAxACcA1gAAACoAAAAAAAAAJQAAAEwZAAALAAAA1BgAAMIrAAAAAA` + - `AAKwAAAAAAAAAlAAAAVBkAAAsAAADkGAAA0ysAAAAAAAAsAAAAAAAAACUAAABUGQAACwAA` + - `APwYAADnKwAAAAAAAC0AAAAAAAAAJQAAAFwZAAALAAAADBkAAPgrAAAAAAAALgAAAAAAAA` + - `AlAAAAZBkAAAsAAAAcGQAACSwAAAAAAAAvAAAAAAAAACUAAABUGQAACwAAACwZAAAiLAAA` + - `AAAAADAAAAAAAAAAJQAAAFQZAAALAAAAPBkAADMsAAAAAAAAMQAAAAEAAAABAAAAAAAAAA` + - `sAAAAAAAAARCwAALErAAACAAAAbisAAHUrAAACAAAAfisAAHUrAAABAAAAhSsAAAIAAACO` + - `KwAAdSsAAAIAAACVKwAAdSsAAAIAAACcKwAAdSsAAAIAAACjKwAAdSsAAAIAAACqKwAAdS` + - `sAAAIAAgABAAAAnCkAAAYAAABbAQUAcBA3AAAADgAGAAQAAgAAAKIpAAAOAAAAEmAzBAsA` + - `VCAFAFQABgAaAQEAcSBPABAAEgAPAAMAAwABAAAArCkAAAgAAABbAQYAWQIHAHAQNwAAAA` + - `4ABgABAAMAAACzKQAAqAAAABUCAEASYRIEFQAIAFJTBwArA5QAAAABIRoCCgAaA9QAcSAV` + - `ADIAVFIGAHEQTQACAAwCbiAsABIAVFEGAHEQTQABAAwBbiAtAAEAVFAGAHEQTQAAAAwAIg` + - `EqAHAgPABRAG4gLwAQAFRQBgASEXEgUQAQAFRQBgBxEE0AAAAMABoBBABuIDEAEABUUAYA` + - `cRBNAAAADABUUQYAcRBNAAEADAFuECoAAQAMAXIQFAABAAoBbiAwABAAVFAGAHEgUQBAAF` + - `RQBgBxEE0AAAAMAG4gMgBAAFRQBgBxEE0AAAAMAG4QKQAAAFRQBgBxEE0AAAAMAG4QKwAA` + - `AFRQBgAaAaYAbiBjABAADAAfABkAVFEGAHEQTQABAAwBbjAmABAEDgABISiDFAACAAgAKQ` + - `B//xQAkAAIACkAc/8AAAABBAAAAAAAhwAAAAsAAACJAAAAjgAAAAIAAgABAAAA1ykAAAYA` + - `AABbAQgAcBA3AAAADgADAAEAAgAAAN4pAAAMAAAAVCAIAHEQTQAAAAwAEwEIAG4gMgAQAA` + - `4AAgACAAEAAADlKQAABgAAAFsBCQBwEDcAAAAOAAsACgABAAAA7CkAAAYAAABUEAkAbhB2` + - `AAAADgACAAIAAQAAAPwpAAAGAAAAWwEKAHAQNwAAAA4ABAACAAIAAAADKgAAPwAAABIRch` + - `AUAAMACgA1EDkAVCAKAFQACwBxIFEAEABUIAoAVAALAHEQTQAAAAwAGgEEAG4gMQAQAFQg` + - `CgBUAAsAcRBNAAAADABUIQoAVBELAHEQTQABAAwBbhAqAAEADAFyEBQAAQAKAW4gMAAQAF` + - `QgCgBUAAsAEgFxIFEAEAAOAAAABwAFAAEAAAARKgAAGgAAAFQgCgBUAAsAcRBQAAAACgA4` + - `AAMADgA9Bf//EgA1UPz/VCEKAFQRCwBxEFMAAQDYAAABKPUHAAUAAwAAACIqAAAfAAAAVC` + - `AKAFQACwBxEFAAAAAKADgAAwAOAD0G//9UIAoAVAALAJABBAZyMDUAQwEMAXIQNgABAAwB` + - `cSBPABAAKOwAAAIAAgABAAAALyoAAAYAAABbAQsAcBA3AAAADgAFAAEAAwAAADYqAABvAA` + - `AAEuNUQAsAIgEaAHEAUgAAAAwCcCAnACEAcSBOABAAVEALAHEQTQAAAAwAEwEIAG4gMgAQ` + - `AFRACwBxEE0AAAAMABUBCABuIC0AEAAiABsAcDAzADADVEELAHEQTQABAAwBbiAuAAEAVE` + - `ELAFRCCwBxEE0AAgAMAm4wVQAhAFRACwBxEE0AAAAMABoBBABuIDEAEABUQAsAcRBNAAAA` + - `DABUQQsAcRBNAAEADAFuECoAAQAMAXIQFAABAAoBbiAwABAAVEALAHEQTQAAAAwAIgEuAH` + - `AgRABBAG4gKAAQAA4AAAACAAIAAQAAAEkqAAAGAAAAWwEMAHAQNwAAAA4AAgABAAEAAABQ` + - `KgAABgAAAFQQDABxEFQAAAAOAAIAAQABAAAAVyoAAAkAAABwEAAAAQASAFwQFQBpARQADg` + - `AAAAIAAQAAAAAAXyoAAAMAAABUEBYAEQAAAAIAAgAAAAAAZSoAAAMAAABbARYAEQEAAAIA` + - `AgACAAAAbCoAAAQAAABwIGkAEAAOAAIAAQAAAAAAcyoAAAMAAABVEBUADwAAAAIAAgAAAA` + - `AAeSoAAAMAAABcARUADwEAAAEAAAAAAAAAgCoAAAMAAABiABQAEQAAAAEAAQABAAAAhSoA` + - `AAQAAABwEGgAAAAOAAEAAQABAAAAiyoAAAQAAABvEAEAAAAOAAcAAwADAAEAkSoAABkAAA` + - `AS8HEQGAAEAAwBbjAXAFEGCgE5AQMADwABECj+DQEaAgoAGgOCAHEwFgAyASj1DQEo8wAA` + - `AQAAAAcAAQABAhEXIw4AAAEAAAABAAAAoioAAAYAAABiABQAbhBXAAAADgAEAAEAAwABAK` + - `kqAAAzAAAAbhBgAAMADABuEF8AAwAMAW4QCAABAAwBEwKAAG4wDQAQAgwAVAEAADkBCgAa` + - `AAoAGgGvAHEgFQAQAA4AVAAAABoBbwBuIBMAEAAMAHEQOwAAACj0DQAaAQoAGgKuAHEwFg` + - `AhACjrAAAAAAAAKQABAAEBIyoCAAEAAgAAALoqAAAJAAAAIgAvAHAgSAAQAG4gbwABAA4A` + - `AAACAAEAAgAAAMMqAAAGAAAAYgAUAG4gWAAQAA4AAwACAAMAAADLKgAABgAAAGIAFABuMF` + - `kAEAIOAAIAAQACAAAA1CoAAAYAAABiABQAbiBaABAADgAEAAEAAwAAANsqAAAkAAAAGgCm` + - `AG4gYwADAAwAHwAZABQBAgACAW4gXAATAAwBbhAbAAEADAFuEB4AAQAMARICbjAlABACIg` + - `AsAHAgQAAwAG4gbwADAA4ABgACAAMAAADlKgAAVwAAABITIgAEABoBcQBwIAQAEAAaAXYA` + - `biA5AFEACgE4ARwAYAEEABMCFQA0IRYAIgAEABoBcgBwIAQAEABuIAYAMAAaAUkAcSAHAB` + - `AADABuMHUABAMOABoB2gBuIDgAFQAKATgBHgBgAQQAEwITADQhGAAaAQMAbiAMABAAGgF0` + - `ABoCXwBuIDoAJQAMAm4wCwAQAhoBcwBuIAUAEAAo024gDABQABoBcwBuIAUAEAAoygAABg` + - `ADAAMAAAD5KgAAPgAAACIABAAaAXAAcCAEABAAGgHaAG4gOAAUAAoBOAEtAGABBAATAhMA` + - `NCEnABoBAwBuIAwAEAAaAXQAGgJfAG4gOgAkAAwCbjALABACGgF1AG4wCgAQBRoBcwBuIA` + - `UAEAAaAU0AcSAHABAADAASIW4wdQADAQ4AbiAMAEAAKOgDAAIAAwAAAAsrAAAJAAAAIgAr` + - `AHAwPgAQAm4gbwABAA4AAAACAAEAAgAAABQrAAAJAAAAIgAwAHAgSgAQAG4gbwABAA4AAA` + - `ACAAEAAQAAABsrAAAJAAAAbhBeAAEADABuEDQAAAAMABEAAAAFAAQAAgAAACArAAAcAAAA` + - `EhAyAgYAEiAyAgMADgAS8DIDCAAaAAAAcCBbAAEAKPduEAkABAAMAG4QEgAAAAwAcCBbAA` + - `EAKOsBAAEAAQAAADIrAAAEAAAAcBBWAAAADgACAAIAAgAAADkrAAAHAAAAbyACABAAbiB3` + - `ABAADgAAAAQAAgACAAAAQisAACgAAABwEGoAAgBvIAMAMgBwEHEAAgBuEGEAAgAMAG4QDg` + - `AAAAwAbiB3AAIAFAACAAIBbiBcAAIADABuEBsAAAAMACIBLQBwIEIAIQBuIBkAEAAOAAcA` + - `AQAFAAEATysAAGAAAABuEGUABgAMAG4QIAAAAAwAbhAcAAAADAA5AAMADgBuECQAAAAKAW` + - `4QIQAAAAoCbhAiAAAACgNuECMAAAAKAHBQZwAWMijsDQAiAAkAcBAPAAAAbhBlAAYADAFu` + - `ECAAAQAMAW4gHwABABQBAgACAW4gXAAWAAwBbhAbAAEADAFSAgMAbhAaAAEACgNuEBAAAA` + - `AKBLFDUgQDALFDUgQCAG4QHQABAAoBbhARAAAACgWxUVIAAgCRAAEAcFBnACZDKK8AAAAA` + - `IgABAAEBJCMEAAIAAgAAAGQrAAAPAAAAUjABAN0AADATASAAMxAHABIQcCBwAAIADgASAC` + - `j7AABkDQAAAAAAAAAAAAAAAAAAcA0AAAAAAAABAAAAAAAAAD4AAAB8DQAAhA0AAAAAAAAA` + - `AAAAAAAAAJANAAAAAAAAAAAAAAAAAACcDQAAAAAAAAAAAAAAAAAAqA0AAAAAAAAAAAAAAA` + - `AAALQNAAAAAAAAAAAAAAAAAAABAAAAHAAAAAEAAAAmAAAAAQAAABQAAAABAAAADwAAAAIA` + - `AAAAAAAAAwAAAAAAAAAAAAAAAgAAACcAJwADAAAAJwAnACkAAAABAAAAAAAAAAIAAAAEAC` + - `IAAQAAACcAAAACAAAAJwA0AAIAAAACAAAAAQAAADEAAAACAAAAMQAaAAQAAAAAAAAAAAAA` + - `AAMAAAAAAAAABAAAAAEAAAADAAAAAgAAAAQAAAABAAAABwAAAAEAAAAJAAAAAQAAAAwAAA` + - `ABAAAADgAAAAkAAAAVAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAVABYAAQAAABYAAAABAAAA` + - `IgAAAAQAAAAiAAAAAAAAAAEAAAArAAAAAQAAAC8AAAACAAAAMQAAAAIAAAAxACcAAQAAAD` + - `MAAAACAAAADQAAAAIAAAAVAAAAAwAAAB0AAAATAAAAAQAAACUAAAACAAAAMQAzAAAAAQoA` + - `AygpVgADKi8qAAEwAAY8aW5pdD4AEkRFRkFVTFRfSU5QVVRfVFlQRQAVREVGQVVMVF9LRV` + - `lCT0FSRF9DT0RFAA5GSUxFX09QRU5fQ09ERQAORklMRV9TQVZFX0NPREUABEZ5bmUAFUdv` + - `TmF0aXZlQWN0aXZpdHkuamF2YQABSQADSUlJAARJSUlJAANJTEwABElMTEwAAUwAAkxJAA` + - `NMSUkAAkxMAANMTEkAA0xMTAAcTGFuZHJvaWQvYXBwL05hdGl2ZUFjdGl2aXR5OwAfTGFu` + - `ZHJvaWQvY29udGVudC9Db21wb25lbnROYW1lOwAZTGFuZHJvaWQvY29udGVudC9Db250ZX` + - `h0OwAYTGFuZHJvaWQvY29udGVudC9JbnRlbnQ7ACFMYW5kcm9pZC9jb250ZW50L3BtL0Fj` + - `dGl2aXR5SW5mbzsAI0xhbmRyb2lkL2NvbnRlbnQvcG0vUGFja2FnZU1hbmFnZXI7ACNMYW` + - `5kcm9pZC9jb250ZW50L3Jlcy9Db25maWd1cmF0aW9uOwAfTGFuZHJvaWQvY29udGVudC9y` + - `ZXMvUmVzb3VyY2VzOwAXTGFuZHJvaWQvZ3JhcGhpY3MvUmVjdDsAEUxhbmRyb2lkL25ldC` + - `9Vcmk7ABpMYW5kcm9pZC9vcy9CdWlsZCRWRVJTSU9OOwATTGFuZHJvaWQvb3MvQnVuZGxl` + - `OwAUTGFuZHJvaWQvb3MvSUJpbmRlcjsAF0xhbmRyb2lkL3RleHQvRWRpdGFibGU7ABpMYW` + - `5kcm9pZC90ZXh0L1RleHRXYXRjaGVyOwASTGFuZHJvaWQvdXRpbC9Mb2c7ADNMYW5kcm9p` + - `ZC92aWV3L0tleUNoYXJhY3Rlck1hcCRVbmF2YWlsYWJsZUV4Y2VwdGlvbjsAHkxhbmRyb2` + - `lkL3ZpZXcvS2V5Q2hhcmFjdGVyTWFwOwAXTGFuZHJvaWQvdmlldy9LZXlFdmVudDsAKkxh` + - `bmRyb2lkL3ZpZXcvVmlldyRPbkxheW91dENoYW5nZUxpc3RlbmVyOwATTGFuZHJvaWQvdm` + - `lldy9WaWV3OwAlTGFuZHJvaWQvdmlldy9WaWV3R3JvdXAkTGF5b3V0UGFyYW1zOwAVTGFu` + - `ZHJvaWQvdmlldy9XaW5kb3c7ABtMYW5kcm9pZC92aWV3L1dpbmRvd0luc2V0czsALUxhbm` + - `Ryb2lkL3ZpZXcvaW5wdXRtZXRob2QvSW5wdXRNZXRob2RNYW5hZ2VyOwAZTGFuZHJvaWQv` + - `d2lkZ2V0L0VkaXRUZXh0OwApTGFuZHJvaWQvd2lkZ2V0L0ZyYW1lTGF5b3V0JExheW91dF` + - `BhcmFtczsAMExhbmRyb2lkL3dpZGdldC9UZXh0VmlldyRPbkVkaXRvckFjdGlvbkxpc3Rl` + - `bmVyOwAZTGFuZHJvaWQvd2lkZ2V0L1RleHRWaWV3OwAjTGRhbHZpay9hbm5vdGF0aW9uL0` + - `VuY2xvc2luZ01ldGhvZDsAHkxkYWx2aWsvYW5ub3RhdGlvbi9Jbm5lckNsYXNzOwAdTGRh` + - `bHZpay9hbm5vdGF0aW9uL1NpZ25hdHVyZTsADkxqYXZhL2lvL0ZpbGU7ABhMamF2YS9sYW` + - `5nL0NoYXJTZXF1ZW5jZTsAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwAdTGphdmEvbGFuZy9O` + - `b1N1Y2hNZXRob2RFcnJvcjsAEkxqYXZhL2xhbmcvT2JqZWN0OwAUTGphdmEvbGFuZy9SdW` + - `5uYWJsZTsAEkxqYXZhL2xhbmcvU3RyaW5nOwASTGphdmEvbGFuZy9TeXN0ZW07ABVMamF2` + - `YS9sYW5nL1Rocm93YWJsZTsAJUxvcmcvZ29sYW5nL2FwcC9Hb05hdGl2ZUFjdGl2aXR5JD` + - `EkMTsAI0xvcmcvZ29sYW5nL2FwcC9Hb05hdGl2ZUFjdGl2aXR5JDE7ACNMb3JnL2dvbGFu` + - `Zy9hcHAvR29OYXRpdmVBY3Rpdml0eSQyOwAjTG9yZy9nb2xhbmcvYXBwL0dvTmF0aXZlQW` + - `N0aXZpdHkkMzsAJUxvcmcvZ29sYW5nL2FwcC9Hb05hdGl2ZUFjdGl2aXR5JDQkMTsAI0xv` + - `cmcvZ29sYW5nL2FwcC9Hb05hdGl2ZUFjdGl2aXR5JDQ7ACNMb3JnL2dvbGFuZy9hcHAvR2` + - `9OYXRpdmVBY3Rpdml0eSQ1OwAhTG9yZy9nb2xhbmcvYXBwL0dvTmF0aXZlQWN0aXZpdHk7` + - `ABROVU1CRVJfS0VZQk9BUkRfQ09ERQAJT3BlbiBGaWxlABZQQVNTV09SRF9LRVlCT0FSRF` + - `9DT0RFAAdTREtfSU5UABhTSU5HTEVMSU5FX0tFWUJPQVJEX0NPREUACVNhdmUgRmlsZQAB` + - `VgACVkkAA1ZJSQAFVklJSUkABFZJSUwAAlZMAANWTEkABVZMSUlJAApWTElJSUlJSUlJAA` + - `NWTEwAAlZaAAFaAAJaTAADWkxJAARaTElMAANaTFoAE1tMamF2YS9sYW5nL1N0cmluZzsA` + - `Alx8AAphY2Nlc3MkMDAwAAphY2Nlc3MkMDAyAAphY2Nlc3MkMTAwAAphY2Nlc3MkMjAwAA` + - `phY2Nlc3MkMjAyAAphY2Nlc3MkMzAwAAphY2Nlc3MkNDAwAAphY2Nlc3MkNTAxAAthY2Nl` + - `c3NGbGFncwALYWRkQ2F0ZWdvcnkADmFkZENvbnRlbnRWaWV3AAhhZGRGbGFncwAZYWRkT2` + - `5MYXlvdXRDaGFuZ2VMaXN0ZW5lcgAWYWRkVGV4dENoYW5nZWRMaXN0ZW5lcgAQYWZ0ZXJU` + - `ZXh0Q2hhbmdlZAAUYW5kcm9pZC5hcHAubGliX25hbWUAJWFuZHJvaWQuaW50ZW50LmFjdG` + - `lvbi5DUkVBVEVfRE9DVU1FTlQAI2FuZHJvaWQuaW50ZW50LmFjdGlvbi5PUEVOX0RPQ1VN` + - `RU5UAChhbmRyb2lkLmludGVudC5hY3Rpb24uT1BFTl9ET0NVTUVOVF9UUkVFACBhbmRyb2` + - `lkLmludGVudC5jYXRlZ29yeS5PUEVOQUJMRQAfYW5kcm9pZC5pbnRlbnQuZXh0cmEuTUlN` + - `RV9UWVBFUwAaYW5kcm9pZC5pbnRlbnQuZXh0cmEuVElUTEUAF2FwcGxpY2F0aW9uL3gtZG` + - `lyZWN0b3J5AAtiYWNrUHJlc3NlZAARYmVmb3JlVGV4dENoYW5nZWQADGJyaW5nVG9Gcm9u` + - `dAAIY29udGFpbnMADWNyZWF0ZUNob29zZXIADmRvSGlkZUtleWJvYXJkAA5kb1Nob3dGaW` + - `xlT3BlbgAOZG9TaG93RmlsZVNhdmUADmRvU2hvd0tleWJvYXJkAAFlAAZlcXVhbHMAIWV4` + - `Y2VwdGlvbiByZWFkaW5nIEtleUNoYXJhY3Rlck1hcAASZmlsZVBpY2tlclJldHVybmVkAA` + - `xmaW5kVmlld0J5SWQADmZpbmlzaEFjdGl2aXR5AANnZXQAD2dldEFic29sdXRlUGF0aAAP` + - `Z2V0QWN0aXZpdHlJbmZvAAtnZXRDYWNoZURpcgAMZ2V0Q29tcG9uZW50ABBnZXRDb25maW` + - `d1cmF0aW9uAAdnZXREYXRhAAxnZXREZWNvclZpZXcACWdldEhlaWdodAAJZ2V0SW50ZW50` + - `ABFnZXRQYWNrYWdlTWFuYWdlcgAMZ2V0UmVzb3VyY2VzAAtnZXRSb290VmlldwATZ2V0Um` + - `9vdFdpbmRvd0luc2V0cwAHZ2V0UnVuZQAJZ2V0U3RyaW5nABBnZXRTeXN0ZW1TZXJ2aWNl` + - `ABpnZXRTeXN0ZW1XaW5kb3dJbnNldEJvdHRvbQAYZ2V0U3lzdGVtV2luZG93SW5zZXRMZW` + - `Z0ABlnZXRTeXN0ZW1XaW5kb3dJbnNldFJpZ2h0ABdnZXRTeXN0ZW1XaW5kb3dJbnNldFRv` + - `cAAHZ2V0VGV4dAAJZ2V0VG1wZGlyAAhnZXRXaWR0aAAJZ2V0V2luZG93AA5nZXRXaW5kb3` + - `dUb2tlbgAcZ2V0V2luZG93VmlzaWJsZURpc3BsYXlGcmFtZQAQZ29OYXRpdmVBY3Rpdml0` + - `eQAGaGVpZ2h0AAxoaWRlS2V5Ym9hcmQAF2hpZGVTb2Z0SW5wdXRGcm9tV2luZG93AAlpZ2` + - `5vcmVLZXkADGlucHV0X21ldGhvZAANaW5zZXRzQ2hhbmdlZAAOa2V5Ym9hcmREZWxldGUA` + - `DWtleWJvYXJkVHlwZWQABGxlZnQABmxlbmd0aAAEbG9hZAALbG9hZExpYnJhcnkAEmxvYW` + - `RMaWJyYXJ5IGZhaWxlZAAnbG9hZExpYnJhcnk6IG5vIG1hbmlmZXN0IG1ldGFkYXRhIGZv` + - `dW5kAAltVGV4dEVkaXQACG1ldGFEYXRhAARuYW1lABBvbkFjdGl2aXR5UmVzdWx0AA1vbk` + - `JhY2tQcmVzc2VkABZvbkNvbmZpZ3VyYXRpb25DaGFuZ2VkAAhvbkNyZWF0ZQAOb25FZGl0` + - `b3JBY3Rpb24ADm9uTGF5b3V0Q2hhbmdlAA1vblRleHRDaGFuZ2VkAAhwdXRFeHRyYQAMcm` + - `VxdWVzdEZvY3VzAANydW4ADXJ1bk9uVWlUaHJlYWQAC3NldERhcmtNb2RlAA1zZXRJbWVP` + - `cHRpb25zAAxzZXRJbnB1dFR5cGUAD3NldExheW91dFBhcmFtcwAZc2V0T25FZGl0b3JBY3` + - `Rpb25MaXN0ZW5lcgAMc2V0U2VsZWN0aW9uAAdzZXRUZXh0AAdzZXRUeXBlAA1zZXRWaXNp` + - `YmlsaXR5AApzZXR1cEVudHJ5AAxzaG93RmlsZU9wZW4ADHNob3dGaWxlU2F2ZQAMc2hvd0` + - `tleWJvYXJkAA1zaG93U29mdElucHV0AAVzcGxpdAAWc3RhcnRBY3Rpdml0eUZvclJlc3Vs` + - `dAALc3ViU2VxdWVuY2UABnRoaXMkMAAGdGhpcyQxAAh0b1N0cmluZwADdG9wAAZ1aU1vZG` + - `UAInVua25vd24ga2V5Ym9hcmQgdHlwZSwgdXNlIGRlZmF1bHQADHVwZGF0ZUxheW91dAAL` + - `dXBkYXRlVGhlbWUAEHZhbCRrZXlib2FyZFR5cGUABXZhbHVlAAV3aWR0aAABfABuAQAHDg` + - `BxAwAAAAcOPJcAVQIAAAcOAFgAB0oPLQIPaHmWlwIL4Gm0ARcPW5aWl6WWAlksIzwvAnNZ` + - `AJEBAQAHDgCUAQAHDrQA6wEBAAcOAO4BCQAAAAAAAAAAAAcOWgCEAgEABw4AnwIBAAcdaX` + - `jSARsPiQCRAgQAAAAABw6tAnodLT11AIcCBAAAAAAHDqoaLQD0AQEABw4A9wEABx3htLVb` + - `lra0ARcQAiTgAMQCAQAHDgDHAgAHDloANQAHDjg/LQAeAQAHDgAeAgAABw4AHgIAAAcOAB` + - `4BAAcOAB4CAAAHDgAeAAcOAB4BAAcOAB4BAAcOAL8BAwAAAAcdhzQCeywgHoMAiQEABw5a` + - `ANYBAAcOS6NMS38Cex2HSx4A9AEABw4CNoYAmgEBAAcOWgCuAQIAAAcOWgBRAQAHDloAjQ` + - `EABw6HtIiMAJ4BAQAHHXjheESWAncd4Vq0ajwAsgECAAAHDnjhWrdaWqUCex0AVQEABw4C` + - `MYYAxAIABw6MADoABw4ArwIDAAAABw4CDGgCeR08bEsAwAIABw48AM4CAQAHDjw8AOUBAQ` + - `AHDjw8PLW0jAA/AAcOwwIOLAJ2HYeFTB5atbT/0ADTAgEABw6WPBsAAh4B2AEaPwIfAmgE` + - `ALIBHgIeAdgBGloCIAHYARwBFwICHgHYARpXAh4B2AEabgIeAdgBGkkCHgHYARpxAh4B2A` + - `EaXQdEAAAIBAAEAQQCBAIEAwQBAAEBAQWQIDyAgATAGz0B3BsAAgEBBpAgAZAgPoCABIgc` + - `PwGoHAABAQEIkCBAgIAEiB9BAaQfAAEBAQmQIEKAgATMH0MB6B8AAQEDCpAgRICABIQgRQ` + - `GgIAEBsCEBAfQhAAEBAQuQIEiAgATEIkkB4CIAAQEBDJAgSoCABNAkSwHsJAgCFgwNGgEa` + - `ARoBGgEaARoBGgEKFQIBAkyBgASIJQGIIKwlAYggxCUBiCDcJQGIIPQlAYggjCYBiCCkJg` + - `GIILwmAYgg1CYCggIABYICAAcI7CYECMAnAYICAAGCAgABggIAAQLcJwaCAgABAuAoAQiE` + - `KQEIoCkBCLwpVwDYKQEAsCoBAPArAQD8LAMBoC0HAMQtBwToLQEBsC4BAcguAQHoLggAyC` + - `8BBKQxAAARAAAAAAAAAAEAAAAAAAAAAQAAANsAAABwAAAAAgAAADUAAADcAwAAAwAAAEUA` + - `AACwBAAABAAAABcAAADsBwAABQAAAHgAAACkCAAABgAAAAgAAABkDAAAAxAAAAgAAABkDQ` + - `AAASAAACwAAADADQAABiAAAAcAAADUGAAAARAAACYAAABMGQAAAiAAANsAAACkGgAAAyAA` + - `ACwAAACcKQAABCAAAAkAAABuKwAABSAAAAEAAACxKwAAACAAAAgAAADCKwAAABAAAAEAAA` + - `DwLAAA` + + `JgDNAAAAMQAgANUAAAAxACcA1gAAACoAAAAAAAAAJQAAAFAZAAALAAAA2BgAAMYrAAAAAA` + + `AAKwAAAAAAAAAlAAAAWBkAAAsAAADoGAAA1ysAAAAAAAAsAAAAAAAAACUAAABYGQAACwAA` + + `AAAZAADrKwAAAAAAAC0AAAAAAAAAJQAAAGAZAAALAAAAEBkAAPwrAAAAAAAALgAAAAAAAA` + + `AlAAAAaBkAAAsAAAAgGQAADSwAAAAAAAAvAAAAAAAAACUAAABYGQAACwAAADAZAAAmLAAA` + + `AAAAADAAAAAAAAAAJQAAAFgZAAALAAAAQBkAADcsAAAAAAAAMQAAAAEAAAABAAAAAAAAAA` + + `sAAAAAAAAASCwAALUrAAACAAAAcisAAHkrAAACAAAAgisAAHkrAAABAAAAiSsAAAIAAACS` + + `KwAAeSsAAAIAAACZKwAAeSsAAAIAAACgKwAAeSsAAAIAAACnKwAAeSsAAAIAAACuKwAAeS` + + `sAAAIAAgABAAAAoCkAAAYAAABbAQUAcBA3AAAADgAGAAQAAgAAAKYpAAAOAAAAEmAzBAsA` + + `VCAFAFQABgAaAQEAcSBPABAAEgAPAAMAAwABAAAAsCkAAAgAAABbAQYAWQIHAHAQNwAAAA` + + `4ABgABAAMAAAC3KQAAqgAAABUCAEASYRIEFQAIAFJTBwArA5YAAAABIRoCCgAaA9QAcSAV` + + `ADIAVFIGAHEQTQACAAwCFQMAArYxbiAsABIAVFEGAHEQTQABAAwBbiAtAAEAVFAGAHEQTQ` + + `AAAAwAIgEqAHAgPABRAG4gLwAQAFRQBgASEXEgUQAQAFRQBgBxEE0AAAAMABoBBABuIDEA` + + `EABUUAYAcRBNAAAADABUUQYAcRBNAAEADAFuECoAAQAMAXIQFAABAAoBbiAwABAAVFAGAH` + + `EgUQBAAFRQBgBxEE0AAAAMAG4gMgBAAFRQBgBxEE0AAAAMAG4QKQAAAFRQBgBxEE0AAAAM` + + `AG4QKwAAAFRQBgAaAaYAbiBjABAADAAfABkAVFEGAHEQTQABAAwBbjAmABAEDgABISiAFA` + + `ACAAgAKQB8/xQAkAAIACkAcP8AAQQAAAAAAIoAAAALAAAAjAAAAJEAAAACAAIAAQAAANsp` + + `AAAGAAAAWwEIAHAQNwAAAA4AAwABAAIAAADiKQAADAAAAFQgCABxEE0AAAAMABMBCABuID` + + `IAEAAOAAIAAgABAAAA6SkAAAYAAABbAQkAcBA3AAAADgALAAoAAQAAAPApAAAGAAAAVBAJ` + + `AG4QdgAAAA4AAgACAAEAAAAAKgAABgAAAFsBCgBwEDcAAAAOAAQAAgACAAAAByoAAD8AAA` + + `ASEXIQFAADAAoANRA5AFQgCgBUAAsAcSBRABAAVCAKAFQACwBxEE0AAAAMABoBBABuIDEA` + + `EABUIAoAVAALAHEQTQAAAAwAVCEKAFQRCwBxEE0AAQAMAW4QKgABAAwBchAUAAEACgFuID` + + `AAEABUIAoAVAALABIBcSBRABAADgAAAAcABQABAAAAFSoAABoAAABUIAoAVAALAHEQUAAA` + + `AAoAOAADAA4APQX//xIANVD8/1QhCgBUEQsAcRBTAAEA2AAAASj1BwAFAAMAAAAmKgAAHw` + + `AAAFQgCgBUAAsAcRBQAAAACgA4AAMADgA9Bv//VCAKAFQACwCQAQQGcjA1AEMBDAFyEDYA` + + `AQAMAXEgTwAQACjsAAACAAIAAQAAADMqAAAGAAAAWwELAHAQNwAAAA4ABQABAAMAAAA6Kg` + + `AAbwAAABLjVEALACIBGgBxAFIAAAAMAnAgJwAhAHEgTgAQAFRACwBxEE0AAAAMABMBCABu` + + `IDIAEABUQAsAcRBNAAAADAAVAQgAbiAtABAAIgAbAHAwMwAwA1RBCwBxEE0AAQAMAW4gLg` + + `ABAFRBCwBUQgsAcRBNAAIADAJuMFUAIQBUQAsAcRBNAAAADAAaAQQAbiAxABAAVEALAHEQ` + + `TQAAAAwAVEELAHEQTQABAAwBbhAqAAEADAFyEBQAAQAKAW4gMAAQAFRACwBxEE0AAAAMAC` + + `IBLgBwIEQAQQBuICgAEAAOAAAAAgACAAEAAABNKgAABgAAAFsBDABwEDcAAAAOAAIAAQAB` + + `AAAAVCoAAAYAAABUEAwAcRBUAAAADgACAAEAAQAAAFsqAAAJAAAAcBAAAAEAEgBcEBUAaQ` + + `EUAA4AAAACAAEAAAAAAGMqAAADAAAAVBAWABEAAAACAAIAAAAAAGkqAAADAAAAWwEWABEB` + + `AAACAAIAAgAAAHAqAAAEAAAAcCBpABAADgACAAEAAAAAAHcqAAADAAAAVRAVAA8AAAACAA` + + `IAAAAAAH0qAAADAAAAXAEVAA8BAAABAAAAAAAAAIQqAAADAAAAYgAUABEAAAABAAEAAQAA` + + `AIkqAAAEAAAAcBBoAAAADgABAAEAAQAAAI8qAAAEAAAAbxABAAAADgAHAAMAAwABAJUqAA` + + `AZAAAAEvBxEBgABAAMAW4wFwBRBgoBOQEDAA8AARAo/g0BGgIKABoDggBxMBYAMgEo9Q0B` + + `KPMAAAEAAAAHAAEAAQIRFyMOAAABAAAAAQAAAKYqAAAGAAAAYgAUAG4QVwAAAA4ABAABAA` + + `MAAQCtKgAAMwAAAG4QYAADAAwAbhBfAAMADAFuEAgAAQAMARMCgABuMA0AEAIMAFQBAAA5` + + `AQoAGgAKABoBrwBxIBUAEAAOAFQAAAAaAW8AbiATABAADABxEDsAAAAo9A0AGgEKABoCrg` + + `BxMBYAIQAo6wAAAAAAACkAAQABASMqAgABAAIAAAC+KgAACQAAACIALwBwIEgAEABuIG8A` + + `AQAOAAAAAgABAAIAAADHKgAABgAAAGIAFABuIFgAEAAOAAMAAgADAAAAzyoAAAYAAABiAB` + + `QAbjBZABACDgACAAEAAgAAANgqAAAGAAAAYgAUAG4gWgAQAA4ABAABAAMAAADfKgAAJAAA` + + `ABoApgBuIGMAAwAMAB8AGQAUAQIAAgFuIFwAEwAMAW4QGwABAAwBbhAeAAEADAESAm4wJQ` + + `AQAiIALABwIEAAMABuIG8AAwAOAAYAAgADAAAA6SoAAFcAAAASEyIABAAaAXEAcCAEABAA` + + `GgF2AG4gOQBRAAoBOAEcAGABBAATAhUANCEWACIABAAaAXIAcCAEABAAbiAGADAAGgFJAH` + + `EgBwAQAAwAbjB1AAQDDgAaAdoAbiA4ABUACgE4AR4AYAEEABMCEwA0IRgAGgEDAG4gDAAQ` + + `ABoBdAAaAl8AbiA6ACUADAJuMAsAEAIaAXMAbiAFABAAKNNuIAwAUAAaAXMAbiAFABAAKM` + + `oAAAYAAwADAAAA/SoAAD4AAAAiAAQAGgFwAHAgBAAQABoB2gBuIDgAFAAKATgBLQBgAQQA` + + `EwITADQhJwAaAQMAbiAMABAAGgF0ABoCXwBuIDoAJAAMAm4wCwAQAhoBdQBuMAoAEAUaAX` + + `MAbiAFABAAGgFNAHEgBwAQAAwAEiFuMHUAAwEOAG4gDABAACjoAwACAAMAAAAPKwAACQAA` + + `ACIAKwBwMD4AEAJuIG8AAQAOAAAAAgABAAIAAAAYKwAACQAAACIAMABwIEoAEABuIG8AAQ` + + `AOAAAAAgABAAEAAAAfKwAACQAAAG4QXgABAAwAbhA0AAAADAARAAAABQAEAAIAAAAkKwAA` + + `HAAAABIQMgIGABIgMgIDAA4AEvAyAwgAGgAAAHAgWwABACj3bhAJAAQADABuEBIAAAAMAH` + + `AgWwABACjrAQABAAEAAAA2KwAABAAAAHAQVgAAAA4AAgACAAIAAAA9KwAABwAAAG8gAgAQ` + + `AG4gdwAQAA4AAAAEAAIAAgAAAEYrAAAoAAAAcBBqAAIAbyADADIAcBBxAAIAbhBhAAIADA` + + `BuEA4AAAAMAG4gdwACABQAAgACAW4gXAACAAwAbhAbAAAADAAiAS0AcCBCACEAbiAZABAA` + + `DgAHAAEABQABAFMrAABgAAAAbhBlAAYADABuECAAAAAMAG4QHAAAAAwAOQADAA4AbhAkAA` + + `AACgFuECEAAAAKAm4QIgAAAAoDbhAjAAAACgBwUGcAFjIo7A0AIgAJAHAQDwAAAG4QZQAG` + + `AAwBbhAgAAEADAFuIB8AAQAUAQIAAgFuIFwAFgAMAW4QGwABAAwBUgIDAG4QGgABAAoDbh` + + `AQAAAACgSxQ1IEAwCxQ1IEAgBuEB0AAQAKAW4QEQAAAAoFsVFSAAIAkQABAHBQZwAmQyiv` + + `AAAAACIAAQABASQjBAACAAIAAABoKwAADwAAAFIwAQDdAAAwEwEgADMQBwASEHAgcAACAA` + + `4AEgAo+wAAZA0AAAAAAAAAAAAAAAAAAHANAAAAAAAAAQAAAAAAAAA+AAAAfA0AAIQNAAAA` + + `AAAAAAAAAAAAAACQDQAAAAAAAAAAAAAAAAAAnA0AAAAAAAAAAAAAAAAAAKgNAAAAAAAAAA` + + `AAAAAAAAC0DQAAAAAAAAAAAAAAAAAAAQAAABwAAAABAAAAJgAAAAEAAAAUAAAAAQAAAA8A` + + `AAACAAAAAAAAAAMAAAAAAAAAAAAAAAIAAAAnACcAAwAAACcAJwApAAAAAQAAAAAAAAACAA` + + `AABAAiAAEAAAAnAAAAAgAAACcANAACAAAAAgAAAAEAAAAxAAAAAgAAADEAGgAEAAAAAAAA` + + `AAAAAAADAAAAAAAAAAQAAAABAAAAAwAAAAIAAAAEAAAAAQAAAAcAAAABAAAACQAAAAEAAA` + + `AMAAAAAQAAAA4AAAAJAAAAFQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAFQAWAAEAAAAWAAAA` + + `AQAAACIAAAAEAAAAIgAAAAAAAAABAAAAKwAAAAEAAAAvAAAAAgAAADEAAAACAAAAMQAnAA` + + `EAAAAzAAAAAgAAAA0AAAACAAAAFQAAAAMAAAAdAAAAEwAAAAEAAAAlAAAAAgAAADEAMwAA` + + `AAEKAAMoKVYAAyovKgABMAAGPGluaXQ+ABJERUZBVUxUX0lOUFVUX1RZUEUAFURFRkFVTF` + + `RfS0VZQk9BUkRfQ09ERQAORklMRV9PUEVOX0NPREUADkZJTEVfU0FWRV9DT0RFAARGeW5l` + + `ABVHb05hdGl2ZUFjdGl2aXR5LmphdmEAAUkAA0lJSQAESUlJSQADSUxMAARJTExMAAFMAA` + + `JMSQADTElJAAJMTAADTExJAANMTEwAHExhbmRyb2lkL2FwcC9OYXRpdmVBY3Rpdml0eTsA` + + `H0xhbmRyb2lkL2NvbnRlbnQvQ29tcG9uZW50TmFtZTsAGUxhbmRyb2lkL2NvbnRlbnQvQ2` + + `9udGV4dDsAGExhbmRyb2lkL2NvbnRlbnQvSW50ZW50OwAhTGFuZHJvaWQvY29udGVudC9w` + + `bS9BY3Rpdml0eUluZm87ACNMYW5kcm9pZC9jb250ZW50L3BtL1BhY2thZ2VNYW5hZ2VyOw` + + `AjTGFuZHJvaWQvY29udGVudC9yZXMvQ29uZmlndXJhdGlvbjsAH0xhbmRyb2lkL2NvbnRl` + + `bnQvcmVzL1Jlc291cmNlczsAF0xhbmRyb2lkL2dyYXBoaWNzL1JlY3Q7ABFMYW5kcm9pZC` + + `9uZXQvVXJpOwAaTGFuZHJvaWQvb3MvQnVpbGQkVkVSU0lPTjsAE0xhbmRyb2lkL29zL0J1` + + `bmRsZTsAFExhbmRyb2lkL29zL0lCaW5kZXI7ABdMYW5kcm9pZC90ZXh0L0VkaXRhYmxlOw` + + `AaTGFuZHJvaWQvdGV4dC9UZXh0V2F0Y2hlcjsAEkxhbmRyb2lkL3V0aWwvTG9nOwAzTGFu` + + `ZHJvaWQvdmlldy9LZXlDaGFyYWN0ZXJNYXAkVW5hdmFpbGFibGVFeGNlcHRpb247AB5MYW` + + `5kcm9pZC92aWV3L0tleUNoYXJhY3Rlck1hcDsAF0xhbmRyb2lkL3ZpZXcvS2V5RXZlbnQ7` + + `ACpMYW5kcm9pZC92aWV3L1ZpZXckT25MYXlvdXRDaGFuZ2VMaXN0ZW5lcjsAE0xhbmRyb2` + + `lkL3ZpZXcvVmlldzsAJUxhbmRyb2lkL3ZpZXcvVmlld0dyb3VwJExheW91dFBhcmFtczsA` + + `FUxhbmRyb2lkL3ZpZXcvV2luZG93OwAbTGFuZHJvaWQvdmlldy9XaW5kb3dJbnNldHM7AC` + + `1MYW5kcm9pZC92aWV3L2lucHV0bWV0aG9kL0lucHV0TWV0aG9kTWFuYWdlcjsAGUxhbmRy` + + `b2lkL3dpZGdldC9FZGl0VGV4dDsAKUxhbmRyb2lkL3dpZGdldC9GcmFtZUxheW91dCRMYX` + + `lvdXRQYXJhbXM7ADBMYW5kcm9pZC93aWRnZXQvVGV4dFZpZXckT25FZGl0b3JBY3Rpb25M` + + `aXN0ZW5lcjsAGUxhbmRyb2lkL3dpZGdldC9UZXh0VmlldzsAI0xkYWx2aWsvYW5ub3RhdG` + + `lvbi9FbmNsb3NpbmdNZXRob2Q7AB5MZGFsdmlrL2Fubm90YXRpb24vSW5uZXJDbGFzczsA` + + `HUxkYWx2aWsvYW5ub3RhdGlvbi9TaWduYXR1cmU7AA5MamF2YS9pby9GaWxlOwAYTGphdm` + + `EvbGFuZy9DaGFyU2VxdWVuY2U7ABVMamF2YS9sYW5nL0V4Y2VwdGlvbjsAHUxqYXZhL2xh` + + `bmcvTm9TdWNoTWV0aG9kRXJyb3I7ABJMamF2YS9sYW5nL09iamVjdDsAFExqYXZhL2xhbm` + + `cvUnVubmFibGU7ABJMamF2YS9sYW5nL1N0cmluZzsAEkxqYXZhL2xhbmcvU3lzdGVtOwAV` + + `TGphdmEvbGFuZy9UaHJvd2FibGU7ACVMb3JnL2dvbGFuZy9hcHAvR29OYXRpdmVBY3Rpdm` + + `l0eSQxJDE7ACNMb3JnL2dvbGFuZy9hcHAvR29OYXRpdmVBY3Rpdml0eSQxOwAjTG9yZy9n` + + `b2xhbmcvYXBwL0dvTmF0aXZlQWN0aXZpdHkkMjsAI0xvcmcvZ29sYW5nL2FwcC9Hb05hdG` + + `l2ZUFjdGl2aXR5JDM7ACVMb3JnL2dvbGFuZy9hcHAvR29OYXRpdmVBY3Rpdml0eSQ0JDE7` + + `ACNMb3JnL2dvbGFuZy9hcHAvR29OYXRpdmVBY3Rpdml0eSQ0OwAjTG9yZy9nb2xhbmcvYX` + + `BwL0dvTmF0aXZlQWN0aXZpdHkkNTsAIUxvcmcvZ29sYW5nL2FwcC9Hb05hdGl2ZUFjdGl2` + + `aXR5OwAUTlVNQkVSX0tFWUJPQVJEX0NPREUACU9wZW4gRmlsZQAWUEFTU1dPUkRfS0VZQk` + + `9BUkRfQ09ERQAHU0RLX0lOVAAYU0lOR0xFTElORV9LRVlCT0FSRF9DT0RFAAlTYXZlIEZp` + + `bGUAAVYAAlZJAANWSUkABVZJSUlJAARWSUlMAAJWTAADVkxJAAVWTElJSQAKVkxJSUlJSU` + + `lJSQADVkxMAAJWWgABWgACWkwAA1pMSQAEWkxJTAADWkxaABNbTGphdmEvbGFuZy9TdHJp` + + `bmc7AAJcfAAKYWNjZXNzJDAwMAAKYWNjZXNzJDAwMgAKYWNjZXNzJDEwMAAKYWNjZXNzJD` + + `IwMAAKYWNjZXNzJDIwMgAKYWNjZXNzJDMwMAAKYWNjZXNzJDQwMAAKYWNjZXNzJDUwMQAL` + + `YWNjZXNzRmxhZ3MAC2FkZENhdGVnb3J5AA5hZGRDb250ZW50VmlldwAIYWRkRmxhZ3MAGW` + + `FkZE9uTGF5b3V0Q2hhbmdlTGlzdGVuZXIAFmFkZFRleHRDaGFuZ2VkTGlzdGVuZXIAEGFm` + + `dGVyVGV4dENoYW5nZWQAFGFuZHJvaWQuYXBwLmxpYl9uYW1lACVhbmRyb2lkLmludGVudC` + + `5hY3Rpb24uQ1JFQVRFX0RPQ1VNRU5UACNhbmRyb2lkLmludGVudC5hY3Rpb24uT1BFTl9E` + + `T0NVTUVOVAAoYW5kcm9pZC5pbnRlbnQuYWN0aW9uLk9QRU5fRE9DVU1FTlRfVFJFRQAgYW` + + `5kcm9pZC5pbnRlbnQuY2F0ZWdvcnkuT1BFTkFCTEUAH2FuZHJvaWQuaW50ZW50LmV4dHJh` + + `Lk1JTUVfVFlQRVMAGmFuZHJvaWQuaW50ZW50LmV4dHJhLlRJVExFABdhcHBsaWNhdGlvbi` + + `94LWRpcmVjdG9yeQALYmFja1ByZXNzZWQAEWJlZm9yZVRleHRDaGFuZ2VkAAxicmluZ1Rv` + + `RnJvbnQACGNvbnRhaW5zAA1jcmVhdGVDaG9vc2VyAA5kb0hpZGVLZXlib2FyZAAOZG9TaG` + + `93RmlsZU9wZW4ADmRvU2hvd0ZpbGVTYXZlAA5kb1Nob3dLZXlib2FyZAABZQAGZXF1YWxz` + + `ACFleGNlcHRpb24gcmVhZGluZyBLZXlDaGFyYWN0ZXJNYXAAEmZpbGVQaWNrZXJSZXR1cm` + + `5lZAAMZmluZFZpZXdCeUlkAA5maW5pc2hBY3Rpdml0eQADZ2V0AA9nZXRBYnNvbHV0ZVBh` + + `dGgAD2dldEFjdGl2aXR5SW5mbwALZ2V0Q2FjaGVEaXIADGdldENvbXBvbmVudAAQZ2V0Q2` + + `9uZmlndXJhdGlvbgAHZ2V0RGF0YQAMZ2V0RGVjb3JWaWV3AAlnZXRIZWlnaHQACWdldElu` + + `dGVudAARZ2V0UGFja2FnZU1hbmFnZXIADGdldFJlc291cmNlcwALZ2V0Um9vdFZpZXcAE2` + + `dldFJvb3RXaW5kb3dJbnNldHMAB2dldFJ1bmUACWdldFN0cmluZwAQZ2V0U3lzdGVtU2Vy` + + `dmljZQAaZ2V0U3lzdGVtV2luZG93SW5zZXRCb3R0b20AGGdldFN5c3RlbVdpbmRvd0luc2` + + `V0TGVmdAAZZ2V0U3lzdGVtV2luZG93SW5zZXRSaWdodAAXZ2V0U3lzdGVtV2luZG93SW5z` + + `ZXRUb3AAB2dldFRleHQACWdldFRtcGRpcgAIZ2V0V2lkdGgACWdldFdpbmRvdwAOZ2V0V2` + + `luZG93VG9rZW4AHGdldFdpbmRvd1Zpc2libGVEaXNwbGF5RnJhbWUAEGdvTmF0aXZlQWN0` + + `aXZpdHkABmhlaWdodAAMaGlkZUtleWJvYXJkABdoaWRlU29mdElucHV0RnJvbVdpbmRvdw` + + `AJaWdub3JlS2V5AAxpbnB1dF9tZXRob2QADWluc2V0c0NoYW5nZWQADmtleWJvYXJkRGVs` + + `ZXRlAA1rZXlib2FyZFR5cGVkAARsZWZ0AAZsZW5ndGgABGxvYWQAC2xvYWRMaWJyYXJ5AB` + + `Jsb2FkTGlicmFyeSBmYWlsZWQAJ2xvYWRMaWJyYXJ5OiBubyBtYW5pZmVzdCBtZXRhZGF0` + + `YSBmb3VuZAAJbVRleHRFZGl0AAhtZXRhRGF0YQAEbmFtZQAQb25BY3Rpdml0eVJlc3VsdA` + + `ANb25CYWNrUHJlc3NlZAAWb25Db25maWd1cmF0aW9uQ2hhbmdlZAAIb25DcmVhdGUADm9u` + + `RWRpdG9yQWN0aW9uAA5vbkxheW91dENoYW5nZQANb25UZXh0Q2hhbmdlZAAIcHV0RXh0cm` + + `EADHJlcXVlc3RGb2N1cwADcnVuAA1ydW5PblVpVGhyZWFkAAtzZXREYXJrTW9kZQANc2V0` + + `SW1lT3B0aW9ucwAMc2V0SW5wdXRUeXBlAA9zZXRMYXlvdXRQYXJhbXMAGXNldE9uRWRpdG` + + `9yQWN0aW9uTGlzdGVuZXIADHNldFNlbGVjdGlvbgAHc2V0VGV4dAAHc2V0VHlwZQANc2V0` + + `VmlzaWJpbGl0eQAKc2V0dXBFbnRyeQAMc2hvd0ZpbGVPcGVuAAxzaG93RmlsZVNhdmUADH` + + `Nob3dLZXlib2FyZAANc2hvd1NvZnRJbnB1dAAFc3BsaXQAFnN0YXJ0QWN0aXZpdHlGb3JS` + + `ZXN1bHQAC3N1YlNlcXVlbmNlAAZ0aGlzJDAABnRoaXMkMQAIdG9TdHJpbmcAA3RvcAAGdW` + + `lNb2RlACJ1bmtub3duIGtleWJvYXJkIHR5cGUsIHVzZSBkZWZhdWx0AAx1cGRhdGVMYXlv` + + `dXQAC3VwZGF0ZVRoZW1lABB2YWwka2V5Ym9hcmRUeXBlAAV2YWx1ZQAFd2lkdGgAAXwAbg` + + `EABw4AcQMAAAAHDjyXAFUCAAAHDgBYAAdKDy0CD2h5w5cCC+BptAEXD1uWlpellgJZLCM8` + + `LwJzWQCRAQEABw4AlAEABw60AOsBAQAHDgDuAQkAAAAAAAAAAAAHDloAhAIBAAcOAJ8CAQ` + + `AHHWl40gEbD4kAkQIEAAAAAAcOrQJ6HS09dQCHAgQAAAAABw6qGi0A9AEBAAcOAPcBAAcd` + + `4bS1W5a2tAEXEAIk4ADEAgEABw4AxwIABw5aADUABw44Py0AHgEABw4AHgIAAAcOAB4CAA` + + `AHDgAeAQAHDgAeAgAABw4AHgAHDgAeAQAHDgAeAQAHDgC/AQMAAAAHHYc0AnssIB6DAIkB` + + `AAcOWgDWAQAHDkujTEt/Ansdh0seAPQBAAcOAjaGAJoBAQAHDloArgECAAAHDloAUQEABw` + + `5aAI0BAAcOh7SIjACeAQEABx144XhElgJ3HeFatGo8ALIBAgAABw544Vq3WlqlAnsdAFUB` + + `AAcOAjGGAMQCAAcOjAA6AAcOAK8CAwAAAAcOAgxoAnkdPGxLAMACAAcOPADOAgEABw48PA` + + `DlAQEABw48PDy1tIwAPwAHDsMCDiwCdh2HhUweWrW0/9AA0wIBAAcOljwbAAIeAdgBGj8C` + + `HwJoBACyAR4CHgHYARpaAiAB2AEcARcCAh4B2AEaVwIeAdgBGm4CHgHYARpJAh4B2AEacQ` + + `IeAdgBGl0HRAAACAQABAEEAgQCBAMEAQABAQEFkCA8gIAEwBs9AdwbAAIBAQaQIAGQID6A` + + `gASIHD8BqBwAAQEBCJAgQICABIwfQQGoHwABAQEJkCBCgIAE0B9DAewfAAEBAwqQIESAgA` + + `SIIEUBpCABAbQhAQH4IQABAQELkCBIgIAEyCJJAeQiAAEBAQyQIEqAgATUJEsB8CQIAhYM` + + `DRoBGgEaARoBGgEaARoBChUCAQJMgYAEjCUBiCCwJQGIIMglAYgg4CUBiCD4JQGIIJAmAY` + + `ggqCYBiCDAJgGIINgmAoICAAWCAgAHCPAmBAjEJwGCAgABggIAAYICAAEC4CcGggIAAQLk` + + `KAEIiCkBCKQpAQjAKVcA3CkBALQqAQD0KwEAgC0DAaQtBwDILQcE7C0BAbQuAQHMLgEB7C` + + `4IAMwvAQSoMQAAEQAAAAAAAAABAAAAAAAAAAEAAADbAAAAcAAAAAIAAAA1AAAA3AMAAAMA` + + `AABFAAAAsAQAAAQAAAAXAAAA7AcAAAUAAAB4AAAApAgAAAYAAAAIAAAAZAwAAAMQAAAIAA` + + `AAZA0AAAEgAAAsAAAAwA0AAAYgAAAHAAAA2BgAAAEQAAAmAAAAUBkAAAIgAADbAAAAqBoA` + + `AAMgAAAsAAAAoCkAAAQgAAAJAAAAcisAAAUgAAABAAAAtSsAAAAgAAAIAAAAxisAAAAQAA` + + `ABAAAA9CwAAA==` + `` diff --git a/cmd/fyne_demo/tutorials/icons.go b/cmd/fyne_demo/tutorials/icons.go index 74d56ff752..0d0ae952b6 100644 --- a/cmd/fyne_demo/tutorials/icons.go +++ b/cmd/fyne_demo/tutorials/icons.go @@ -145,9 +145,9 @@ func loadIcons() []iconInfo { {"ViewRefreshIcon", theme.ViewRefreshIcon()}, {"VisibilityIcon", theme.VisibilityIcon()}, {"VisibilityOffIcon", theme.VisibilityOffIcon()}, - {"ZoomFitIcon", theme.ZoomFitIcon()}, - {"ZoomInIcon", theme.ZoomInIcon()}, - {"ZoomOutIcon", theme.ZoomOutIcon()}, + {"ViewZoomFitIcon", theme.ZoomFitIcon()}, + {"ViewZoomInIcon", theme.ZoomInIcon()}, + {"ViewZoomOutIcon", theme.ZoomOutIcon()}, {"MoreHorizontalIcon", theme.MoreHorizontalIcon()}, {"MoreVerticalIcon", theme.MoreVerticalIcon()}, diff --git a/container/doctabs.go b/container/doctabs.go index 4fc8a482ce..cb276f6b91 100644 --- a/container/doctabs.go +++ b/container/doctabs.go @@ -363,6 +363,13 @@ func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) { 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() @@ -388,7 +395,7 @@ func (r *docTabsRenderer) scrollToSelected() { func (r *docTabsRenderer) updateIndicator(animate bool) { if r.docTabs.current < 0 { r.indicator.FillColor = color.Transparent - r.indicator.Refresh() + r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), animate) return } diff --git a/data/binding/listbinding.go b/data/binding/listbinding.go index a0bb5d2d16..539f307c5a 100644 --- a/data/binding/listbinding.go +++ b/data/binding/listbinding.go @@ -16,6 +16,9 @@ type listBase struct { // 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 } @@ -25,6 +28,9 @@ func (b *listBase) GetItem(i int) (DataItem, error) { // 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) } diff --git a/data/binding/treebinding.go b/data/binding/treebinding.go index 557f401724..6d9c817a87 100644 --- a/data/binding/treebinding.go +++ b/data/binding/treebinding.go @@ -21,6 +21,9 @@ type treeBase struct { // GetItem returns the DataItem at the specified id. func (t *treeBase) GetItem(id string) (DataItem, error) { + t.lock.RLock() + defer t.lock.RUnlock() + if item, ok := t.items[id]; ok { return item, nil } @@ -30,6 +33,9 @@ func (t *treeBase) GetItem(id string) (DataItem, error) { // ChildIDs returns the ordered IDs of items in this data tree that are children of the specified ID. func (t *treeBase) ChildIDs(id string) []string { + t.lock.RLock() + defer t.lock.RUnlock() + if ids, ok := t.ids[id]; ok { return ids } diff --git a/dialog/file.go b/dialog/file.go index af3ce3ef23..5327d24e64 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -7,6 +7,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" @@ -54,7 +55,9 @@ type fileDialog struct { showHidden bool view viewLayout - data []fyne.URI + + data []fyne.URI + dataLock sync.RWMutex win *widget.PopUp selected fyne.URI @@ -196,8 +199,8 @@ func (f *fileDialog) makeUI() fyne.CanvasObject { f.filesScroll = container.NewScroll(nil) // filesScroll's content will be set by setView function. verticalExtra := float32(float64(fileIconSize) * 0.25) - f.filesScroll.SetMinSize(fyne.NewSize(fileIconCellWidth*2+theme.Padding(), - (fileIconSize+fileTextSize)+theme.Padding()*2+verticalExtra)) + itemMin := f.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize() + f.filesScroll.SetMinSize(itemMin.AddWidthHeight(itemMin.Width+theme.Padding()*3, verticalExtra)) f.breadcrumb = container.NewHBox() f.breadcrumbScroll = container.NewHScroll(container.NewPadded(f.breadcrumb)) @@ -334,7 +337,9 @@ func (f *fileDialog) loadFavorites() { } func (f *fileDialog) refreshDir(dir fyne.ListableURI) { + f.dataLock.Lock() f.data = nil + f.dataLock.Unlock() files, err := dir.List() if err != nil { @@ -366,7 +371,10 @@ func (f *fileDialog) refreshDir(dir fyne.ListableURI) { icons = append(icons, file) } } + + f.dataLock.Lock() f.data = icons + f.dataLock.Unlock() f.files.Refresh() f.filesScroll.Offset = fyne.NewPos(0, 0) @@ -480,20 +488,26 @@ func (f *fileDialog) setSelected(file fyne.URI, id int) { func (f *fileDialog) setView(view viewLayout) { f.view = view count := func() int { + f.dataLock.RLock() + defer f.dataLock.RUnlock() + return len(f.data) } template := func() fyne.CanvasObject { return f.newFileItem(storage.NewFileURI("./tempfile"), true, false) } update := func(id widget.GridWrapItemID, o fyne.CanvasObject) { - dir := f.data[id] - parent := id == 0 && len(dir.Path()) < len(f.dir.Path()) - _, isDir := dir.(fyne.ListableURI) - o.(*fileDialogItem).setLocation(dir, isDir || parent, parent) + if dir, ok := f.getDataItem(id); ok { + parent := id == 0 && len(dir.Path()) < len(f.dir.Path()) + _, isDir := dir.(fyne.ListableURI) + o.(*fileDialogItem).setLocation(dir, isDir || parent, parent) + } } choose := func(id int) { - f.selectedID = id - f.setSelected(f.data[id], id) + if file, ok := f.getDataItem(id); ok { + f.selectedID = id + f.setSelected(file, id) + } } if f.view == gridView { grid := widget.NewGridWrap(count, template, update) @@ -511,6 +525,17 @@ func (f *fileDialog) setView(view viewLayout) { f.filesScroll.Refresh() } +func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) { + f.dataLock.RLock() + defer f.dataLock.RUnlock() + + if id >= len(f.data) { + return nil, false + } + + return f.data[id], true +} + // effectiveStartingDir calculates the directory at which the file dialog should // open, based on the values of startingDirectory, CWD, home, and any error // conditions which occur. @@ -576,8 +601,9 @@ func (f *FileDialog) effectiveStartingDir() fyne.ListableURI { func showFile(file *FileDialog) *fileDialog { d := &fileDialog{file: file, initialFileName: file.initialFileName} ui := d.makeUI() - size := ui.MinSize().Add(fyne.NewSize(fileIconCellWidth*2+theme.Padding()*6+theme.Padding(), - (fileIconSize+fileTextSize)+theme.Padding()*6)) + pad := theme.Padding() + itemMin := d.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize() + size := ui.MinSize().Add(itemMin.AddWidthHeight(itemMin.Width+pad*4, pad*2)) d.win = widget.NewModalPopUp(ui, file.parent.Canvas()) d.win.Resize(size) diff --git a/dialog/file_test.go b/dialog/file_test.go index 37928eb412..b1f9fde167 100644 --- a/dialog/file_test.go +++ b/dialog/file_test.go @@ -117,8 +117,9 @@ func TestFileDialogResize(t *testing.T) { d := &fileDialog{file: file} open := widget.NewButton("open", func() {}) ui := container.NewBorder(nil, nil, nil, open) - originalSize := ui.MinSize().Add(fyne.NewSize(fileIconCellWidth*2+theme.Padding()*4, - (fileIconSize+fileTextSize)+theme.Padding()*4)) + pad := theme.Padding() + itemMin := d.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize() + originalSize := ui.MinSize().Add(itemMin.AddWidthHeight(itemMin.Width+pad*6, pad*3)) d.win = widget.NewModalPopUp(ui, file.parent.Canvas()) d.win.Resize(originalSize) file.dialog = d diff --git a/dialog/fileitem.go b/dialog/fileitem.go index 86e5c15ac7..ba1c0e0959 100644 --- a/dialog/fileitem.go +++ b/dialog/fileitem.go @@ -11,7 +11,6 @@ import ( const ( fileIconSize = 64 fileInlineIconSize = 24 - fileTextSize = 24 fileIconCellWidth = fileIconSize * 1.25 ) @@ -26,14 +25,16 @@ type fileDialogItem struct { func (i *fileDialogItem) CreateRenderer() fyne.WidgetRenderer { text := widget.NewLabelWithStyle(i.name, fyne.TextAlignCenter, fyne.TextStyle{}) - text.Wrapping = fyne.TextTruncate + text.Truncation = fyne.TextTruncateEllipsis + text.Wrapping = fyne.TextWrapBreak icon := widget.NewFileIcon(i.location) return &fileItemRenderer{ - item: i, - icon: icon, - text: text, - objects: []fyne.CanvasObject{icon, text}, + item: i, + icon: icon, + text: text, + objects: []fyne.CanvasObject{icon, text}, + fileTextSize: widget.NewLabel("M\nM").MinSize().Height, // cache two-line label height, } } @@ -76,52 +77,53 @@ func (f *fileDialog) newFileItem(location fyne.URI, dir, up bool) *fileDialogIte } type fileItemRenderer struct { - item *fileDialogItem + item *fileDialogItem + fileTextSize float32 icon *widget.FileIcon text *widget.Label objects []fyne.CanvasObject } -func (s fileItemRenderer) Layout(size fyne.Size) { +func (s *fileItemRenderer) Layout(size fyne.Size) { if s.item.picker.view == gridView { s.icon.Resize(fyne.NewSize(fileIconSize, fileIconSize)) s.icon.Move(fyne.NewPos((size.Width-fileIconSize)/2, 0)) s.text.Alignment = fyne.TextAlignCenter - s.text.Resize(fyne.NewSize(size.Width, fileTextSize)) - s.text.Move(fyne.NewPos(0, size.Height-s.text.MinSize().Height)) + s.text.Resize(fyne.NewSize(size.Width, s.fileTextSize)) + s.text.Move(fyne.NewPos(0, size.Height-s.fileTextSize)) } else { s.icon.Resize(fyne.NewSize(fileInlineIconSize, fileInlineIconSize)) s.icon.Move(fyne.NewPos(theme.Padding(), (size.Height-fileInlineIconSize)/2)) s.text.Alignment = fyne.TextAlignLeading - s.text.Resize(fyne.NewSize(size.Width, fileTextSize)) - s.text.Move(fyne.NewPos(fileInlineIconSize, (size.Height-s.text.MinSize().Height)/2)) + textMin := s.text.MinSize() + s.text.Resize(fyne.NewSize(size.Width, textMin.Height)) + s.text.Move(fyne.NewPos(fileInlineIconSize, (size.Height-textMin.Height)/2)) } s.text.Refresh() } -func (s fileItemRenderer) MinSize() fyne.Size { - var padding fyne.Size - +func (s *fileItemRenderer) MinSize() fyne.Size { if s.item.picker.view == gridView { - padding = fyne.NewSize(fileIconCellWidth-fileIconSize, theme.Padding()) - return fyne.NewSize(fileIconSize, fileIconSize+fileTextSize).Add(padding) + return fyne.NewSize(fileIconCellWidth, fileIconSize+s.fileTextSize) } - padding = fyne.NewSize(theme.Padding(), theme.Padding()*4) - return fyne.NewSize(fileInlineIconSize+s.text.MinSize().Width, fileTextSize).Add(padding) + textMin := s.text.MinSize() + return fyne.NewSize(fileInlineIconSize+textMin.Width+theme.Padding(), textMin.Height) } -func (s fileItemRenderer) Refresh() { +func (s *fileItemRenderer) Refresh() { + s.fileTextSize = widget.NewLabel("M\nM").MinSize().Height // cache two-line label height + s.text.SetText(s.item.name) s.icon.SetURI(s.item.location) } -func (s fileItemRenderer) Objects() []fyne.CanvasObject { +func (s *fileItemRenderer) Objects() []fyne.CanvasObject { return s.objects } -func (s fileItemRenderer) Destroy() { +func (s *fileItemRenderer) Destroy() { } diff --git a/dialog/fileitem_test.go b/dialog/fileitem_test.go index 3e2b972cdd..2a6d603b0b 100644 --- a/dialog/fileitem_test.go +++ b/dialog/fileitem_test.go @@ -6,7 +6,9 @@ import ( "github.com/stretchr/testify/assert" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/test" ) func TestFileItem_Name(t *testing.T) { @@ -106,3 +108,19 @@ func TestNewFileItem_ParentFolder(t *testing.T) { assert.Equal(t, "(Parent)", item.name) assert.Equal(t, parentDir.String()+"/", f.data[0].String()) } + +func TestFileItem_Wrap(t *testing.T) { + f := &fileDialog{file: &FileDialog{}} + _ = f.makeUI() + item := f.newFileItem(storage.NewFileURI("/path/to/filename.txt"), false, false) + item.Resize(item.MinSize()) + label := test.WidgetRenderer(item).(*fileItemRenderer).text + assert.Equal(t, "filename", label.Text) + texts := test.WidgetRenderer(label).Objects() + assert.Equal(t, 1, len(texts)) + + item.setLocation(storage.NewFileURI("/path/to/averylongfilename.svg"), false, false) + texts = test.WidgetRenderer(label).Objects() + assert.Equal(t, 2, len(texts)) + assert.Equal(t, "averylon", texts[0].(*canvas.Text).Text) +} diff --git a/internal/driver/mobile/android.c b/internal/driver/mobile/android.c index 3c57531b03..f1c5bcc620 100644 --- a/internal/driver/mobile/android.c +++ b/internal/driver/mobile/android.c @@ -41,7 +41,7 @@ static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, return m; } -char* getString(uintptr_t jni_env, uintptr_t ctx, jstring str) { +const char* getString(uintptr_t jni_env, uintptr_t ctx, jstring str) { JNIEnv *env = (JNIEnv*)jni_env; const char *chars = (*env)->GetStringUTFChars(env, str, NULL); @@ -65,11 +65,11 @@ jobject parseURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { jobject getClipboard(uintptr_t jni_env, uintptr_t ctx) { JNIEnv *env = (JNIEnv*)jni_env; - jclass ctxClass = (*env)->GetObjectClass(env, ctx); + jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx); jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); jstring service = (*env)->NewStringUTF(env, "clipboard"); - jobject ret = (jobject)(*env)->CallObjectMethod(env, ctx, getSystemService, service); + jobject ret = (*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, service); jthrowable err = (*env)->ExceptionOccurred(env); if (err != NULL) { @@ -80,7 +80,7 @@ jobject getClipboard(uintptr_t jni_env, uintptr_t ctx) { return ret; } -char *getClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { +const char *getClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { JNIEnv *env = (JNIEnv*)jni_env; jobject mgr = getClipboard(jni_env, ctx); if (mgr == NULL) { @@ -120,10 +120,10 @@ void setClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, ch jobject getContentResolver(uintptr_t jni_env, uintptr_t ctx) { JNIEnv *env = (JNIEnv*)jni_env; - jclass ctxClass = (*env)->GetObjectClass(env, ctx); + jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx); jmethodID getContentResolver = find_method(env, ctxClass, "getContentResolver", "()Landroid/content/ContentResolver;"); - return (jobject)(*env)->CallObjectMethod(env, ctx, getContentResolver); + return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getContentResolver); } void* openStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { @@ -165,7 +165,7 @@ void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { return (*env)->NewGlobalRef(env, stream); } -char* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* total) { +jbyte* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* total) { JNIEnv *env = (JNIEnv*)jni_env; jclass streamClass = (*env)->GetObjectClass(env, stream); jmethodID read = find_method(env, streamClass, "read", "([BII)I"); @@ -178,12 +178,12 @@ char* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* t return NULL; } - char* bytes = malloc(sizeof(char)*count); + jbyte* bytes = (jbyte*)malloc(sizeof(jbyte)*count); (*env)->GetByteArrayRegion(env, data, 0, count, bytes); return bytes; } -void writeStream(uintptr_t jni_env, uintptr_t ctx, void* stream, char* buf, int len) { +void writeStream(uintptr_t jni_env, uintptr_t ctx, void* stream, jbyte* buf, int len) { JNIEnv *env = (JNIEnv*)jni_env; jclass streamClass = (*env)->GetObjectClass(env, stream); jmethodID write = find_method(env, streamClass, "write", "([BII)V"); @@ -246,7 +246,7 @@ bool canListContentURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { return false; } - char *str = getString(jni_env, ctx, type); + const char *str = getString(jni_env, ctx, type); return strcmp(str, "vnd.android.document/directory") == 0; } @@ -297,7 +297,7 @@ bool createListableURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { return false; } -char* contentURIGetFileName(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { +const char* contentURIGetFileName(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { JNIEnv *env = (JNIEnv*)jni_env; jobject resolver = getContentResolver(jni_env, ctx); jobject uri = parseURI(jni_env, ctx, uriCstr); @@ -322,7 +322,7 @@ char* contentURIGetFileName(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { if (((jboolean)(*env)->CallBooleanMethod(env, cursor, first)) == JNI_TRUE) { jstring name = (jstring)(*env)->CallObjectMethod(env, cursor, get, 0); - char *fname = getString(jni_env, ctx, name); + const char *fname = getString(jni_env, ctx, name); return fname; } @@ -401,7 +401,7 @@ char* listContentURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { jmethodID toString = (*env)->GetMethodID(env, uriClass, "toString", "()Ljava/lang/String;"); jstring s = (jstring)(*env)->CallObjectMethod(env, childUri, toString); - char *uid = getString(jni_env, ctx, s); + const char *uid = getString(jni_env, ctx, s); // append char *old = ret; diff --git a/internal/driver/mobile/app/GoNativeActivity.java b/internal/driver/mobile/app/GoNativeActivity.java index 866f3246bd..5521b957ae 100644 --- a/internal/driver/mobile/app/GoNativeActivity.java +++ b/internal/driver/mobile/app/GoNativeActivity.java @@ -104,7 +104,7 @@ public void run() { default: Log.e("Fyne", "unknown keyboard type, use default"); } - mTextEdit.setImeOptions(imeOptions); + mTextEdit.setImeOptions(imeOptions|EditorInfo.IME_FLAG_NO_FULLSCREEN); mTextEdit.setInputType(inputType); mTextEdit.setOnEditorActionListener(new OnEditorActionListener() { diff --git a/internal/driver/mobile/app/android.c b/internal/driver/mobile/app/android.c index 1eaaed344c..efe35195ae 100644 --- a/internal/driver/mobile/app/android.c +++ b/internal/driver/mobile/app/android.c @@ -67,13 +67,15 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { static int main_running = 0; // ensure we refresh context on resume in case something has changed... -void processOnResume(ANativeActivity *activity) { +void onResume(ANativeActivity *activity) { JNIEnv* env = activity->env; setCurrentContext(activity->vm, (*env)->NewGlobalRef(env, activity->clazz)); - - onResume(activity); } +void onStart(ANativeActivity *activity) {} +void onPause(ANativeActivity *activity) {} +void onStop(ANativeActivity *activity) {} + // Entry point from our subclassed NativeActivity. // // By here, the Go runtime has been initialized (as we are running in @@ -127,7 +129,7 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_ // Note that onNativeWindowResized is not called on resize. Avoid it. // https://code.google.com/p/android/issues/detail?id=180645 activity->callbacks->onStart = onStart; - activity->callbacks->onResume = processOnResume; + activity->callbacks->onResume = onResume; activity->callbacks->onSaveInstanceState = onSaveInstanceState; activity->callbacks->onPause = onPause; activity->callbacks->onStop = onStop; diff --git a/internal/driver/mobile/app/android.go b/internal/driver/mobile/app/android.go index fdc09b8597..24eb000330 100644 --- a/internal/driver/mobile/app/android.go +++ b/internal/driver/mobile/app/android.go @@ -131,27 +131,11 @@ func callMain(mainPC uintptr) { go callfn.CallFn(mainPC) } -//export onStart -func onStart(activity *C.ANativeActivity) { -} - -//export onResume -func onResume(activity *C.ANativeActivity) { -} - //export onSaveInstanceState func onSaveInstanceState(activity *C.ANativeActivity, outSize *C.size_t) unsafe.Pointer { return nil } -//export onPause -func onPause(activity *C.ANativeActivity) { -} - -//export onStop -func onStop(activity *C.ANativeActivity) { -} - //export onBackPressed func onBackPressed() { k := key.Event{ diff --git a/internal/driver/mobile/app/app.go b/internal/driver/mobile/app/app.go index 3c21778f4a..de56614499 100644 --- a/internal/driver/mobile/app/app.go +++ b/internal/driver/mobile/app/app.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build freebsd || linux || darwin || windows -// +build freebsd linux darwin windows +//go:build freebsd || linux || darwin || windows || openbsd +// +build freebsd linux darwin windows openbsd package app diff --git a/internal/driver/mobile/app/x11.c b/internal/driver/mobile/app/x11.c index 938cdb1028..c0c86ade4f 100644 --- a/internal/driver/mobile/app/x11.c +++ b/internal/driver/mobile/app/x11.c @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (linux && !android) || freebsd -// +build linux,!android freebsd +//go:build (linux && !android) || freebsd || openbsd +// +build linux,!android freebsd openbsd #include "_cgo_export.h" #include diff --git a/internal/driver/mobile/app/x11.go b/internal/driver/mobile/app/x11.go index 300297c3b1..c9e2d524cd 100644 --- a/internal/driver/mobile/app/x11.go +++ b/internal/driver/mobile/app/x11.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (linux && !android) || freebsd -// +build linux,!android freebsd +//go:build (linux && !android) || freebsd || openbsd +// +build linux,!android freebsd openbsd package app @@ -16,6 +16,7 @@ than screens with touch panels. /* #cgo LDFLAGS: -lEGL -lGLESv2 -lX11 #cgo freebsd CFLAGS: -I/usr/local/include/ +#cgo openbsd CFLAGS: -I/usr/X11R6/include/ void createWindow(void); void processEvents(void); diff --git a/internal/driver/mobile/canvas.go b/internal/driver/mobile/canvas.go index 623c520ed0..ec2375ba3b 100644 --- a/internal/driver/mobile/canvas.go +++ b/internal/driver/mobile/canvas.go @@ -7,6 +7,7 @@ import ( "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/mobile" "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/driver" @@ -167,6 +168,9 @@ func (c *mobileCanvas) setMenu(menu fyne.CanvasObject) { } func (c *mobileCanvas) setWindowHead(head fyne.CanvasObject) { + if c.padded { + head = container.NewPadded(head) + } c.windowHead = head c.SetMobileWindowHeadTree(head) } diff --git a/internal/driver/mobile/menu.go b/internal/driver/mobile/menu.go index 50e24d06db..43123baccc 100644 --- a/internal/driver/mobile/menu.go +++ b/internal/driver/mobile/menu.go @@ -55,6 +55,9 @@ func (c *mobileCanvas) showMenu(menu *fyne.MainMenu) { for _, item := range menu.Items { panel.Add(newMenuLabel(item, panel, c)) } + if c.padded { + panel = container.NewPadded(panel) + } bg := canvas.NewRectangle(theme.BackgroundColor()) shadow := canvas.NewHorizontalGradient(theme.ShadowColor(), color.Transparent) diff --git a/internal/driver/mobile/menu_test.go b/internal/driver/mobile/menu_test.go index 4681ae04d4..8581330ebb 100644 --- a/internal/driver/mobile/menu_test.go +++ b/internal/driver/mobile/menu_test.go @@ -32,6 +32,7 @@ func TestMobileCanvas_DismissBar(t *testing.T) { func TestMobileCanvas_DismissMenu(t *testing.T) { c := NewCanvas().(*mobileCanvas) + c.padded = false c.SetContent(canvas.NewRectangle(theme.BackgroundColor())) menu := fyne.NewMainMenu( fyne.NewMenu("Test", fyne.NewMenuItem("TapMe", func() {}))) diff --git a/internal/driver/mobile/mobileinit/ctx_android.go b/internal/driver/mobile/mobileinit/ctx_android.go index b58881a26d..562cc381b9 100644 --- a/internal/driver/mobile/mobileinit/ctx_android.go +++ b/internal/driver/mobile/mobileinit/ctx_android.go @@ -55,6 +55,11 @@ static char* checkException(uintptr_t jnienv) { static void unlockJNI(JavaVM *vm) { (*vm)->DetachCurrentThread(vm); } + +static void deletePrevCtx(JNIEnv* env,jobject ctx){ + if (ctx == NULL) { return; } + (*env)->DeleteGlobalRef(env, ctx); +} */ import "C" @@ -80,7 +85,13 @@ var currentCtx C.jobject // The android.context.Context object must be a global reference. func SetCurrentContext(vm unsafe.Pointer, ctx uintptr) { currentVM = (*C.JavaVM)(vm) + currentCtxPrev := currentCtx currentCtx = (C.jobject)(ctx) + RunOnJVM(func(vm, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) + C.deletePrevCtx(env, C.jobject(currentCtxPrev)) + return nil + }) } // RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv. diff --git a/internal/repository/memory.go b/internal/repository/memory.go index e7870a07a1..3537f58523 100644 --- a/internal/repository/memory.go +++ b/internal/repository/memory.go @@ -1,13 +1,13 @@ package repository import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/storage/repository" - "fmt" "io" "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" ) // declare conformance to interfaces @@ -297,7 +297,7 @@ func (m *InMemoryRepository) List(u fyne.URI) ([]fyne.URI, error) { // does not have one. pSplit := strings.Split(p, "/") ncomp := len(pSplit) - if p[len(p)-1] == '/' { + if len(p) > 0 && p[len(p)-1] == '/' { ncomp-- } diff --git a/internal/repository/memory_test.go b/internal/repository/memory_test.go index 17bb5d0eff..b61b59c1da 100644 --- a/internal/repository/memory_test.go +++ b/internal/repository/memory_test.go @@ -68,6 +68,10 @@ func TestInMemoryRepositoryParsing(t *testing.T) { baz, _ := storage.ParseURI("mem:///baz") assert.Nil(t, err) assert.NotNil(t, baz) + + empty, _ := storage.ParseURI("mem:") + assert.Nil(t, err) + assert.NotNil(t, empty) } func TestInMemoryRepositoryExists(t *testing.T) { @@ -328,6 +332,7 @@ func TestInMemoryRepositoryListing(t *testing.T) { // set up our repository - it's OK if we already registered it m := NewInMemoryRepository("mem") repository.Register("mem", m) + m.Data[""] = []byte{1, 2, 3} m.Data["/foo"] = []byte{1, 2, 3} m.Data["/foo/bar"] = []byte{1, 2, 3} m.Data["/foo/baz/"] = []byte{1, 2, 3} @@ -346,6 +351,11 @@ func TestInMemoryRepositoryListing(t *testing.T) { stringListing = append(stringListing, u.String()) } assert.ElementsMatch(t, []string{"mem:///foo/bar", "mem:///foo/baz/"}, stringListing) + + empty, _ := storage.ParseURI("mem:") // invalid path + canList, err = storage.CanList(empty) + assert.NotNil(t, err) + assert.False(t, canList) } func TestInMemoryRepositoryCreateListable(t *testing.T) { diff --git a/internal/svg/svg.go b/internal/svg/svg.go index 6ba8b64c2d..131f5faac0 100644 --- a/internal/svg/svg.go +++ b/internal/svg/svg.go @@ -212,6 +212,7 @@ type objGroup struct { Ellipses []*ellipseObj `xml:"ellipse"` Rects []*rectObj `xml:"rect"` Polygons []*polygonObj `xml:"polygon"` + Groups []*objGroup `xml:"g"` } func replacePathsFill(paths []*pathObj, hexColor string, opacity string) { @@ -266,6 +267,7 @@ func replaceGroupObjectFill(groups []*objGroup, hexColor string, opacity string) replacePathsFill(grp.Paths, hexColor, opacity) replaceRectsFill(grp.Rects, hexColor, opacity) replacePolygonsFill(grp.Polygons, hexColor, opacity) + replaceGroupObjectFill(grp.Groups, hexColor, opacity) } } diff --git a/internal/svg/testdata/colorized/group_rects.png b/internal/svg/testdata/colorized/group_rects.png index f1137e1df4..a2abd02fcc 100644 Binary files a/internal/svg/testdata/colorized/group_rects.png and b/internal/svg/testdata/colorized/group_rects.png differ diff --git a/internal/svg/testdata/info_GroupRects.svg b/internal/svg/testdata/info_GroupRects.svg index 15a290a814..8b037ae3c9 100644 --- a/internal/svg/testdata/info_GroupRects.svg +++ b/internal/svg/testdata/info_GroupRects.svg @@ -5,7 +5,9 @@ - + + + diff --git a/test/theme.go b/test/theme.go index 138207e984..d7ac6b81c4 100644 --- a/test/theme.go +++ b/test/theme.go @@ -31,6 +31,7 @@ func Theme() fyne.Theme { theme.ColorNameForeground: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, theme.ColorNameHover: color.NRGBA{R: 0x88, G: 0xff, B: 0xff, A: 0x22}, theme.ColorNameHeaderBackground: color.NRGBA{R: 0x22, G: 0x22, B: 0x22, A: 0xff}, + theme.ColorNameHyperlink: color.NRGBA{R: 0xff, G: 0xcc, B: 0x80, A: 0xff}, theme.ColorNameInputBackground: color.NRGBA{R: 0x66, G: 0x66, B: 0x66, A: 0xff}, theme.ColorNameInputBorder: color.NRGBA{R: 0x86, G: 0x86, B: 0x86, A: 0xff}, theme.ColorNameMenuBackground: color.NRGBA{R: 0x56, G: 0x56, B: 0x56, A: 0xff}, diff --git a/theme/json.go b/theme/json.go index 3712574bbb..a0bbf81b67 100644 --- a/theme/json.go +++ b/theme/json.go @@ -39,7 +39,7 @@ type hexColor string func (h hexColor) color() (color.Color, error) { data := h - switch len(h) { + switch len([]rune(h)) { case 8, 6: case 9, 7: // remove # prefix data = h[1:] diff --git a/theme/json_test.go b/theme/json_test.go index 59c676be5d..1b071008bb 100644 --- a/theme/json_test.go +++ b/theme/json_test.go @@ -28,6 +28,10 @@ func TestFromJSON(t *testing.T) { assert.Equal(t, float32(5), th.Size(SizeNameInlineIcon)) assert.Equal(t, "NotoMono-Regular.ttf", th.Font(fyne.TextStyle{Monospace: true}).Name()) assert.Equal(t, "cancel_Paths.svg", th.Icon(IconNameCancel).Name()) + + th, _ = FromJSON("{\"Colors\":{\"foreground\":\"\xb1\"}}") + c := th.Color(ColorNameForeground, VariantLight) + assert.NotNil(t, c) } func TestFromTOML_Resource(t *testing.T) { diff --git a/widget/gridwrap.go b/widget/gridwrap.go index 666e153e46..8a782edd5e 100644 --- a/widget/gridwrap.go +++ b/widget/gridwrap.go @@ -528,13 +528,16 @@ func (l *gridWrapLayout) setupGridItem(li *gridWrapItem, id GridWrapItemID, focu f(id, li.child) } li.onTapped = func() { - l.list.RefreshItem(l.list.currentFocus) - canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list) - if canvas != nil { - canvas.Focus(l.list) + if !fyne.CurrentDevice().IsMobile() { + l.list.RefreshItem(l.list.currentFocus) + canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list) + if canvas != nil { + canvas.Focus(l.list) + } + + l.list.currentFocus = id } - l.list.currentFocus = id l.list.Select(id) } } diff --git a/widget/list.go b/widget/list.go index 1183d2b923..0aeb686c31 100644 --- a/widget/list.go +++ b/widget/list.go @@ -611,12 +611,15 @@ func (l *listLayout) setupListItem(li *listItem, id ListItemID, focus bool) { f(id, li.child) } li.onTapped = func() { - canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list) - if canvas != nil { - canvas.Focus(l.list) + if !fyne.CurrentDevice().IsMobile() { + canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list) + if canvas != nil { + canvas.Focus(l.list) + } + + l.list.currentFocus = id } - l.list.currentFocus = id l.list.Select(id) } } diff --git a/widget/richtext.go b/widget/richtext.go index f358d6f5b8..9ef858d79e 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -1017,8 +1017,8 @@ func lineBounds(seg *TextSegment, wrap fyne.TextWrap, trunc fyne.TextTruncation, } default: if trunc == fyne.TextTruncateEllipsis { - txt := seg.Text[low:high] - end, full := truncateLimit(txt, seg.Visual().(*canvas.Text), int(measureWidth), []rune{'…'}) + txt := []rune(seg.Text)[low:high] + end, full := truncateLimit(string(txt), seg.Visual().(*canvas.Text), int(measureWidth), []rune{'…'}) high = low + end bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, !full}) reuse++ diff --git a/widget/richtext_test.go b/widget/richtext_test.go index 00c5ef4074..136ae13411 100644 --- a/widget/richtext_test.go +++ b/widget/richtext_test.go @@ -969,6 +969,16 @@ func TestText_lineBounds(t *testing.T) { }, ellipses: 1, }, + { + name: "Multi_byte_ellipsis_not_truncated", + text: "🪃 234", + trunc: fyne.TextTruncateEllipsis, + wrap: fyne.TextWrapOff, + want: [][2]int{ + {0, 5}, + }, + ellipses: 0, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/widget/table.go b/widget/table.go index d0b5c141e7..41b8c15e99 100644 --- a/widget/table.go +++ b/widget/table.go @@ -40,7 +40,7 @@ type TableCellID struct { type Table struct { BaseWidget - Length func() (int, int) `json:"-"` + Length func() (rows int, cols int) `json:"-"` CreateCell func() fyne.CanvasObject `json:"-"` UpdateCell func(id TableCellID, template fyne.CanvasObject) `json:"-"` OnSelected func(id TableCellID) `json:"-"` @@ -104,7 +104,7 @@ type Table struct { // passed template CanvasObject. // // Since: 1.4 -func NewTable(length func() (int, int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table { +func NewTable(length func() (rows int, cols int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table { t := &Table{Length: length, CreateCell: create, UpdateCell: update} t.ExtendBaseWidget(t) return t @@ -117,7 +117,7 @@ func NewTable(length func() (int, int), create func() fyne.CanvasObject, update // The row and column headers will stick to the leading and top edges of the table and contain "1-10" and "A-Z" formatted labels. // // Since: 2.4 -func NewTableWithHeaders(length func() (int, int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table { +func NewTableWithHeaders(length func() (rows int, cols int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table { t := NewTable(length, create, update) t.ShowHeaderRow = true t.ShowHeaderColumn = true @@ -549,22 +549,24 @@ func (t *Table) Tapped(e *fyne.PointEvent) { } col := t.columnAt(e.Position) - if col == -1 { + if col == noCellMatch { return // out of col range } row := t.rowAt(e.Position) - if row == -1 { + if row == noCellMatch { return // out of row range } t.Select(TableCellID{row, col}) - t.RefreshItem(t.currentFocus) - canvas := fyne.CurrentApp().Driver().CanvasForObject(t) - if canvas != nil { - canvas.Focus(t) + if !fyne.CurrentDevice().IsMobile() { + t.RefreshItem(t.currentFocus) + canvas := fyne.CurrentApp().Driver().CanvasForObject(t) + if canvas != nil { + canvas.Focus(t) + } + t.currentFocus = TableCellID{row, col} + t.RefreshItem(t.currentFocus) } - t.currentFocus = TableCellID{row, col} - t.RefreshItem(t.currentFocus) } // columnAt returns a positive integer (or 0) for the column that is found at the `pos` X position. diff --git a/widget/table_test.go b/widget/table_test.go index eed19396c1..8ed6eb5cae 100644 --- a/widget/table_test.go +++ b/widget/table_test.go @@ -646,6 +646,36 @@ func TestTable_Selection(t *testing.T) { assert.Equal(t, 1, selectedRow) } +func TestTable_Selection_OnHeader(t *testing.T) { + test.NewApp() + defer test.NewApp() + + table := NewTableWithHeaders( + func() (int, int) { return 5, 5 }, + func() fyne.CanvasObject { + return NewLabel("placeholder") + }, + func(id TableCellID, c fyne.CanvasObject) { + text := fmt.Sprintf("Cell %d, %d", id.Row, id.Col) + c.(*Label).SetText(text) + }) + assert.Nil(t, table.selectedCell) + + w := test.NewWindow(table) + defer w.Close() + w.Resize(fyne.NewSize(180, 180)) + + selected := false + table.OnSelected = func(TableCellID) { + selected = true + } + test.TapCanvas(w.Canvas(), fyne.NewPos(35, 5)) + assert.False(t, selected) + + test.TapCanvas(w.Canvas(), fyne.NewPos(5, 58)) + assert.False(t, selected) +} + func TestTable_Select(t *testing.T) { test.NewApp() defer test.NewApp() diff --git a/widget/tree.go b/widget/tree.go index 4660f588e8..bd71c2efb7 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -348,11 +348,17 @@ func (t *Tree) TypedKey(event *fyne.KeyEvent) { t.ScrollTo(t.currentFocus) t.RefreshItem(t.currentFocus) case fyne.KeyLeft: - t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) { - if id == t.currentFocus && p != "" { - t.currentFocus = p - } - }) + // If the current focus is on a branch which is open, just close it + if t.IsBranch(t.currentFocus) && t.IsBranchOpen(t.currentFocus) { + t.CloseBranch(t.currentFocus) + } else { + // Every other case should move the focus to the current parent node + t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) { + if id == t.currentFocus && p != "" { + t.currentFocus = p + } + }) + } t.RefreshItem(t.currentFocus) t.ScrollTo(t.currentFocus) @@ -877,12 +883,14 @@ func (n *treeNode) Tapped(*fyne.PointEvent) { } n.tree.Select(n.uid) - canvas := fyne.CurrentApp().Driver().CanvasForObject(n.tree) - if canvas != nil { - canvas.Focus(n.tree) + if !fyne.CurrentDevice().IsMobile() { + canvas := fyne.CurrentApp().Driver().CanvasForObject(n.tree) + if canvas != nil { + canvas.Focus(n.tree) + } + n.tree.currentFocus = n.uid + n.Refresh() } - n.tree.currentFocus = n.uid - n.Refresh() } func (n *treeNode) partialRefresh() { diff --git a/widget/tree_internal_test.go b/widget/tree_internal_test.go index 60555f9a53..2107b4c982 100644 --- a/widget/tree_internal_test.go +++ b/widget/tree_internal_test.go @@ -194,6 +194,127 @@ func TestTree_Focus(t *testing.T) { assert.Equal(t, "foo", tree.selected[0]) } +func TestTree_Keyboard(t *testing.T) { + // Prepare data for a tree like this: + // item_1 + // |- item_1_1 + // |- item_1_1_1 + // |- item_1_1_2 + // |- item_1_2 + // |- item_1_2_1 + // |- item_1_2_2 + // item_2 + // |- item_2_1 + // |- item_2_2 + var treeData = map[string][]string{ + "": {"item_1", "item_2"}, + "item_1": {"item_1_1", "item_1_2"}, + "item_2": {"item_2_1", "item_2_2"}, + "item_1_1": {"item_1_1_1", "item_1_1_2"}, + "item_1_2": {"item_1_2_1", "item_1_2_2"}, + } + tree := NewTreeWithStrings(treeData) + window := test.NewWindow(tree) + defer window.Close() + window.Resize(tree.MinSize().Max(fyne.NewSize(250, 400))) + + canvas := window.Canvas().(test.WindowlessCanvas) + assert.Nil(t, canvas.Focused()) + + // Start with a fully collapsed tree + tree.CloseAllBranches() + + // Select the first node + canvas.FocusNext() + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), false) + + // Open the node "item_1" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyRight}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1_1", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), false) + + // Go to next node "item1_2" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyDown}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1_2", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), false) + + // Open the node "item_1_2" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyRight}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1_2_1", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), true) + + // Go to next node "item_1_2_2" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyDown}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1_2_2", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), true) + + // Press left on the non-branch node "item_1_2_2" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyLeft}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1_2", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), true) + + // Press left on the open branch node "item_1_2" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyLeft}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1_2", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), false) + + // Press left on the closed branch node "item_1_2" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyLeft}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), true) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), false) + + // Press left on the open branch node "item_1" + tree.TypedKey(&fyne.KeyEvent{Name: fyne.KeyLeft}) + // Validate the state + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, "item_1", canvas.Focused().(*Tree).currentFocus) + assert.Equal(t, tree.IsBranchOpen("item_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_2"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_1"), false) + assert.Equal(t, tree.IsBranchOpen("item_1_2"), false) +} + func TestTree_Indentation(t *testing.T) { data := make(map[string][]string) tree := NewTreeWithStrings(data)