forked from kiwanami/emacs-calfw
-
Notifications
You must be signed in to change notification settings - Fork 0
/
calfw.el
2702 lines (2364 loc) · 102 KB
/
calfw.el
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
;;; calfw.el --- Calendar view framework on Emacs
;; Copyright (C) 2011 SAKURAI Masashi
;; Author: SAKURAI Masashi <m.sakurai at kiwanami.net>
;; Keywords: calendar
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This program is a framework for the Calendar component. In the
;; Emacs, uses can show schedules in the calendar views, like iCal,
;; Outlook and Google Calendar.
;;; Installation:
;; Place this program in your load path and add following code.
;; (require 'calfw)
;;; Usage:
;; Executing the command `cfw:open-calendar-buffer', switch to the calendar buffer.
;; You can navigate the date like calendar.el.
;; Schedule data which are shown in the calendar view, are collected
;; by the `cfw:source' objects. See the function `cfw:open-debug-calendar' for example.
;; This program gets the holidays using the function
;; `calendar-holiday-list'. See the document of the holidays.el and
;; the Info text for customizing the holidays.
;;; Add-ons:
;; Following programs are also useful:
;; - calfw-howm.el : Display howm schedules.
;; - calfw-ical.el : Display schedules of the iCalendar format.
;; - calfw-org.el : Display orgmode schedules.
;;; Code:
(eval-when-compile (require 'cl))
(require 'calendar)
(require 'holidays)
;;; Constants
(defconst cfw:week-sunday 0)
(defconst cfw:week-monday 1)
(defconst cfw:week-tuesday 2)
(defconst cfw:week-wednesday 3)
(defconst cfw:week-thursday 4)
(defconst cfw:week-friday 5)
(defconst cfw:week-saturday 6)
(defconst cfw:week-days 7)
;;; Customs
(defcustom cfw:fchar-vertical-line ?|
"The character used for drawing vertical lines."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-horizontal-line ?-
"The character used for drawing horizontal lines."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-junction ?+
"The character used for drawing junction lines."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-top-right-corner ?+
"The character used for drawing the top-right corner."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-top-left-corner ?+
"The character used for drawing the top-left corner."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-left-junction ?+
"The character used for drawing junction lines at the left side."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-right-junction ?+
"The character used for drawing junction lines at the right side."
:group 'cfw
:type 'character)
(defcustom cfw:fchar-top-junction ?+
"The character used for drawing junction lines at the top side."
:group 'cfw
:type 'character)
(defcustom cfw:read-date-command 'cfw:read-date-command-simple
"The command used to read the date in `cfw:navi-goto-date-command',
for example `cfw:read-date-command-simple' or `cfw:org-read-date-command'."
:group 'cfw
:type 'function)
;;; Faces
(defface cfw:face-title
'((((class color) (background light))
:foreground "DarkGrey" :weight bold :height 2.0 :inherit variable-pitch)
(((class color) (background dark))
:foreground "darkgoldenrod3" :weight bold :height 2.0 :inherit variable-pitch)
(t :height 1.5 :weight bold :inherit variable-pitch))
"Face for title" :group 'calfw)
(defface cfw:face-header
'((((class color) (background light))
:foreground "Slategray4" :background "Gray90" :weight bold)
(((class color) (background dark))
:foreground "maroon2" :weight bold))
"Face for headers" :group 'calfw)
(defface cfw:face-sunday
'((((class color) (background light))
:foreground "red2" :background "#ffd5e5" :weight bold)
(((class color) (background dark))
:foreground "red" :weight bold))
"Face for Sunday" :group 'calfw)
(defface cfw:face-saturday
'((((class color) (background light))
:foreground "Blue" :background "#d4e5ff" :weight bold)
(((class color) (background light))
:foreground "Blue" :weight bold))
"Face for Saturday" :group 'calfw)
(defface cfw:face-holiday
'((((class color) (background light))
:background "#ffd5e5")
(((class color) (background dark))
:background "grey10" :foreground "purple" :weight bold))
"Face for holidays" :group 'calfw)
(defface cfw:face-grid
'((((class color) (background light))
:foreground "SlateBlue")
(((class color) (background dark))
:foreground "DarkGrey"))
"Face for grids"
:group 'calfw)
(defface cfw:face-default-content
'((((class color) (background light))
:foreground "#2952a3")
(((class color) (background dark))
:foreground "green2"))
"Face for default contents"
:group 'calfw)
(defface cfw:face-periods
'((((class color) (background light))
:background "#668cd9" :foreground "White" :slant italic)
(((class color) (background dark))
:foreground "cyan"))
"Face for period" :group 'calfw)
(defface cfw:face-day-title
'((((class color) (background light))
:background "#f8f9ff")
(((class color) (background dark))
:background "grey10"))
"Face for day title"
:group 'calfw)
(defface cfw:face-default-day
'((((class color) (background light))
:weight bold :inherit cfw:face-day-title)
(((class color) (background dark))
:weight bold :inherit cfw:face-day-title))
"Face for default day" :group 'calfw)
(defface cfw:face-annotation
'((((class color)) :foreground "RosyBrown" :inherit cfw:face-day-title))
"Face for annotations"
:group 'calfw)
(defface cfw:face-disable
'((((class color)) :foreground "DarkGray" :inherit cfw:face-day-title))
"Face for days out of focused period"
:group 'calfw)
(defface cfw:face-today-title
'((((class color) (background light))
:background "#fad163")
(((class color) (background dark))
:background "red4" :weight bold))
"Face for today" :group 'calfw)
(defface cfw:face-today
'((((class color) (background light))
:background "#fff7d7")
(((class color) (background dark))
:foreground "Cyan" :weight bold))
"Face for today" :group 'calfw)
(defface cfw:face-select
'((((class color) (background light))
:background "#c3c9f8")
(((class color) (background dark))
:background "Blue4"))
"Face for selection" :group 'calfw)
(defvar cfw:face-item-separator-color "SlateBlue"
"Color for the separator line of items in a day.")
;;; Utilities
(defun cfw:k (key alist)
"[internal] Get a content by key from the given alist."
(cdr (assq key alist)))
(defun cfw:rt (text face)
"[internal] Put a face to the given text."
(unless (stringp text) (setq text (format "%s" (or text ""))))
(put-text-property 0 (length text) 'face face text)
(put-text-property 0 (length text) 'font-lock-face face text)
text)
(defun cfw:tp (text prop value)
"[internal] Put a text property to the entire text string."
(if (< 0 (length text))
(put-text-property 0 (length text) prop value text))
text)
(defun cfw:extract-text-props (text &rest excludes)
"[internal] Return text properties."
(loop with ret = nil
with props = (text-properties-at 0 text)
for name = (car props)
for val = (cadr props)
while props
do
(when (and name (not (memq name excludes)))
(setq ret (cons name (cons val ret))))
(setq props (cddr props))
finally return ret))
(defun cfw:define-keymap (keymap-list)
"[internal] Key map definition utility.
KEYMAP-LIST is a source list like ((key . command) ... )."
(let ((map (make-sparse-keymap)))
(mapc
(lambda (i)
(define-key map
(if (stringp (car i))
(read-kbd-macro (car i)) (car i))
(cdr i)))
keymap-list)
map))
(defun cfw:trim (str)
"[internal] Trim the space char-actors."
(if (string-match "^[ \t\n\r]*\\(.*?\\)[ \t\n\r]*$" str)
(match-string 1 str)
str))
;;; Date Time Transformation
(defun cfw:date (month day year)
"Construct a date object in the calendar format."
(and month day year
(list month day year)))
(defun cfw:emacs-to-calendar (time)
"Transform an emacs time format to a calendar one."
(let ((dt (decode-time time)))
(list (nth 4 dt) (nth 3 dt) (nth 5 dt))))
(defun cfw:calendar-to-emacs (date)
"Transform a calendar time format to an emacs one."
(encode-time 0 0 0
(calendar-extract-day date)
(calendar-extract-month date)
(calendar-extract-year date)))
(defun cfw:month-year-equal-p (date1 date2)
"Return `t' if numbers of month and year of DATE1 is equals to
ones of DATE2. Otherwise is `nil'."
(and
(= (calendar-extract-month date1)
(calendar-extract-month date2))
(= (calendar-extract-year date1)
(calendar-extract-year date2))))
(defun cfw:date-less-equal-p (d1 d2)
"Return `t' if date value D1 is less than or equals to date value D2."
(let ((ed1 (cfw:calendar-to-emacs d1))
(ed2 (cfw:calendar-to-emacs d2)))
(or (equal ed1 ed2)
(time-less-p ed1 ed2))))
(defun cfw:date-between (begin end date)
"Return `t' if date value DATE exists between BEGIN and END."
(and (cfw:date-less-equal-p begin date)
(cfw:date-less-equal-p date end)))
(defun cfw:month-year-contain-p (month year date2)
"Return `t' if date value DATE2 is included in MONTH and YEAR."
(and
(= month (calendar-extract-month date2))
(= year (calendar-extract-year date2))))
(defun cfw:date-after (date num)
"Return the date after NUM days from DATE."
(calendar-gregorian-from-absolute
(+ (calendar-absolute-from-gregorian date) num)))
(defun cfw:strtime-emacs (time)
"Format emacs time value TIME to the string form YYYY/MM/DD."
(format-time-string "%Y/%m/%d" time))
(defun cfw:strtime (date)
"Format calendar date value DATE to the string form YYYY/MM/DD."
(cfw:strtime-emacs (cfw:calendar-to-emacs date)))
(defun cfw:parsetime-emacs (str)
"Transform the string format YYYY/MM/DD to an emacs time value."
(when (string-match "\\([0-9]+\\)\\/\\([0-9]+\\)\\/\\([0-9]+\\)" str)
(apply 'encode-time
(let (ret)
(dotimes (i 6)
(push (string-to-number (or (match-string (+ i 1) str) "0")) ret))
ret))))
(defun cfw:parsetime (str)
"Transform the string format YYYY/MM/DD to a calendar date value."
(cfw:emacs-to-calendar (cfw:parsetime-emacs str)))
(defun cfw:read-date-command-simple (string-date)
"Move the cursor to the specified date."
(interactive "sInput Date (YYYY/MM/DD): ")
(cfw:parsetime string-date))
(defun cfw:enumerate-days (begin end)
"Enumerate date objects between BEGIN and END."
(when (> (calendar-absolute-from-gregorian begin)
(calendar-absolute-from-gregorian end))
(error "Invalid period : %S - %S" begin end))
(let ((d begin) ret (cont t))
(while cont
(push (copy-sequence d) ret)
(setq cont (not (equal d end)))
(setq d (cfw:date-after d 1)))
(nreverse ret)))
(defun cfw:week-begin-date (date)
"Return date of beginning of the week in which DATE is."
(let ((num (- calendar-week-start-day
(calendar-day-of-week date))))
(cfw:date-after date (if (< 0 num) (- num cfw:week-days) num))))
(defun cfw:week-end-date (date)
"Return date of end of the week in which DATE is."
(let ((num (+ (- calendar-week-start-day 1)
(- cfw:week-days (calendar-day-of-week date)))))
(cfw:date-after date (cond
((> 0 num) (+ num cfw:week-days))
((<= cfw:week-days num) (- num cfw:week-days))
(t num)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Component
;; This structure defines attributes of the calendar component.
;; These attributes are internal use. Other programs should access
;; through the functions of the component interface.
;; [cfw:component]
;; dest : an object of `cfw:dest'
;; model : an object of the calendar model
;; selected : selected date
;; view : a symbol of view type (month, week, two-weeks, ...)
;; update-hooks : a list of hook functions for update event
;; selectoin-change-hooks : a list of hook functions for selection change event
;; click-hooks : a list of hook functions for click event
(defstruct cfw:component dest model selected view
update-hooks selection-change-hooks click-hooks)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Data Source
;; This structure defines data sources of the calendar.
;; [cfw:source]
;; name : data source title
;; data : a function that generates an alist of date-contents
;; update : a function that is called when the user needs to update the contents (optional)
;; color : foreground color for normal items (optional)
;; period-fgcolor : foreground color for period items (optional)
;; period-bgcolor : background color for period items (optional)
;; opt-face : a plist of additional face properties for normal items (optional)
;; opt-period-face : a plist of additional face properties for period items (optional)
;;
;; If `period-bgcolor' is nil, the value of `color' is used.
;; If `period-fgcolor' is nil, the black or white (negative color of `period-bgcolor') is used.
(defstruct cfw:source name data update color period-bgcolor period-fgcolor opt-face opt-period-face)
(defun cfw:source-period-bgcolor-get (source)
"[internal] Return a background color for period items.
If `cfw:source-period-bgcolor' is nil, the value of
`cfw:source-color' is used."
(or (cfw:source-period-bgcolor source)
(let ((c (cfw:source-color source)))
(when c
(setf (cfw:source-period-bgcolor source) c))
c)))
(defun cfw:source-period-fgcolor-get (source)
"[internal] Return a foreground color for period items.
If `cfw:source-period-fgcolor' is nil, the black or
white (negative color of `cfw:source-period-bgcolor') is used."
(or (cfw:source-period-fgcolor source)
(let ((c (destructuring-bind
(r g b) (color-values (or (cfw:source-period-bgcolor-get source) "black"))
(if (< 147500 (+ r g b)) "black" "white")))) ; (* 65536 3 0.75)
(setf (cfw:source-period-fgcolor source) c)
c)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Rendering Destination
;; This structure object is the abstraction of the rendering
;; destinations, such as buffers, regions and so on.
;; [cfw:dest]
;; type : identify symbol for destination type. (buffer, region, text)
;; buffer : a buffer object of rendering destination.
;; min-func : a function that returns upper limit of rendering destination.
;; max-func : a function that returns lower limit of rendering destination.
;; width : width of the reference size.
;; height : height of the reference size.
;; clear-func : a function that clears the rendering destination.
;; before-update-func : a function that is called at the beginning of rendering routine.
;; after-update-func : a function that is called at the end of rendering routine.
;; select-ol : a list of overlays for selection
;; today-ol : a list of overlays for today
(defstruct cfw:dest
type buffer min-func max-func width height
clear-func before-update-func after-update-func select-ol today-ol)
;; shortcut functions
(eval-when-compile
(defmacro cfw:dest-with-region (dest &rest body)
(let (($dest (gensym)))
`(let ((,$dest ,dest))
(with-current-buffer (cfw:dest-buffer ,$dest)
(save-restriction
(narrow-to-region
(cfw:dest-point-min ,$dest) (cfw:dest-point-max ,$dest))
,@body))))))
(put 'cfw:dest-with-region 'lisp-indent-function 1)
(defun cfw:dest-point-min (c)
(funcall (cfw:dest-min-func c)))
(defun cfw:dest-point-max (c)
(funcall (cfw:dest-max-func c)))
(defun cfw:dest-clear (c)
(funcall (cfw:dest-clear-func c)))
(defun cfw:dest-before-update (c)
(when (cfw:dest-before-update-func c)
(funcall (cfw:dest-before-update-func c))))
(defun cfw:dest-after-update (c)
(when (cfw:dest-after-update-func c)
(funcall (cfw:dest-after-update-func c))))
;; private functions
(defun cfw:dest-ol-selection-clear (dest)
"[internal] Clear the selection overlays on the current calendar view."
(loop for i in (cfw:dest-select-ol dest)
do (delete-overlay i))
(setf (cfw:dest-select-ol dest) nil))
(defun cfw:dest-ol-selection-set (dest date)
"[internal] Put a selection overlay on DATE. The selection overlay can be
put on some days, calling this function many times. If DATE is
not included on the current calendar view, do nothing. This
function does not manage the selections, just put the overlay."
(lexical-let (ols)
(cfw:dest-with-region dest
(cfw:find-all-by-date
dest date
(lambda (begin end)
(let ((overlay (make-overlay begin end)))
(overlay-put overlay 'face
(if (eq 'cfw:face-day-title
(get-text-property begin 'face))
'cfw:face-select))
(push overlay ols)))))
(setf (cfw:dest-select-ol dest) ols)))
(defun cfw:dest-ol-today-clear (dest)
"[internal] Clear decoration overlays."
(loop for i in (cfw:dest-today-ol dest)
do (delete-overlay i))
(setf (cfw:dest-today-ol dest) nil))
(defun cfw:dest-ol-today-set (dest)
"[internal] Put a highlight face on today."
(lexical-let (ols)
(cfw:dest-with-region dest
(cfw:find-all-by-date
dest (calendar-current-date)
(lambda (begin end)
(let ((overlay (make-overlay begin end)))
(overlay-put overlay 'face
(if (eq 'cfw:face-day-title
(get-text-property begin 'face))
'cfw:face-today-title 'cfw:face-today))
(push overlay ols)))))
(setf (cfw:dest-today-ol dest) ols)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Low level API
;; Buffer
(defconst cfw:calendar-buffer-name "*cfw-calendar*" "[internal] Default buffer name for the calendar view.")
(defun cfw:dest-init-buffer (&optional buf width height custom-map)
"Create a buffer destination.
This destination uses an entire buffer and set up the major-mode
`cfw:calendar-mode' and the key map `cfw:calendar-mode-map'. BUF
is a buffer name to render the calendar view. If BUF is nil, the
default buffer name `cfw:calendar-buffer-name' is used. WIDTH
and HEIGHT are reference size of the calendar view. If those are
nil, the size of calendar is calculated from the window that
shows BUF or the selected window. The component
object is stored at the buffer local variable `cfw:component'.
CUSTOM-MAP is the additional keymap that is added to default
keymap `cfw:calendar-mode-map'."
(lexical-let
((buffer (or buf (get-buffer-create cfw:calendar-buffer-name)))
(window (or (and buf (get-buffer-window buf)) (selected-window)))
dest)
(setq dest
(make-cfw:dest
:type 'buffer
:min-func 'point-min
:max-func 'point-max
:buffer buffer
:width (or width (window-width window))
:height (or height (window-height window))
:clear-func (lambda ()
(with-current-buffer buffer
(erase-buffer)))))
(with-current-buffer buffer
(unless (eq major-mode 'cfw:calendar-mode)
(cfw:calendar-mode custom-map)))
dest))
;; Region
(defun cfw:dest-init-region (buf mark-begin mark-end &optional width height)
"Create a region destination. The calendar is drew between
MARK-BEGIN and MARK-END in the buffer BUF. MARK-BEGIN and
MARK-END are separated by more than one character, such as a
space. This destination is employed to be embedded in the some
application buffer. Because this destination does not set up
any modes and key maps for the buffer, the application that uses
the calfw is responsible to manage the buffer and key maps."
(lexical-let
((mark-begin mark-begin) (mark-end mark-end)
(window (or (get-buffer-window buf) (selected-window))))
(make-cfw:dest
:type 'region
:min-func (lambda () (marker-position mark-begin))
:max-func (lambda () (marker-position mark-end))
:buffer buf
:width (or width (window-width window))
:height (or height (window-height window))
:clear-func
(lambda ()
(cfw:dest-region-clear (marker-position mark-begin)
(marker-position mark-end)))
)))
(defun cfw:dest-region-clear (begin end)
"[internal] Clear the content text."
(when (< 2 (- end begin))
(delete-region begin (1- end)))
(goto-char begin))
;; Inline text
(defconst cfw:dest-background-buffer " *cfw:dest-background*")
(defun cfw:dest-init-inline (width height)
"Create a text destination."
(lexical-let
((buffer (get-buffer-create cfw:dest-background-buffer))
(window (selected-window))
dest)
(setq dest
(make-cfw:dest
:type 'text
:min-func 'point-min
:max-func 'point-max
:buffer buffer
:width (or width (window-width window))
:height (or height (window-height window))
:clear-func (lambda ()
(with-current-buffer buffer
(erase-buffer)))))
dest))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Component API
;; Create
(defun cfw:cp-new (dest model view &optional selected-date)
"[internal] Create a new component object.
DEST is a cfw:dest object. MODEL is a model object. VIEW is a
symbol of the view type: month, two-weeks, week and day.
SELECTED-DATE is a selected date initially. This function is
called by the initialization functions,
`cfw:create-calendar-component-buffer',
`cfw:create-calendar-component-region' and
`cfw:get-calendar-text'."
(let ((cp (make-cfw:component
:dest dest
:model model
:view (or view 'month)
:selected (or selected-date (calendar-current-date)))))
(cfw:cp-update cp)
cp))
;; Getting the component instance
(defun cfw:cp-get-component ()
"Return the component object on the current cursor position.
Firstly, getting a text property `cfw:component' on the current
position. If no object is found in the text property, the buffer
local variable `cfw:component' is tried to get. If no object is
found at the variable, return nil."
(let ((component (get-text-property (point) 'cfw:component)))
(unless component
(unless (local-variable-p 'cfw:component (current-buffer))
(error "Not found cfw:component attribute..."))
(setq component (buffer-local-value 'cfw:component (current-buffer))))
component))
;; Getter
(defun cfw:cp-get-selected-date (component)
"Return the selected date of the component."
(cfw:component-selected component))
(defun cfw:cp-get-contents-sources (component)
"Return a list of the content sources."
(cfw:model-get-contents-sources (cfw:component-model component)))
(defun cfw:cp-get-annotation-sources (component)
"Return a list of the annotation sources."
(cfw:model-get-annotation-sources (cfw:component-model component)))
(defun cfw:cp-get-view (component)
"Return a symbol of the current view type."
(cfw:component-view component))
(defun cfw:cp-get-buffer (component)
"Return a buffer object on which the component draws the content."
(cfw:dest-buffer (cfw:component-dest component)))
(defun cfw:cp-displayed-date-p (component date)
"If the date is displayed in the current view, return `t'. Otherwise return `nil'."
(let* ((model (cfw:component-model component))
(begin (cfw:k 'begin-date model))
(end (cfw:k 'end-date model)))
(unless (and begin end) (error "Wrong model : %S" model))
(cfw:date-between begin end date)))
;; Setter
(defun cfw:cp-move-cursor (dest date)
"[internal] Just move the cursor onto the date. This function
is called by `cfw:cp-set-selected-date'."
(let ((pos (cfw:find-by-date dest date)))
(when pos
(goto-char pos)
(unless (eql (selected-window) (get-buffer-window (current-buffer)))
(set-window-point (get-buffer-window (current-buffer)) pos)))))
(defun cfw:cp-set-selected-date (component date)
"Select the date on the component. If the current view doesn't contain the date,
this function updates the view to display the date."
(let ((last (cfw:component-selected component))
(dest (cfw:component-dest component))
(model (cfw:component-model component)))
(cond
((cfw:cp-displayed-date-p component date)
(setf (cfw:component-selected component) date)
(cfw:dest-before-update dest)
(cfw:dest-ol-selection-clear dest)
(cfw:dest-ol-selection-set dest date)
(cfw:dest-after-update dest)
(cfw:cp-move-cursor dest date)
(unless (equal last date)
(cfw:cp-fire-selection-change-hooks component)))
(t
(cfw:model-set-init-date date model)
(setf (cfw:component-selected component) date)
(cfw:cp-update component)
(cfw:cp-fire-selection-change-hooks component)
;; Because this function will be called from cfw:cp-update, do nothing here.
))))
(defun cfw:cp-set-contents-sources (component sources)
"Set content sources for the component.
SOURCES is a list of content sources."
(cfw:model-set-contents-sources
(cfw:component-model component) sources))
(defun cfw:cp-set-annotation-sources (component sources)
"Set annotation sources for the component.
SOURCES is a list of annotation sources."
(cfw:model-set-annotation-sources
(cfw:component-model component) sources))
(defun cfw:cp-set-view (component view)
"Change the view type of the component and re-draw the content.
VIEW is a symbol of the view type."
(setf (cfw:component-view component) view)
(cfw:cp-update component))
(defun cfw:cp-resize (component width height)
"Resize the component size and re-draw the content."
(let* ((dest (cfw:component-dest component))
(buf (cfw:dest-buffer dest))
(window (or (and buf (get-buffer-window buf)) (selected-window))))
(setf (cfw:dest-width dest) (or width (window-width window))
(cfw:dest-height dest) (or height (window-height window))))
(cfw:cp-update component))
;; Hook
(defun cfw:cp-add-update-hook (component hook)
"Add the update hook function to the component.
HOOK is a function that has no argument."
(push hook (cfw:component-update-hooks component)))
(defun cfw:cp-add-selection-change-hook (component hook)
"Add the selection change hook function to the component.
HOOK is a function that has no argument."
(push hook (cfw:component-selection-change-hooks component)))
(defun cfw:cp-add-click-hook (component hook)
"Add the click hook function to the component.
HOOK is a function that has no argument."
(push hook (cfw:component-click-hooks component)))
;;; private methods
(defun cfw:cp-dispatch-view-impl (view)
"[internal] Return a view function which is corresponding to the view symbol.
VIEW is a symbol of the view type."
(cond
((eq 'month view) 'cfw:view-month)
((eq 'week view) 'cfw:view-week)
((eq 'two-weeks view) 'cfw:view-two-weeks)
((eq 'day view) 'cfw:view-day)
(t (error "Not found such view : %s" view))))
(defun cfw:cp-update (component)
"[internal] Clear and re-draw the component content."
(let* ((buf (cfw:cp-get-buffer component))
(dest (cfw:component-dest component)))
(with-current-buffer buf
(cfw:dest-before-update dest)
(cfw:dest-ol-selection-clear dest)
(cfw:dest-ol-today-clear dest)
(let ((buffer-read-only nil))
(cfw:dest-with-region dest
(cfw:dest-clear dest)
(funcall (cfw:cp-dispatch-view-impl
(cfw:component-view component))
component)))
(cfw:dest-ol-today-set dest)
(cfw:cp-set-selected-date
component (cfw:component-selected component))
(cfw:dest-after-update dest)
(cfw:cp-fire-update-hooks component))))
(defun cfw:cp-fire-click-hooks (component)
"[internal] Call click hook functions of the component with no arguments."
(loop for f in (cfw:component-click-hooks component)
do (condition-case err
(funcall f)
(nil (message "Calfw: Click / Hook error %S [%s]" f err)))))
(defun cfw:cp-fire-selection-change-hooks (component)
"[internal] Call selection change hook functions of the component with no arguments."
(loop for f in (cfw:component-selection-change-hooks component)
do (condition-case err
(funcall f)
(nil (message "Calfw: Selection change / Hook error %S [%s]" f err)))))
(defun cfw:cp-fire-update-hooks (component)
"[internal] Call update hook functions of the component with no arguments."
(loop for f in (cfw:component-update-hooks component)
do (condition-case err
(funcall f)
(nil (message "Calfw: Update / Hook error %S [%s]" f err)))))
;;; Models
(defun cfw:model-abstract-new (date contents-sources annotation-sources &optional sorter)
"Return an abstract model object.
DATE is initial date for the calculation of the start date and end one.
CONTENTS-SOURCES is a list of contents functions.
ANNOTATION-SOURCES is a list of annotation functions."
(unless date (setq date (calendar-current-date)))
`((init-date . ,date)
(contents-sources . ,contents-sources)
(annotation-sources . ,annotation-sources)
(sorter . ,(or sorter cfw:default-text-sorter))))
(defun cfw:model-abstract-derived (date org-model)
"Return an abstract model object. The contents functions and annotation ones are copied from ORG-MODEL.
DATE is initial date for the calculation of the start date and end one.
ORG-MODEL is a model object to inherit."
(cfw:model-abstract-new
date
(cfw:model-get-contents-sources org-model)
(cfw:model-get-annotation-sources org-model)
(cfw:model-get-sorter org-model)))
(defun cfw:model-create-updated-view-data (model view-data)
"[internal] Clear previous view model data from MODEL and return a new model with VIEW-DATA."
(append
(cfw:model-abstract-derived
(cfw:k 'init-date model) model)
view-data))
(defvar cfw:default-text-sorter 'string-lessp "[internal] Default sorting criteria in a calendar cell.")
;; public functions
(defun cfw:model-get-holiday-by-date (date model)
"Return a holiday title on the DATE."
(cfw:contents-get date (cfw:k 'holidays model)))
(defun cfw:model-get-contents-by-date (date model)
"Return a list of contents on the DATE."
(cfw:contents-get date (cfw:k 'contents model)))
(defun cfw:model-get-annotation-by-date (date model)
"Return an annotation on the DATE."
(cfw:contents-get date (cfw:k 'annotations model)))
(defun cfw:model-get-periods-by-date (date model)
"Return a list of periods on the DATE."
(loop for period in (cfw:k 'periods model)
for (begin end content) = period
if (cfw:date-between begin end date)
collect period))
(defun cfw:model-get-sorter (model)
"Return a sorter function."
(cfw:k 'sorter model))
;; private functions
(defun cfw:model-get-contents-sources (model)
"[internal] Return a list of content sources of the model."
(cfw:k 'contents-sources model))
(defun cfw:model-get-annotation-sources (model)
"[internal] Return a list of annotation sources of the model."
(cfw:k 'annotation-sources model))
(defun cfw:model-set-init-date (date model)
"[internal] Set the init-date that is used to calculate the
display period of the calendar."
(let ((cell (assq 'init-date model)))
(cond
(cell (setcdr cell date))
(t (push (cons 'init-date date) model))))
date)
(defun cfw:model-set-contents-sources (sources model)
"[internal] Set the content sources of the model."
(let ((cell (assq 'contents-sources model)))
(cond
(cell (setcdr cell sources))
(t (push (cons 'contents-sources sources) model))))
sources)
(defun cfw:model-set-annotation-sources (sources model)
"[internal] Set the annotation sources of the model."
(let ((cell (assq 'annotation-sources model)))
(cond
(cell (setcdr cell sources))
(t (push (cons 'annotation-sources sources) model))))
sources)
(defun cfw:contents-get (date contents)
"[internal] Return a list of contents on the DATE."
(cdr (cfw:contents-get-internal date contents)))
(defun cfw:contents-get-internal (date contents)
"[internal] Return a cons cell that has the key DATE.
One can modify the returned cons cell destructively."
(cond
((or (null date) (null contents)) nil)
(t (loop for i in contents
if (equal date (car i))
return i
finally return nil))))
(defun cfw:contents-add (date content contents)
"[internal] Add a record, DATE as a key and CONTENT as a body,
to CONTENTS destructively. If CONTENTS has a record for DATE,
this function appends CONTENT to the record. Return the modified
contents list."
(let* ((prv (cfw:contents-get-internal date contents))
(lst (if (listp content) (copy-sequence content) (list content))))
(if prv
(setcdr prv (append (cdr prv) lst))
(push (cons date lst) contents)))
contents)
(defun cfw:contents-merge (begin end sources)
"[internal] Return an contents alist between begin date and end one,
calling functions `:data' function."
(cond
((null sources) nil)
((= 1 (length sources))
(cfw:contents-put-source
(funcall (cfw:source-data (car sources)) begin end)
(car sources)))
(t
(loop for s in sources
for f = (cfw:source-data s)
for cnts = (cfw:contents-put-source
(funcall f begin end) s)
with contents = nil
do
(loop for c in cnts
for (d . line) = c
do (setq contents (cfw:contents-add d line contents)))
finally return contents))))
(defun cfw:contents-put-source (contents source)
"[internal] Put the source object to the text property
`cfw:source' in the contents list. During rendering, the source
object is used to put some face property."
(cond
((null source) contents)
(t
(loop for (k . lst) in contents
if (eq k 'periods)
collect ; periods
(cons k
(loop for (begin end summaries) in lst
for summary-text = (if (listp summaries)