-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPanel.qml
More file actions
438 lines (381 loc) · 13.1 KB
/
Copy pathPanel.qml
File metadata and controls
438 lines (381 loc) · 13.1 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
import QtQuick
import Quickshell
import qs.Commons
import qs.Ui
import "PlanovaModel.js" as Model
import "components"
// The Planova popup: a month calendar whose days carry todo/event
// indicators, a detail view of the selected day (events, tasks, notes),
// quick add, refill, and a notes browser - all reading and writing
// Planova's markdown files through the Store.
//
// BarWidget.qml owns the bar label and hands this panel the button to
// anchor against plus the shared Store.
Panel {
id: root
moduleName: "fr.rvier.planova"
ipcTarget: "fr.rvier.planova"
manageIpc: false
property var anchorItem: null
property var store: null
// The bar tracks the widget mounted in its slot - BarWidget.qml - not this
// nested panel, so everything the bar identifies a panel by must be that
// widget.
property var hostWidget: null
readonly property var barIdentity: hostWidget || root
// ---- View state.
property date today: new Date()
readonly property string todayKey: Model.keyForDate(today)
property string selectedKey: todayKey
property int viewYear: today.getFullYear()
property int viewMonth: today.getMonth()
// tab: which surface is up. mode: what the calendar tab is showing.
property string tab: "calendar" // "calendar" | "notes"
property string mode: "main" // "main" | "quickadd" | "refill"
// Keyboard focus inside the day detail list (j/k over tasks).
property bool detailFocus: false
property int detailIndex: 0
readonly property var selectedSummary: {
if (!store) return null
store.revision
return store.summaryFor(selectedKey)
}
readonly property var selectedTasks: selectedSummary ? selectedSummary.tasks : []
readonly property var weeks: Model.monthGrid(viewYear, viewMonth, todayKey, selectedKey)
readonly property var selectedDate: Model.dateForKey(selectedKey) || today
readonly property bool viewingCurrentMonth: viewYear === today.getFullYear() && viewMonth === today.getMonth()
// Guarded so the widget renders before the bar is injected.
readonly property color contentForeground: bar ? bar.foreground : Color.foreground
readonly property string contentFontFamily: bar ? bar.fontFamily : Style.font.family
function open() {
refresh()
root.controller.show()
Qt.callLater(function() {
if (root.opened) setCenterHoverRevealSuppressed(true)
})
}
function close() {
setCenterHoverRevealSuppressed(false)
root.mode = "main"
root.detailFocus = false
confirm.opened = false
root.controller.hide()
}
function toggle() {
if (root.opened) root.close()
else root.open()
}
function switchPanel(direction) {
if (root.bar && typeof root.bar.switchPanelFrom === "function")
return root.bar.switchPanelFrom(root.barIdentity, direction)
return false
}
function setCenterHoverRevealSuppressed(value) {
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
root.bar.centerHoverRevealSuppressed = value
}
function refresh() {
root.today = new Date()
goToToday()
if (store) store.rescan()
}
function goToToday() {
root.viewYear = today.getFullYear()
root.viewMonth = today.getMonth()
root.selectedKey = todayKey
root.detailFocus = false
}
function selectKey(key) {
root.selectedKey = String(key)
var parts = Model.parseKey(key)
if (parts) {
root.viewYear = parts.year
root.viewMonth = parts.month
}
root.detailFocus = false
}
function moveSelection(deltaDays) {
selectKey(Model.stepDay(selectedKey, deltaDays))
}
function moveMonth(delta) {
var next = Model.stepMonth(viewYear, viewMonth, delta)
root.viewYear = next.year
root.viewMonth = next.month
// Keep the selection inside the visible month so keyboard nav never
// strands the cursor off-screen.
var parts = Model.parseKey(selectedKey)
if (!parts || parts.year !== next.year || parts.month !== next.month) {
var day = parts ? Math.min(parts.day, 28) : 1
root.selectedKey = Model.keyFor(next.year, next.month, day)
}
}
function moveYear(delta) {
moveMonth(delta * 12)
}
function refocus() {
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
function startQuickAdd(type) {
root.mode = "quickadd"
quickAdd.begin(type || "todo")
}
function startRefill() {
if (!store) return
refillView.begin(store.collectRefill(selectedKey))
root.mode = "refill"
}
function leaveMode() {
root.mode = "main"
refocus()
}
function editSelectedDay() {
if (!store) return
store.openDaily(selectedKey)
root.close()
}
function toggleTaskAtCursor() {
if (!store) return
var tasks = selectedTasks
if (detailIndex < 0 || detailIndex >= tasks.length) return
store.toggleTask(selectedKey, tasks[detailIndex].lineIndex)
}
// ---- Keyboard routing. One place decides what every semantic key means
// for the surface currently up; text fields block the catcher and
// handle Esc/Enter themselves.
function handleMove(dx, dy) {
if (confirm.opened) {
if (dx !== 0) confirm.selectedIndex = confirm.selectedIndex === 0 ? 1 : 0
return
}
if (tab === "notes") { notesTab.moveCursor(dy); return }
if (mode === "refill") { refillView.moveCursor(dy); return }
if (mode !== "main") return
if (detailFocus) {
if (dy !== 0) {
var count = selectedTasks.length
if (count > 0) detailIndex = Math.max(0, Math.min(count - 1, detailIndex + dy))
}
return
}
if (dx !== 0) moveSelection(dx)
if (dy !== 0) moveSelection(dy * 7)
}
// Return commits the refill; Space toggles rows. PanelKeyCatcher fires
// both returnRequested and activateRequested on Enter, so the return
// handler eats the paired activate.
property bool _skipActivate: false
function handleReturn() {
if (confirm.opened) return
if (mode === "refill") {
refillView.commit()
_skipActivate = true
}
}
function handleActivate() {
if (_skipActivate) { _skipActivate = false; return }
if (confirm.opened) {
if (confirm.selectedIndex === 0) confirm.canceled()
else confirm.confirmed()
return
}
if (tab === "notes") { notesTab.activate(); return }
if (mode === "refill") { refillView.toggleAtCursor(); return }
if (mode !== "main") return
if (detailFocus) { toggleTaskAtCursor(); return }
if (selectedTasks.length > 0) {
root.detailIndex = 0
root.detailFocus = true
}
}
function handleClose() {
if (confirm.opened) { confirm.canceled(); return }
if (mode !== "main") { leaveMode(); return }
if (detailFocus) { root.detailFocus = false; return }
root.close()
}
function handleDelete() {
if (tab === "notes") { notesTab.requestDelete(); return }
if (tab === "calendar" && mode === "main" && detailFocus) toggleTaskAtCursor()
}
function handleTextKey(t) {
if (confirm.opened) return
if (t === "n") { root.tab = "notes"; return }
if (t === "c") { root.tab = "calendar"; return }
if (tab === "notes") {
if (t === "o") notesTab.startCreate()
else if (t === "/") notesTab.focusSearch()
else if (t === "e") notesTab.activate()
else if (t === "r" || t === "R") notesTab.startRename()
return
}
if (mode !== "main") return
if (t === "[") moveMonth(-1)
else if (t === "]") moveMonth(1)
else if (t === "{") moveYear(-1)
else if (t === "}") moveYear(1)
else if (t === "t" || t === "T") goToToday()
else if (t === "a") startQuickAdd("todo")
else if (t === "e" || t === "E") startQuickAdd("event")
else if (t === "g" || t === "G" || t === "L") startQuickAdd("log")
else if (t === "r") startRefill()
else if (t === "o" || t === "O") editSelectedDay()
}
SystemClock {
id: clock
precision: SystemClock.Minutes
onDateChanged: {
if (Model.keyForDate(clock.date) === String(root.todayKey)) return
var followToday = root.selectedKey === root.todayKey
root.today = clock.date
if (followToday) root.goToToday()
}
}
KeyboardPanel {
id: panel
anchorItem: root.anchorItem
owner: root.barIdentity
bar: root.bar
open: root.opened
centerOnBar: true
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(560))
contentHeight: panel.fittedContentHeight(contentColumn.implicitHeight, Style.space(820))
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
blocked: quickAdd.editing || notesTab.editing
onMoveRequested: function(dx, dy) { root.handleMove(dx, dy) }
onReturnRequested: root.handleReturn()
onActivateRequested: root.handleActivate()
onCloseRequested: root.handleClose()
onDeleteRequested: root.handleDelete()
onTabRequested: function(direction) { root.switchPanel(direction) }
onTextKey: function(t) { root.handleTextKey(t) }
Flickable {
id: panelScroll
anchors.fill: parent
contentWidth: contentColumn.width
contentHeight: contentColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
interactive: contentHeight > height
Column {
id: contentColumn
width: panelScroll.width
spacing: Style.space(10)
// ---- Header: the selected day as the hero, tab switch on the
// right. Clicking the hero goes home to today.
Item {
width: parent.width
height: heroRow.height
Row {
id: heroRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.space(16)
Text {
anchors.baseline: heroDate.baseline
text: ""
color: heroMouse.containsMouse
? Style.hoverStateColor(root.contentForeground, Color.accent)
: root.contentForeground
font.family: root.contentFontFamily
font.pixelSize: 34
}
Text {
id: heroDate
anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDate(root.selectedDate, "dddd, MMMM d")
color: heroMouse.containsMouse
? Style.hoverStateColor(root.contentForeground, Color.accent)
: root.contentForeground
font.family: root.contentFontFamily
font.pixelSize: 30
font.bold: true
}
}
MouseArea {
id: heroMouse
x: heroRow.x
y: heroRow.y
width: heroRow.width
height: heroRow.height
enabled: root.selectedKey !== root.todayKey
hoverEnabled: enabled
cursorShape: Qt.PointingHandCursor
onClicked: root.goToToday()
PanelToolTip {
visible: heroMouse.containsMouse
text: "Back to today"
fontFamily: root.contentFontFamily
}
}
PanelActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
iconText: root.tab === "calendar" ? "" : ""
tooltipText: root.tab === "calendar" ? "Notes (n)" : "Calendar (c)"
foreground: root.contentForeground
fontFamily: root.contentFontFamily
onClicked: root.tab = root.tab === "calendar" ? "notes" : "calendar"
}
}
PanelSeparator {
width: parent.width
foreground: root.contentForeground
}
CalendarTab {
id: calendarTab
visible: root.tab === "calendar" && root.mode === "main"
width: parent.width
panel: root
}
QuickAddForm {
id: quickAdd
visible: root.tab === "calendar" && root.mode === "quickadd"
width: parent.width
panel: root
}
RefillView {
id: refillView
visible: root.tab === "calendar" && root.mode === "refill"
width: parent.width
panel: root
}
NotesTab {
id: notesTab
visible: root.tab === "notes"
width: parent.width
panel: root
}
}
}
ConfirmDialog {
id: confirm
anchors.fill: parent
fontFamily: root.contentFontFamily
property var onConfirm: null
function ask(message, action) {
confirm.message = message
confirm.onConfirm = action
confirm.selectedIndex = 1
confirm.opened = true
}
onCanceled: {
confirm.opened = false
root.refocus()
}
onConfirmed: {
confirm.opened = false
if (confirm.onConfirm) confirm.onConfirm()
confirm.onConfirm = null
root.refocus()
}
}
}
}
// Exposed so components can raise confirmations without reaching into the
// key catcher's children.
function askConfirm(message, action) {
confirm.ask(message, action)
}
}