-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.go
More file actions
195 lines (168 loc) · 7.82 KB
/
Copy pathbuild.go
File metadata and controls
195 lines (168 loc) · 7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Application assembly and wiring: buildViewer composes app-owned
// components with the registered feature widgets, builds the root overlay
// stack, and installs the window's input handlers.
package ui
import (
"fmt"
"image"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"github.com/frathe/picfetch/internal/decodepool"
"github.com/frathe/picfetch/internal/filesort"
"github.com/frathe/picfetch/internal/imaging"
"github.com/frathe/picfetch/internal/preferences"
"github.com/frathe/picfetch/internal/ui/autoupdate"
"github.com/frathe/picfetch/internal/ui/infoview"
)
// buildViewer wires up every widget, the drop handler, and the key handler
// for a fresh window from inputs already loaded by loadStartupState. Tests
// call the same assembly path, so e2e coverage cannot drift from Run.
func buildViewer(application fyne.App, startup startupState) (*viewer, fyne.Window) {
window := application.NewWindow(appTitle)
savedSession := startup.savedSession
prefs := startup.prefs
// Declared ahead of the constructors below so their tap/click callbacks
// can close over it: a callback only ever runs on a later interaction,
// by which point view is assigned, but it needs to be referenceable
// before the viewer itself can be constructed (which in turn needs the
// widgets built below).
var view *viewer
img := canvas.NewImageFromImage(nil)
img.FillMode = canvas.ImageFillContain
img.ScaleMode = canvas.ImageScaleSmooth
img.Hide()
dz := newDropzoneUI(
func() { view.openFileDialog() },
func() { view.restoreSession() },
)
scan := newScanUI()
sortUIC := newSortUI()
toastComp := newToast(func() { view.ForceRepaint() })
info := infoview.New(func() {
view.exif.Show()
// Same reason showWindowExif syncs by hand: the EXIF window
// fires an observer on close, none on open.
view.syncMenus()
})
loadingBar := widget.NewProgressBarInfinite()
loadingBar.Hide()
view = &viewer{
app: application,
win: window,
quit: application.Quit,
img: img,
hint: dz.hint,
dropzone: dz.root,
dropzoneArt: dz.art,
welcomeArt: dz.welcomeArt,
emptyStateArt: dz.emptyStateArt,
restoreLink: dz.restoreLink,
savedSession: savedSession,
loadingBar: loadingBar,
toast: toastComp,
info: info,
state: newAppState(filesort.FromPref(prefs.SortMode), prefs.MergeMode),
baseTitle: appTitle,
imgCache: imaging.NewImgCache(int64(prefs.MaxImageCacheMB) * bytesPerMB),
preloads: decodepool.New[string, struct{}](preloadConcurrency),
settings: settings{
maxScan: prefs.MaxScanFiles,
maxWinW: prefs.MaxWindowWidth,
maxWinH: prefs.MaxWindowHeight,
imgCacheMB: prefs.MaxImageCacheMB,
},
wallpaperDir: defaultWallpaperDir(),
updater: autoupdate.New(application, autoupdate.DefaultDir(), func(day string) {
preferences.SaveLastUpdateCheckDay(application, day)
}),
keyModifiers: defaultKeyModifiers,
stopWinPosPoll: noPollerStop,
}
view.vector.debounce = defaultVectorDebounce
view.vector.rasterize = func(vec *imaging.Vector, w, h int) (image.Image, error) { return vec.RasterAt(w, h) }
view.vector.after = time.After
view.vector.do = fyne.Do
view.frameAfter = time.After
// Wired here rather than in the literal above: the closure captures view,
// whose state field that literal is still building. Any mutator that
// goes through appState.removeFile evicts the removed file's decode
// without having to remember to - see onRemove's field comment (state.go).
view.state.onRemove = func(u fyne.URI) { view.imgCache.Remove(u.String()) }
view.scanOp.art = scan.art
view.scanOp.spinner = scan.spinner
view.scanOp.label = scan.label
view.sortOp.spinner = sortUIC.spinner
view.sortOp.label = sortUIC.label
// Seeded here for the same reason imgCache's budget is: buildViewer
// applies the saved preference directly rather than through the
// setter. Every later change goes through SetMaxImageCacheMB, which
// keeps the two in step.
imaging.SetMaxVectorRasterPixels(vectorRasterPixelsFor(prefs.MaxImageCacheMB))
if n := len(savedSession); n > 0 {
dz.restoreLink.SetText(fmt.Sprintf(lang.L("Restore last session (%d images)"), n))
dz.restoreLink.Show()
}
registerFeatures(view, application, window, prefs)
// The bar lives in its own overlay layer on top of the stack, pinned to
// the top edge by the VBox layout, so showing/hiding it never resizes
// or shifts the image underneath. VBoxLayout sizes each child to its
// own MinSize height, so loadingBar is wrapped to force that height to
// loadingBarHeight regardless of the widget's natural (themed) size.
overlay := container.New(layout.NewVBoxLayout(), container.New(fixedHeightLayout{height: loadingBarHeight}, loadingBar))
scanContainer := container.NewCenter(container.NewVBox(scan.art, scan.spinner, scan.label))
sortContainer := container.NewCenter(container.NewVBox(sortUIC.spinner, sortUIC.label))
// Pinned to the bottom edge, mirroring how loadingBar is pinned to the
// top: a leading spacer eats all the slack space in the VBox, leaving
// the card its natural size at the bottom.
toastOverlay := container.New(layout.NewVBoxLayout(), layout.NewSpacer(), container.NewCenter(toastComp.card))
// Pinned to the top-left corner: an HBox with a trailing spacer keeps
// the info card at its natural (unstretched) width instead of HBox's
// default of filling the row, and nesting that inside a VBox (with
// nothing below it) keeps it pinned to the top instead of vertically
// centered.
infoOverlay := container.New(layout.NewVBoxLayout(), container.NewHBox(info.Object(), layout.NewSpacer()))
// Order is paint order, back to front, and the tail of it is load-bearing.
// The grid's backdrop is opaque and fills the window, so anything stacked
// below it is simply invisible while it is open - which is fine for the
// image view underneath, and wrong for the things that now have to
// appear *over* an open grid: the batch delete confirmation and the
// export-format prompt (both share widgets.ChoiceCard, whose own scrim
// is translucent, so the grid dims through it rather than being hidden
// by it) and the toast that reports what the batch did.
window.SetContent(container.New(windowSizeTracker(view, window),
view.zoom.Widget(), dz.root, scanContainer, sortContainer, overlay, infoOverlay,
view.grid.Overlay(), view.deletion.Overlay(), view.exportPrompt.Overlay(), toastOverlay))
window.SetMainMenu(buildMainMenu(view))
// Fyne's Darwin driver inserts our Window menu next to GLFW's system
// Window menu. Folding them must wait until setupNativeMenu has run.
// fyne.Do is not enough here: before Run, Do from the main goroutine
// runs inline, while SetMainMenu still queues the native rebuild until
// the window view exists. Run folds immediately after Show, when that
// queue has drained. refreshMainMenu repeats the fold after later
// Refresh rebuilds.
fyne.Do(func() { syncNativeMenuBar(view.win.MainMenu()) })
window.SetOnDropped(func(_ fyne.Position, uris []fyne.URI) {
view.handleDrop(uris)
})
// F1 opens the manual, Escape clears back to the initial state (or quits
// if it's already there), the arrow keys (plus Home/End) walk through
// the dropped files, S toggles sort order, and M toggles merge mode.
// See viewer.handleKeyEvent for the dispatch.
window.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) {
view.handleKeyEvent(ev)
})
// Typed characters, as opposed to key names: the grid's filename search
// is the one feature that needs the actual character - case included,
// and punctuation a fyne.KeyEvent has no name for. See
// viewer.handleTypedRune.
window.Canvas().SetOnTypedRune(func(r rune) {
view.handleTypedRune(r)
})
wireGlobalShortcuts(window.Canvas(), view)
return view, window
}