Repository navigation
Expand file tree
/
Copy pathWorkspace.qml
More file actions
3074 lines (2809 loc) · 102 KB
/
Copy pathWorkspace.qml
File metadata and controls
3074 lines (2809 loc) · 102 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
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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Shared application state and UI composition. Launchers supply windows
// and platform services; providers own note storage and backend behavior.
import QtQuick
import "design"
import "design/controls"
import "services/platform"
import "services/processes"
import "ui"
import "ui/TabColors.js" as TabColors
import "services/shortcuts" as Shortcuts
import "services/clipboard" as Clipboard
import "services/markdown" as Markdown
import "services/microsoft" as Microsoft
import "services/requests" as Requests
import "services/notes" as NoteServices
import "services/notes/sidebar.js" as Sidebar
import "services/notes/ordering.js" as Ordering
import "services/providers" as ProviderServices
import "services/files" as Files
import "services/settings" as SettingsServices
import "services/extensions" as Extensions
import "services/themes" as Themes
import "services/commands" as Commands
import "ui/commands" as CommandUi
import QtQuick.Window
// Providers implement the same contract in both hosts. Built-in providers
// live in plugin packages; legacy external directories use a compatibility adapter.
Item {
id: root
property bool supportsOverlay: false
property Item contentHost: root
// A close under way: the UI is already closed, and the host exits once
// every accepted write has answered (finishClose). Opening ends it.
property bool closing: false
property bool initialized: false
readonly property string statePath: Platform.stateDir + "/note-note.json"
readonly property alias configPath: settingsStore.path
property bool opened: false
onOpenedChanged: {
if (!opened) {
commandPalette.requestClose()
}
}
property bool detached: !supportsOverlay
// The sidebar width the user dragged the splitter to, in pixels, kept
// across runs. 0 means they never did, and the default width stands.
property real listWidth: 0
// The sidebar folded away behind the view bar's toggle, kept across runs.
property bool listCollapsed: false
property bool deleteConfirmOpen: false
// Which page stands in for the workspace, by name — "" while the notes
// themselves are on screen. A name rather than a flag each, so two pages
// cannot both believe they are the one being looked at.
property string page: ""
readonly property bool pageOpen: root.page !== ""
property string filterText: ""
property string statusText: ""
// Who said it: a message that refreshes itself (the rate-limit countdown)
// replaces only its own text, never another's.
property string statusKey: ""
// The status bar's provider badge follows the active notebook.
property string sourceName: "Note Note"
property url sourceLogo: ""
property color sourceBase: "transparent"
readonly property color sourceInk: sourceBase.a > 0 ? Qt.tint(foreground, Util.alpha(sourceBase, TabColors.inkAlpha())) : foreground
// Current note. `loadingNote` guards against editor change signals firing
// a save while a note is being swapped in.
property alias currentPath: session.currentPath
property alias loadingNote: session.loadingNote
// The open note's load ended in an error and the pane is showing nothing.
// Retried on the next open() — a queued read is dropped when the window
// hides, so this is the ordinary way a hidden window ends a load.
property alias loadFailed: session.loadFailed
property alias dirty: session.dirty
readonly property string currentCrumb: root.revision >= 0 ? crumbOf(root.currentPath) : ""
property alias loadingPath: session.loadingPath
// Shares the [menu] surface tokens, so a theme that styles the launcher
// styles this too.
property color background: Color.menu.background
property color foreground: Color.menu.text
property color borderColor: Color.menu.border
// The overlay card paints its rounded background and border under the
// content, and clips nothing: chrome that sits flush in a corner would
// square it off. So the flush pieces round their own corners to nest
// inside the border's arc — the card's radius minus the border they are
// inset by. Zero when detached: there the window is square and the
// compositor does the rounding.
readonly property real chromeRadius: detached ? 0 : Math.max(0, Style.cornerRadius - Math.max(1, Style.space(2)))
property color scrim: Color.menu.scrim
property color accent: Color.accent
property color selectedBackground: Color.menu.selectedBackground
property color selectedText: Color.menu.selectedText
// The document and chrome share a bundled sans-serif family in four faces.
readonly property string noteFont: nimbusSans.name
readonly property string interfaceFont: nimbusSans.name
// The type scale, hung off the shell's base size so it follows the
// theme. Three steps: the note's text stands a step above the chrome
// around it (sidebar, tabs), and the view bar's captions a step below,
// so the eye lands on the note first.
readonly property int noteFontSize: Math.round(Style.font.baseSize * 1.25)
readonly property int chromeFontSize: Style.font.subtitle
readonly property int captionFontSize: Style.font.bodySmall
// ── services & providers ────────────────────────────────────────────
// Providers get their own Microsoft sign-in (own registration, own token,
// own scopes) from the shared service code:
// services.microsoft.create(providerId, scopes, clientId).
property var accounts: []
// Links use the theme accent blended with its text color for readable ink.
readonly property string linkColour: Color.token("editor.link").toString()
// A quote keeps the theme's own ink at reduced strength; the editor draws
// the classic bar beside it and the rounded slab behind a code block
// (NoteEditor, block decorations). The document's own code background is
// only the dialect's marker for "this block is code", so it is passed
// fully transparent — presence is all anything reads back, and Qt Quick
// paints even a faint one unevenly (first block only, measured on 6.11).
readonly property string quoteInkColour: Color.token("editor.quoteForeground").toString()
readonly property string codeBackgroundColour: "transparent"
// Native display styling paints the inline-code chip. A script-only host
// cannot, so there the chip's colour is written into the document; such a
// host keeps the System theme, so that colour is the system's.
readonly property string codeChipColour: editor.canColorText
? "transparent" : Color.token("editor.inlineCodeBackground").toString()
readonly property var services: ({
"platform": Platform,
"style": Style,
"colors": Color,
"processes": {
"create": function(owner) {
return processRunnerComponent.createObject(owner)
}
},
"microsoft": {
"create": function(owner, scopes, clientId) {
return root.createMicrosoftAccount(owner, scopes, clientId)
}
},
"requests": {
"queueFor": function(key, provider) {
return root.queueFor(key, provider)
},
"cancelOwner": function(owner) {
root.cancelQueuedFor(owner)
}
}
})
// ── request queues ──────────────────────────────────────────────────
// One lane per rate key, made on demand and owned by the host rather than
// by the provider that asked for it. That is deliberate: a provider is
// destroyed and rebuilt whenever its settings change, and a backend's
// cooldown has to outlive that — the service is throttling the account, not
// the QML object. See services/requests/RequestQueue.qml.
property var queues: ({})
property var queueList: []
property var queueNames: ({})
readonly property bool anyCooling: root.coolingQueue() !== null
property var providers: []
property var providerState: ({})
property bool providersLoaded: false
// id -> Provider.qml url, built-ins and externals alike, populated once by
// loadProviders() regardless of enabled state — so re-enabling a provider
// later never needs a re-scan.
property var providerUrls: ({})
property bool stateReady: false
// ── settings (~/.config/notenote/config.json) ────────────────────────
// Read alongside the state, not chained after it — a different concern, a
// different directory. The plugin catalog is read once the settings say
// which packages are enabled, and loadProviders() waits for all three, or
// a disabled provider would flash on screen before disappearing.
property alias config: settingsStore.config
// The config file is still to be written for the first time: it did not
// exist when read, and it is written once the providers have loaded.
property alias configUnwritten: settingsStore.unwritten
property alias configReady: settingsStore.ready
readonly property alias settings: settingsStore
readonly property alias catalog: pluginCatalog
// ── sidebar rows ────────────────────────────────────────────────────
property var rows: []
property var pendingReorders: ({})
property var footerActions: []
property int revision: 0
// Assignments to `rows`, each of which destroys and rebuilds every delegate.
// Read beside `revision` (rebuildRows calls) it says how many rebuilds
// produced a list that was already on screen — which should be most of
// them (docs/testing.md).
property int rowWrites: 0
// ── what a tab opens with ───────────────────────────────────────────
// A tab opens on the note you last had open in it. That is a fact about
// where the user was rather than about any backend — OneNote's API has no
// opinion about which page this app showed last — so the host keeps it,
// once, for every provider including the ones not written yet.
//
// A provider with a better answer says so with `defaultNote(sectionKey)`
// and is believed instead. OneNote keeps its own, per *notebook*, because
// its notebookTabs setting turns one tab holding every notebook into a tab
// each and back: keys the host has never seen, about notebooks the user has
// been reading all along.
property var lastNotes: ({}) // section key -> the note last open there
// True while the tab on screen is still owed its opening note: set at
// startup and on every switch, cleared as soon as it is given one or the
// user picks something themselves. It stays set while a provider is still
// listing — OneNote answers a second or two after the window is up — so the
// note is opened when its row arrives rather than lost to the race, and it
// costs nothing left set for a note that never comes back.
property bool defaultOwed: true
// The tab the sidebar is open at, persisted verbatim. It is deliberately not
// resolved against the live sections here: a provider that has not listed yet
// (OneNote is async) must not cost the user their tab.
property string activeSection: ""
property var tabs: []
// Per-tab search hit counts, keyed by tab key — beside `tabs`, not inside
// them: the counts move on every keystroke, and a Repeater over a plain
// array rebuilds every delegate when its model changes, so the rail's tabs
// stay a stable model and only these numbers change under them.
property var tabMatches: ({})
property bool switchingTab: false
// ── content search ────────────────────────────────────────────────────
// Titles and previews are matched right here, on every keystroke — that is
// matchesQuery, and it is instant because the rows are already in memory.
// Note *bodies* are not: they live with the providers, so once the typing
// pauses, every provider that offers the optional `search(query, cb)` is
// asked, and the paths it answers with join the same match set the moment
// they arrive. Hits are kept per provider — a slow OneNote answer must not
// wipe the local hits already showing — and every answer names the query
// generation it was asked for, so a reply to text no longer in the field
// changes nothing.
property var contentHits: ({}) // provider id -> { path: true }
// Providers asked and not yet answered, provider id -> true. Replaced
// wholesale, never mutated, like contentHits — reassignment is what lets
// searchBusy recompute.
property var searchWaiting: ({})
property int searchSeq: 0
property int searchRequestSeq: 0
property var searchRequests: ({})
property var invalidSearchProviders: ({})
// Whether note bodies are searched at all. When false the providers are
// simply never asked — the decision is the host's alone, made by calling
// or not calling. True until a setting owns it.
readonly property bool searchContent: true
// Is a content answer still owed? True from the keystroke on: the debounce
// window counts — the ask is coming, just not sent yet — and then each
// provider's searchWaiting entry until its reply lands. Derived, never set:
// the search panel reads it to say "searching…" instead of a premature
// "No match".
readonly property bool searchBusy: root.filterText.length >= 2 && root.searchContent && (contentSearchTimer.running || Object.keys(root.searchWaiting).length > 0)
// The list's keyboard cursor. Usually it is the open note; Ctrl+up/down
// stepping onto a section row (kind "tree") parks it there instead — the
// note stays open in the editor, only the highlight travels. Any real
// selection pulls the cursor back to the note (selectPath).
property string treeCursor: ""
property string deletePath: ""
// A row's menu item waiting on its confirmation: { row, item }.
property var rowActionPending: null
// A name being asked for (`prompt`, `rename`): { message, placeholder,
// text, confirmText, run(name) }, the field opening on `text`.
property var promptPending: null
property string missedSaveNotice: ""
// ── state ───────────────────────────────────────────────────────────
// Asked for by anyone whose state moved — a provider through
// persistRequested, the host itself — and written once per turn of the
// event loop, however many asked: one selection can move the host's memory
// and a provider's in the same breath, and that is one write, not two.
property bool stateWriteDue: false
readonly property int maxStateBytes: 1024 * 1024
// Every accepted write has settled: the note's saves, the file store's,
// the state file's, each lane's, and each provider's own — the moment a
// close may finish and a settings change may commit. Bound to the state
// it reads, so those follow the last write rather than poll for it. A
// provider without `writeBusy` (an older external one) is waited on by
// its `busy`, which covers reads too (PROVIDERS.md).
readonly property bool writesSettled: !session.busy && !files.busy && !settingsStore.busy && !root.stateWriteDue
&& !root.providers.some(function(provider) {
return provider.writeBusy === true || (provider.writeBusy === undefined && provider.busy === true)
})
&& !root.queueList.some(function(queue) {
return queue.writeDepth > 0
})
// The close follows the settling — once the notification has completed:
// the host's answer to readyToClose retires the providers this state
// reads, and a reaction that writes its own inputs mid-update is a
// binding loop.
onWritesSettledChanged: Qt.callLater(root.finishClose)
signal dismissRequested()
signal readyToClose()
// ── shell contract ──────────────────────────────────────────────────
function open(payloadJson) {
root.opened = true
// A close still writing in the background ends here: the workspace is
// wanted again, so the process stays.
root.closing = false
root.deleteConfirmOpen = false
root.rowActionPending = null
root.promptPending = null
root.page = ""
root.pauseQueues(false)
// A save that failed while nobody was looking is reported now, once.
if (root.missedSaveNotice) {
root.showStatus(root.missedSaveNotice)
root.missedSaveNotice = ""
}
// A note whose load never landed — the window closed over it, or the
// backend was busy — would otherwise sit blank and read-only until the
// user picked something else and came back. Ask again.
if (root.currentPath && root.loadFailed) {
root.reloadCurrent()
}
// Not while a sign-in is under way: entering its device code means
// switching to a browser, which can hide and reopen this overlay — that
// must not wipe the very code the user is about to type in.
if (!root.accounts.some(function(a) {
return a.loggingIn
})) {
editor.clearNotice()
}
for (var a = 0; a < root.accounts.length; a++) {
root.accounts[a].refresh()
}
for (var i = 0; i < root.providers.length; i++) {
root.providers[i].refresh()
if (typeof root.providers[i].watch === "function") {
root.providers[i].watch(true)
}
}
Qt.callLater(function() {
editor.focusEditor()
})
}
function stopWatching() {
for (var i = 0; i < root.providers.length; i++) {
if (typeof root.providers[i].watch === "function") {
root.providers[i].watch(false)
}
}
}
function close() {
root.flushSave()
root.opened = false
stopWatching()
root.pauseQueues(true)
}
// A close is final. The UI closes at once (`opened`); the process stays
// until every conversion and accepted write has answered, and then the
// host exits (readyToClose), whatever they answered: a write that fails
// meanwhile is let go, unannounced. Only opening the workspace again, on
// another launch, ends the close (open).
function requestClose() {
commandPalette.requestClose()
if (root.closing) {
return
}
root.flushSave()
root.opened = false
session.cancelLoad()
root.stopWatching()
root.pauseQueues(true)
root.saveState()
// Last, once the flush and the state write are under way: the close
// finishes the moment they settle, which may be now.
root.closing = true
root.finishClose()
}
function finishClose() {
if (!root.closing || !root.writesSettled || lifecycle.busy) {
return
}
root.closing = false
root.readyToClose()
}
function dismiss() {
root.flushSave()
root.opened = false
stopWatching()
root.pauseQueues(true)
root.dismissRequested()
}
function releaseProviders() {
// Dispose dependents while the workspace and its request lanes still
// have live QML contexts. Their read processes stop with their owners.
var owned = root.providers.slice()
for (var i = 0; i < owned.length; i++) {
root.retireProvider(owned[i])
}
}
function toggle() {
if (root.opened) {
root.dismiss()
} else {
root.open("{}")
}
}
function setDetached(value) {
if (!root.supportsOverlay) {
return
}
var next = value === true || value === "true"
if (next === root.detached) {
return
}
commandPalette.requestClose()
root.detached = next
saveState()
root.showStatus(next ? "Detached — an ordinary window now, so move, resize and tile it as usual" : "Back to the summoned overlay")
}
// A page stands in for the workspace, so it is not dismissed the way a
// dialog is: what closes it is asking to see notes again — its own ✕,
// Escape, or a notebook tab in the bar above it. Opening one while another
// is up simply swaps them; there is only ever the one.
function openPage(name) {
root.page = name
}
function closePage() {
if (!root.pageOpen) {
return
}
root.page = ""
// The editor is only just visible again; let the frame that reveals it
// finish before handing it the keyboard.
Qt.callLater(function() {
editor.focusEditor()
})
}
// The page on screen, or null for the notes. One place to ask, so another
// page is a row in the menu and a line here rather than a flag threaded
// through the layout.
function currentPage() {
if (root.page === "settings") {
return settingsPage
}
if (root.page === "keys") {
return keysPage
}
return null
}
function goBack() {
if (titleBar.searchFocused) {
if (root.filterText.length > 0) {
root.clearSearch()
} else if (!root.detached) {
root.dismiss()
}
return
}
if (root.detached) {
titleBar.focusSearch()
} else {
root.dismiss()
}
}
function showStatus(text, key) {
root.statusText = text
root.statusKey = text ? (key || "") : ""
if (text) {
statusTimer.restart()
}
}
function copyText(text) {
Platform.copyText(text)
}
function createMicrosoftAccount(owner, scopes, clientId) {
// A provider recreated on a settings change asks for its account again
// the one its old instance left behind would otherwise stay in the list,
// costing a status process on every open for nobody.
root.accounts = root.accounts.filter(function(a) {
if (a.owner !== owner) {
return true
}
a.destroy()
return false
})
var acc = accountComponent.createObject(root, {
"owner": owner,
"clientId": clientId || "",
"scopes": ["offline_access", "User.Read"].concat(scopes || []).join(" ")
})
acc.codeReceived.connect(function(code, uri) {
editor.showNotice("Sign in to Microsoft for " + owner, "Open " + uri + " in a browser, enter this code, and sign in with your Microsoft account. This screen updates by itself once you are done.", code, [{
"label": "Copy code",
"icon": "",
"action": function() {
root.copyText(code)
}
}, {
"label": "Copy link",
"icon": "",
"action": function() {
root.copyText(uri)
}
}, {
"label": "Open sign-in page",
"icon": "",
"action": function() {
Platform.openUrl(uri)
}
}])
})
// The "signed in" notice clears itself once the account reports in —
// and only itself, by the serial the editor handed back.
var signedInNotice = 0
acc.loginSucceeded.connect(function() {
signedInNotice = editor.showNotice("Signed in", "Fetching your notes…", "", [])
})
acc.loginFailed.connect(function(error) {
editor.showNotice("Sign-in failed", error, "", [{
"label": "Try again",
"icon": "",
"action": function() {
acc.login()
}
}])
})
acc.signOutFailed.connect(function(error) {
editor.showNotice("Sign-out failed", "You are still signed in to Microsoft for " + owner + ": " + error, "", [{
"label": "Try again",
"icon": "",
"action": function() {
acc.logout()
}
}])
})
acc.updated.connect(function() {
if (acc.signedIn && signedInNotice) {
editor.clearNotice(signedInNotice)
signedInNotice = 0
}
root.rebuildRows()
})
root.accounts = root.accounts.concat([acc])
// An account knows its own status from birth, not from the next open():
// a provider made while the window is already open (a settings change)
// would otherwise sit at "not signed in" until the overlay is summoned
// again, since open() is the only other thing that asks.
acc.refresh()
return acc
}
function queueFor(key, provider) {
if (provider && provider.name) {
root.queueNames[key] = provider.name
}
if (root.queues[key]) {
return root.queues[key]
}
var q = queueComponent.createObject(root, {
"domain": key,
"paused": !root.opened
})
if (!q) {
console.warn("note-note: could not create a request queue for", key)
return null
}
root.queues[key] = q
root.queueList = root.queueList.concat([q])
return q
}
function cancelQueuedFor(owner) {
for (var i = 0; i < root.queueList.length; i++) {
root.queueList[i].cancelOwner(owner)
}
}
// Hidden: ordinary reads and polls stop. Writes keep draining, and explicit
// background work such as the search index can continue in the same lane.
function pauseQueues(paused) {
for (var i = 0; i < root.queueList.length; i++) {
root.queueList[i].paused = paused
}
}
// Of the parked lanes, the one with the longest still to wait — or null.
// Each lane's `cooling` and `cooldownRemaining` are properties, so a
// binding over this follows them.
function coolingQueue() {
var worst = null
for (var i = 0; i < root.queueList.length; i++) {
var q = root.queueList[i]
if (q.cooling && (worst === null || q.cooldownRemaining > worst.cooldownRemaining)) {
worst = q
}
}
return worst
}
// A note's path is its provider's id, a colon, and the rest (PROVIDERS.md).
function providerOf(path) {
return path ? providerById(path.substring(0, path.indexOf(":"))) : null
}
function providerById(id) {
for (var i = 0; i < root.providers.length; i++) {
if (root.providers[i].id === id) {
return root.providers[i]
}
}
return null
}
// The provider of the open tab. Section keys start with the provider's id.
function activeProvider() {
var k = activeKey()
return k ? providerOfKey(k) : null
}
// A provider's entry in config.providers is the host's file, but its keys
// are the provider's own settings: the ones it declares in `settings`,
// each a property whose initial value is the default (PROVIDERS.md).
// Every declared setting the entry holds is assigned right after
// creation, verbatim. `enabled` is never assigned (whether the instance
// exists is what it means), and a key the provider does not declare is
// not its business — so a hand-edited config cannot break a provider,
// only miss it.
function applyProviderSettings(p) {
var entry = (root.config.providers || {})[p.id] || {}
var keys = Array.isArray(p.settings) ? p.settings : []
for (var i = 0; i < keys.length; i++) {
var k = keys[i]
if (!(k in entry)) {
continue
}
try {
p[k] = entry[k]
} catch (e) {
console.warn("note-note: provider", p.id, "setting", k, "was not taken:", e.message)
}
}
}
// The defaults each provider declared, read off a fresh instance before
// its settings were applied: the one place a default is written is the
// provider's own property, and the config file shows it from here.
property var providerDefaults: ({})
function recordProviderDefaults(p) {
var keys = Array.isArray(p.settings) ? p.settings : [], defaults = {}
for (var i = 0; i < keys.length; i++) {
defaults[keys[i]] = p[keys[i]]
}
var all = Object.assign({}, root.providerDefaults)
all[p.id] = defaults
root.providerDefaults = all
}
// The config with this provider's entry filled in from its defaults, so
// the file shows every setting that applies; what the entry holds wins.
function withProviderDefaults(cfg, id) {
var providers = Object.assign({}, cfg.providers || {})
providers[id] = Object.assign({ "enabled": true }, root.providerDefaults[id] || {}, providers[id] || {})
return Object.assign({}, cfg, { "providers": providers })
}
// A provider is what it was registered as: the instance carries the
// identity its package declared, and that identity is loaded once.
function providerRefusal(provider, id) {
if (!provider) {
return "could not be created"
}
if (provider.id !== id) {
return "calls itself \"" + provider.id + "\""
}
return root.providerById(id) ? "is loaded already" : ""
}
function addProvider(id) {
var url = root.providerUrls[id]
var comp = Qt.createComponent(url)
if (comp.status === Component.Error) {
console.warn("note-note: provider failed:", url, comp.errorString())
return null
}
var p = comp.createObject(root, {
"host": root,
"services": root.services
})
var refusal = root.providerRefusal(p, id)
if (refusal) {
console.warn("note-note: provider", id, refusal + ":", url)
if (p) {
root.cancelQueuedFor(p)
p.destroy()
}
return null
}
root.recordProviderDefaults(p)
root.config = root.withProviderDefaults(root.config, p.id)
root.applyProviderSettings(p)
p.updated.connect(function() {
root.rebuildRows()
})
if (p.searchChanged) {
p.searchChanged.connect(function() {
root.invalidateContentSearch(p)
})
}
p.statusRequested.connect(function(t) {
root.showStatus(t)
})
p.noticeRequested.connect(function(title, text, code, actions) {
editor.showNotice(title, text, code, actions)
})
p.noticeCleared.connect(function() {
editor.clearNotice()
})
p.viewRequested.connect(function(title, component, props) {
editor.showView(title, component, props)
})
p.viewCleared.connect(function() {
editor.clearNotice()
})
p.persistRequested.connect(function() {
root.saveState()
})
// The provider asking for the write it was told about. Answered for the
// open note only, since the text to be written is the editor's — and it
// costs nothing when the note has already gone: a switch and a close
// flush it, a delete drops its edits with it, so flushSave finds nothing
// dirty either way. That is what lets a provider's schedule fire late
// without the host having to reach back and cancel it.
if (p.saveRequested) {
p.saveRequested.connect(function(path) {
if (session.resolvedPath(path) === root.currentPath) {
root.flushSave()
}
})
}
if (p.noteChanged) {
p.noteChanged.connect(function(path) {
path = session.resolvedPath(path)
if (path === root.currentPath && !root.dirty && !root.saveInFlight(path) && !root.loadingNote) {
root.reloadCurrent()
}
})
}
if (p.notePathChanged) {
p.notePathChanged.connect(function(previous, next, view) {
session.replacePath(previous, next, view)
root.replaceRememberedNote(previous, next)
if (root.deletePath === previous) {
root.deletePath = next
}
root.invalidateContentSearch(p)
})
}
if (p.noteDeletionFinished) {
p.noteDeletionFinished.connect(function(path, result) {
session.finishDeletion(path, result)
})
}
if (root.providerState[p.id]) {
p.restoreState(root.providerState[p.id])
}
root.providers = root.providers.concat([p])
return p
}
// Manifest descriptors name provider identities before activation. The
// catalog has already rejected ambiguous IDs; disabled providers are never
// instantiated merely to discover their metadata.
// Tabs follow root.providers' order (eachSection walks it start to end).
// An id named in config.providers keeps that key's position — JSON.parse
// preserves the order string keys were written in — so reordering the
// config reorders the tabs; an id absent from config.providers (enabled by
// default, order never asked for) just keeps its natural discovery order,
// appended after every id the user did name.
function orderProviderIds(ids, cfg) {
var known = (cfg && cfg.providers) || {}, order = Object.keys(known), rank = {}
for (var i = 0; i < order.length; i++) {
rank[order[i]] = i
}
var ranked = [], rest = []
for (var j = 0; j < ids.length; j++) {
(rank.hasOwnProperty(ids[j]) ? ranked : rest).push(ids[j])
}
ranked.sort(function(a, b) {
return rank[a] - rank[b]
})
return ranked.concat(rest)
}
function loadProviders(entries) {
var urls = {}
for (var e = 0; e < entries.length; e++) {
urls[entries[e].id] = entries[e].url
}
root.providerUrls = urls
var ids = root.orderProviderIds(entries.map(function(x) {
return x.id
}), root.config)
for (var u = 0; u < ids.length; u++) {
if (root.providerEnabledIn(root.config, ids[u])) {
root.addProvider(ids[u])
}
}
root.providersLoaded = true
if (root.configUnwritten) {
root.writeFirstConfig()
}
if (root.opened) {
root.open("{}")
}
}
function maybeLoadProviders() {
if (!pluginCatalog.ready || !root.configReady || !root.stateReady || root.providersLoaded) {
return
}
root.loadProviders(pluginCatalog.providers)
}
function providerEnabledIn(cfg, id) {
var p = cfg && cfg.providers, e = p && p[id]
return !(e && e.enabled === false); // absent or malformed => enabled
}
// The defaults are written once the providers have recorded theirs, so the
// file shows every known setting from the moment it exists.
function writeFirstConfig() {
settingsStore.create(JSON.stringify(root.config), function(result) {
if (result.error) {
root.reportSave(result.error)
}
})
}
// `revision` is the one the edited text was read at; a file that moved on
// since is not overwritten.
function applySettingsJson(text, callback, revision) {
lifecycle.apply(text, callback, revision)
}
function retireProvider(provider) {
root.cancelQueuedFor(provider)
// Its writes die with it, so their callbacks never come: a group left
// pending would never settle, and its order would stay projected over
// the rows of the provider that takes its place.
Object.keys(root.pendingReorders).forEach(function(key) {
if (root.pendingReorders[key].provider === provider) {
root.setPendingReorder(key, null)
}
})
root.providers = root.providers.filter(function(p) {
return p !== provider
})
var waiting = Object.assign({}, root.searchWaiting)
delete waiting[provider.id]
root.searchWaiting = waiting
delete root.invalidSearchProviders[provider.id]
provider.destroy()
}
function reorderProviders() {
var byId = {}
for (var i = 0; i < root.providers.length; i++) {
byId[root.providers[i].id] = root.providers[i]
}
var ids = root.orderProviderIds(Object.keys(byId), root.config)
root.providers = ids.map(function(id) {
return byId[id]
})
}
// The one place `rows` is written, so the count means what it says.
function setRows(out) {
root.rows = out
root.rowWrites++
}
// `request` is { group, ids, provider }; null settles the group's entry.
function setPendingReorder(key, request) {
var pending = Object.assign({}, root.pendingReorders)
if (request) {
pending[key] = request
} else {
delete pending[key]
}
root.pendingReorders = pending
}
// A drag's result, handed to the group's provider to persist. Until it
// answers, the order is a pending projection over the provider's rows
// (projectReorders), and every publication of it goes through
// rebuildRows, which is what keeps the list's scroll position. A drag
// while an order is still saving is taken the same way: its order is the
// one on screen from then on, and the provider is handed it as well,
// to save after the other.
function reorderItems(group, ids) {
var provider = group ? root.providerById(group.provider) : null
if (!provider || root.filterText || root.closing) {
return
}
if (!Ordering.apply(root.rows, group, ids) || JSON.stringify(Ordering.ids(root.rows, group)) === JSON.stringify(ids)) {
return
}
var modern = typeof provider.reorder === "function"
if (!modern && (!provider.canReorder || typeof provider.setOrder !== "function")) {
return
}
var key = Ordering.key(group)
var request = { group: group, ids: ids.slice(), provider: provider }
root.setPendingReorder(key, request)
root.rebuildRows()
var finished = false
function done(result) {
if (finished) {
return
}
finished = true
// Retiring the provider settles its requests (retireProvider), and a
// newer request may hold the group by now: neither is this one's.
// The newer order is the one that is saved last, so its answer, not
// this one's, says how the group ends — a failure here included.
if (root.pendingReorders[key] !== request) {
return
}
root.setPendingReorder(key, null)
root.rebuildRows()
if (result && result.error) {
root.showStatus(provider.name + ": " + result.error)
}
}
try {
if (modern) {
provider.reorder(group.scope, ids, done)
} else {
// Compatibility for external providers implementing the 1.x hook.
// It has no acknowledgement: its projection stays on screen until
// the provider's next update, matching the previous host behavior.
provider.setOrder(group.scope, ids)
root.setPendingReorder(key, null)
}
} catch (error) {
done({ error: "the order could not be saved" })
}
}
function projectReorders(rows) {
var out = rows
if (!root.filterText) {
Object.keys(root.pendingReorders).forEach(function(key) {
var pending = root.pendingReorders[key]
out = Ordering.apply(out, pending.group, pending.ids) || out
})
}
return out
}
// The user chose this note: it is what its tab opens with next time. Told
// to the note's own provider, and kept here for the tab it was chosen in —
// two different questions, and a search landing in another tab is where
// they come apart. A provider that answers `defaultNote` itself keeps its
// own memory and is not remembered here as well: the entry would never be
// read. Entries for providers no longer loaded go with the next write.
function rememberOpened(path) {
var owner = providerOf(path)
if (owner && typeof owner.noteOpened === "function") {
owner.noteOpened(path)
}
if (owner && typeof owner.defaultNote === "function") {
return
}
var key = activeKey()
if (!key || root.lastNotes[key] === path) {
return
}