-
Notifications
You must be signed in to change notification settings - Fork 23
/
window.go
394 lines (339 loc) · 11.9 KB
/
window.go
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
package ui
import (
"context"
"errors"
"os"
"os/signal"
"runtime"
"syscall"
giouiApp "gioui.org/app"
"gioui.org/gesture"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"golang.org/x/text/language"
"golang.org/x/text/message"
"github.com/crypto-power/cryptopower/app"
libutils "github.com/crypto-power/cryptopower/libwallet/utils"
"github.com/crypto-power/cryptopower/ui/assets"
"github.com/crypto-power/cryptopower/ui/cryptomaterial"
"github.com/crypto-power/cryptopower/ui/load"
"github.com/crypto-power/cryptopower/ui/notification"
"github.com/crypto-power/cryptopower/ui/page"
"github.com/crypto-power/cryptopower/ui/values"
)
// Window represents the app window (and UI in general). There should only be one.
// Window maintains an internal state of variables to determine what to display at
// any point in time.
type Window struct {
*giouiApp.Window
navigator app.WindowNavigator
ctx context.Context
ctxCancel context.CancelFunc
load *load.Load
// Quit channel used to trigger background process to begin implementing the
// shutdown protocol.
Quit chan struct{}
// IsShutdown channel is used to report that background processes have
// completed shutting down, therefore the UI processes can finally stop.
IsShutdown chan struct{}
// clicker used to create click events for application
clicker gesture.Click
}
type (
C = layout.Context
D = layout.Dimensions
)
type WriteClipboard struct {
Text string
}
// CreateWindow creates and initializes a new window with start
// as the first page displayed.
// Should never be called more than once as it calls
// app.NewWindow() which does not support being called more
// than once.
func CreateWindow(appInfo *load.AppInfo) (*Window, error) {
appTitle := giouiApp.Title(values.String(values.StrAppName))
// appSize overwrites gioui's default app size of 'Size(800, 600)'
appSize := giouiApp.Size(values.AppWidth, values.AppHeight)
// appMinSize is the minimum size the app.
appMinSize := giouiApp.MinSize(values.MobileAppWidth, values.MobileAppHeight)
// Display network on the app title if its not on mainnet.
if net := appInfo.AssetsManager.NetType(); net != libutils.Mainnet {
appTitle = giouiApp.Title(values.StringF(values.StrAppTitle, net.Display()))
}
ctx, cancel := context.WithCancel(context.Background())
giouiWindow := giouiApp.NewWindow(appSize, appMinSize, appTitle)
win := &Window{
ctx: ctx,
ctxCancel: cancel,
Window: giouiWindow,
navigator: app.NewSimpleWindowNavigator(giouiWindow.Invalidate),
Quit: make(chan struct{}, 1),
IsShutdown: make(chan struct{}, 1),
}
l, err := win.NewLoad(appInfo)
if err != nil {
return nil, err
}
win.load = l
startPage := page.NewStartPage(win.ctx, win.load)
win.load.AppInfo.ReadyForDisplay(win.Window, startPage)
return win, nil
}
func (win *Window) NewLoad(appInfo *load.AppInfo) (*load.Load, error) {
th := cryptomaterial.NewTheme(assets.FontCollection(), assets.DecredIcons, false)
if th == nil {
return nil, errors.New("unexpected error while loading theme")
}
// fetch status of the wallet if its online.
go libutils.IsOnline()
// Set the user-configured theme colors on app load.
var isDarkModeOn bool
if appInfo.AssetsManager.LoadedWalletsCount() > 0 {
// A valid DB interface must have been set. Otherwise no valid wallet exists.
isDarkModeOn = appInfo.AssetsManager.IsDarkModeOn()
}
th.SwitchDarkMode(isDarkModeOn, assets.DecredIcons)
l := &load.Load{
AppInfo: appInfo,
Theme: th,
// NB: Toasts implementation is maintained here for the cases where its
// very essential to have a toast UI component implementation otherwise
// restraints should be exercised when planning to reuse it else where.
Toast: notification.NewToast(th),
Printer: message.NewPrinter(language.English),
}
// DarkModeSettingChanged checks if any page or any
// modal implements the AppSettingsChangeHandler
l.DarkModeSettingChanged = func(isDarkModeOn bool) {
if page, ok := win.navigator.CurrentPage().(load.AppSettingsChangeHandler); ok {
page.OnDarkModeChanged(isDarkModeOn)
}
if modal := win.navigator.TopModal(); modal != nil {
if modal, ok := modal.(load.AppSettingsChangeHandler); ok {
modal.OnDarkModeChanged(isDarkModeOn)
}
}
}
l.LanguageSettingChanged = func() {
if page, ok := win.navigator.CurrentPage().(load.AppSettingsChangeHandler); ok {
page.OnLanguageChanged()
}
}
l.CurrencySettingChanged = func() {
if page, ok := win.navigator.CurrentPage().(load.AppSettingsChangeHandler); ok {
page.OnCurrencyChanged()
}
}
return l, nil
}
// HandleEvents runs main event handling and page rendering loop.
func (win *Window) HandleEvents() {
done := make(chan os.Signal, 1)
if runtime.GOOS == "windows" {
// For controlled shutdown to work on windows, the channel has to be
// listening to all signals.
// https://github.com/golang/go/commit/8cfa01943a7f43493543efba81996221bb0f27f8
signal.Notify(done)
} else {
// Signals are primarily used on Unix-like systems.
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
}
var isShuttingDown bool
displayShutdownPage := func() {
if isShuttingDown {
return
}
isShuttingDown = true
log.Info("...Initiating the app shutdown protocols...")
// clear all stack and display the shutdown page as backend processes are
// terminating.
win.navigator.ClearStackAndDisplay(page.NewStartPage(win.ctx, win.load, true))
win.ctxCancel()
// Trigger the backend processes shutdown.
win.Quit <- struct{}{}
}
for {
// Select either the os interrupt or the window event, whichever becomes
// ready first.
select {
case <-done:
displayShutdownPage()
case <-win.IsShutdown:
// backend processes shutdown is complete, exit UI process too.
return
case e := <-win.Events():
switch evt := e.(type) {
case system.DestroyEvent:
displayShutdownPage()
case system.FrameEvent:
ops := win.handleFrameEvent(evt)
evt.Frame(ops)
default:
log.Tracef("Unhandled window event %v\n", e)
}
}
}
}
// handleFrameEvent is called when a FrameEvent is received by the active
// window. It expects a new frame in the form of a list of operations that
// describes what to display and how to handle input. This operations list
// is returned to the caller for displaying on screen.
func (win *Window) handleFrameEvent(evt system.FrameEvent) *op.Ops {
win.load.SetCurrentAppWidth(evt.Size.X, evt.Metric)
switch {
case win.navigator.CurrentPage() == nil:
// Prepare to display the StartPage if no page is currently displayed.
win.navigator.Display(win.load.StartPage())
default:
// The app window may have received some user interaction such as key
// presses, a button click, etc which triggered this FrameEvent. Handle
// such interactions before re-displaying the UI components. This
// ensures that the proper interface is displayed to the user based on
// the action(s) they just performed.
win.handleRelevantKeyPresses(evt)
win.navigator.CurrentPage().HandleUserInteractions()
if modal := win.navigator.TopModal(); modal != nil {
modal.Handle()
}
}
// Generate an operations list with instructions for drawing the window's UI
// components onto the screen. Use the generated ops to request key events.
ops := win.prepareToDisplayUI(evt)
win.addKeyEventRequestsToOps(ops)
return ops
}
// handleRelevantKeyPresses checks if any open modal or the current page is a
// load.KeyEventHandler AND if the provided system.FrameEvent contains key press
// events for the modal or page.
func (win *Window) handleRelevantKeyPresses(evt system.FrameEvent) {
handleKeyPressFor := func(tag string, maybeHandler interface{}) {
handler, ok := maybeHandler.(load.KeyEventHandler)
if !ok {
return
}
for _, event := range evt.Queue.Events(tag) {
if keyEvent, isKeyEvent := event.(key.Event); isKeyEvent && keyEvent.State == key.Press {
handler.HandleKeyPress(&keyEvent)
}
}
}
// Handle key events on the top modal first, if there's one.
// Only handle key events on the current page if no modal is displayed.
if modal := win.navigator.TopModal(); modal != nil {
handleKeyPressFor(modal.ID(), modal)
} else {
handleKeyPressFor(win.navigator.CurrentPageID(), win.navigator.CurrentPage())
}
}
// prepareToDisplayUI creates an operation list and writes the layout of all the
// window UI components into it. The created ops is returned and may be used to
// record further operations before finally being rendered on screen via
// system.FrameEvent.Frame(ops).
func (win *Window) prepareToDisplayUI(evt system.FrameEvent) *op.Ops {
backgroundWidget := layout.Expanded(func(gtx C) D {
return win.load.Theme.DropdownBackdrop.Layout(gtx, func(gtx C) D {
return cryptomaterial.Fill(gtx, win.load.Theme.Color.Gray4)
})
})
currentPageWidget := layout.Stacked(func(gtx C) D {
if modal := win.navigator.TopModal(); modal != nil {
gtx = gtx.Disabled()
}
if win.navigator.CurrentPage() == nil {
win.navigator.Display(page.NewStartPage(win.ctx, win.load))
}
return win.load.Theme.DropdownBackdrop.Layout(gtx, func(gtx C) D {
return win.navigator.CurrentPage().Layout(gtx)
})
})
topModalLayout := layout.Stacked(func(gtx C) D {
modal := win.navigator.TopModal()
if modal == nil {
return layout.Dimensions{}
}
return modal.Layout(gtx)
})
// Use a StackLayout to write the above UI components into an operations
// list via a graphical context that is linked to the ops.
ops := &op.Ops{}
gtx := layout.NewContext(ops, evt)
win.addEvents(gtx)
layout.Stack{Alignment: layout.N}.Layout(
gtx,
backgroundWidget,
currentPageWidget,
topModalLayout,
layout.Stacked(win.load.Toast.Layout),
)
win.handleEvents(gtx)
return ops
}
// addKeyEventRequestsToOps checks if the current page or any modal has
// registered to be notified of certain key events and updates the provided
// operations list with instructions to generate a FrameEvent if any of the
// desired keys is pressed on the window.
func (win *Window) addKeyEventRequestsToOps(ops *op.Ops) {
requestKeyEvents := func(tag string, desiredKeys key.Set) {
if desiredKeys == "" {
return
}
// Execute the key.InputOP{}.Add operation after all other operations.
// This is particularly important because some pages call op.Defer to
// signify that some operations should be executed after all other
// operations, which has an undesirable effect of discarding this key
// operation unless it's done last, after all other defers are done.
m := op.Record(ops)
key.InputOp{Tag: tag, Keys: desiredKeys}.Add(ops)
op.Defer(ops, m.Stop())
}
// Request key events on the top modal, if necessary.
// Only request key events on the current page if no modal is displayed.
if modal := win.navigator.TopModal(); modal != nil {
if handler, ok := modal.(load.KeyEventHandler); ok {
requestKeyEvents(modal.ID(), handler.KeysToHandle())
}
} else {
if handler, ok := win.navigator.CurrentPage().(load.KeyEventHandler); ok {
requestKeyEvents(win.navigator.CurrentPageID(), handler.KeysToHandle())
}
}
}
func (win *Window) handleEvents(gtx C) {
win.handleUserClick(gtx)
win.handleShortKeys(gtx)
}
// handleUserClick listen touch action of user for mobile.
func (win *Window) handleUserClick(gtx C) {
for _, evt := range win.clicker.Events(gtx) {
if evt.Type == gesture.TypePress && evt.Source == pointer.Touch {
win.load.Theme.AutoHideSoftKeyBoard(gtx)
}
}
}
// handleShortKeys listen keys pressed.
func (win *Window) handleShortKeys(gtx C) {
// check for presses of the back key.
for _, event := range gtx.Events(win) {
switch event := event.(type) {
case key.Event:
if event.Name == key.NameBack && event.State == key.Press {
win.load.Theme.OnTapBack()
}
}
}
}
func (win *Window) addEvents(gtx C) {
if win.load.IsMobileView() {
win.clicker.Add(gtx.Ops)
}
if runtime.GOOS == "android" {
key.InputOp{
Tag: win,
Keys: key.NameBack,
}.Add(gtx.Ops)
}
}