-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathctable.el
1925 lines (1687 loc) · 72.4 KB
/
ctable.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
;;; ctable.el --- Table component for Emacs Lisp -*- lexical-binding: t; -*-
;; Copyright (C) 2011-2021 SAKURAI Masashi
;; Author: SAKURAI Masashi <m.sakurai at kiwanami.net>
;; URL: https://github.com/kiwanami/emacs-ctable
;; Version: 0.1.3
;; Package-Requires: ((emacs "24.3") (cl-lib "0.5"))
;; Keywords: table
;; 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 table component for Emacs Lisp.
;; Other programs can use this table component for the application UI.
;;; Installation:
;; Place this program in your load path and add following code.
;; (require 'ctable)
;;; Usage:
;; Executing the command `ctbl:open-table-buffer', switch to the table buffer.
;; Table data which are shown in the table view, are collected
;; by the `ctbl:model' objects. See the function `ctbl:demo' for example.
;; See the README document for the details.
;;; Code:
(require 'cl-lib)
(declare-function popup-tip "popup")
(declare-function pos-tip-show "pos-tip")
;;; Models and Parameters
(cl-defstruct ctbl:model
"Table model structure
data : Table data as a list of rows. A row contains a list of columns.
If an instance of `ctbl:async-model' is given, the model is built up asynchronously.
column-model : A list of column models.
sort-state : The current sort order as a list of column indexes.
The index number of the first column is 1.
If the index is negative, the sort order is reversed."
data column-model sort-state)
(cl-defstruct ctbl:async-model
"Asynchronous data model
request : Data request function which receives 4 arguments (begin-num length fn(row-list) fe(errmsg)).
This function should return the next data which begins with `begin-num' and has the length
as `length', evaluating the continuation function `fn' with the data.
If the function `fn' is given `nil', it means no more data.
If the error function `fe' is evaluated with `errmsg', the message is displayed for the user.
init-num : Initial row number. (Default 20)
more-num : Increase row number. (Default 20)
reset : Reset function which is called when user executes update command. (Can be nil)
cancel : Cancel function of data requesting. (Can be nil)
For forward compatibility, these callback functions should have a `&rest' keyword at the end of argument list.
"
request (init-num 20) (more-num 20) reset cancel)
(cl-defstruct ctbl:cmodel
"Table column model structure
title : title string.
sorter : sorting function which transforms a cell value into sort value.
It should return -1, 0 and 1. If nil, `ctbl:sort-string-lessp' is used.
align : text alignment: 'left, 'right and 'center. (default: right)
max-width : maximum width of the column. if nil, no constraint. (default: nil)
min-width : minimum width of the column. if nil, no constraint. (default: nil)
click-hooks : a list of functions for header clicking with two arguments
the `ctbl:component' object and the `ctbl:cmodel' one.
(default: '(`ctbl:cmodel-sort-action'))"
title sorter align max-width min-width
(click-hooks '(ctbl:cmodel-sort-action)))
(cl-defstruct ctbl:param
"Rendering parameters
display-header : if t, display the header row with column models.
fixed-header : if t, display the header row in the header-line area.
bg-colors : '(((row-id . col-id) . colorstr) (t . default-color) ... ) or (lambda (model row-id col-id) colorstr or nil)
vline-colors : \"#RRGGBB\" or '((0 . colorstr) (t . default-color)) or (lambda (model col-index) colorstr or nil)
hline-colors : \"#RRGGBB\" or '((0 . colorstr) (t . default-color)) or (lambda (model row-index) colorstr or nil)
draw-vlines : 'all or '(0 1 2 .. -1) or (lambda (model col-index) t or nil )
draw-hlines : 'all or '(0 1 2 .. -1) or (lambda (model row-index) t or nil )
vertical-line horizontal-line : | -
left-top-corner right-top-corner left-bottom-corner right-bottom-corner : +
top-junction bottom-junction left-junction right-junction cross-junction : +"
display-header fixed-header
bg-colors vline-colors hline-colors draw-vlines draw-hlines vertical-line horizontal-line
left-top-corner right-top-corner left-bottom-corner right-bottom-corner
top-junction bottom-junction left-junction right-junction cross-junction)
(defvar ctbl:completing-read 'completing-read
"Customize for completing-read function.
To use `ido-completing-read', put the following sexp into your
Emacs init file:
(eval-after-load 'ido
'(progn
(setq ctbl:completing-read 'ido-completing-read)))")
(defvar ctbl:default-rendering-param
(make-ctbl:param
:display-header t
:fixed-header nil
:bg-colors nil
:vline-colors "DarkGray"
:hline-colors "DarkGray"
:draw-vlines 'all
:draw-hlines '(1)
:vertical-line ?|
:horizontal-line ?-
:left-top-corner ?+
:right-top-corner ?+
:left-bottom-corner ?+
:right-bottom-corner ?+
:top-junction ?+
:bottom-junction ?+
:left-junction ?+
:right-junction ?+
:cross-junction ?+
)
"Default rendering parameters.")
(defvar ctbl:tooltip-method '(pos-tip popup minibuffer)
"Preferred tooltip methods in order.")
(defvar ctbl:component)
(defvar ctbl:header-text)
;;; Faces
(defface ctbl:face-row-select
'((((class color) (background light))
:background "WhiteSmoke")
(((class color) (background dark))
:background "Blue4"))
"Face for row selection" :group 'ctable)
(defface ctbl:face-cell-select
'((((class color) (background light))
:background "Mistyrose1")
(((class color) (background dark))
:background "Blue2"))
"Face for cell selection" :group 'ctable)
(defface ctbl:face-continue-bar
'((((class color) (background light))
:background "OldLace")
(((class color) (background dark))
:background "Gray26"))
"Face for continue bar" :group 'ctable)
;;; Utilities
(defun ctbl:define-keymap (keymap-list &optional prefix)
"[internal] Keymap utility."
(let ((map (make-sparse-keymap)))
(mapc
(lambda (i)
(define-key map
(if (stringp (car i))
(read-kbd-macro
(if prefix
(replace-regexp-in-string "prefix" prefix (car i))
(car i)))
(car i))
(cdr i)))
keymap-list)
map))
(defun ctbl:cell-id (row-id col-id)
"[internal] Create a cell-id object"
(cons row-id col-id))
(defun ctbl: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)
(defvar ctbl:uid 1)
(defun ctbl:uid ()
"[internal] Generate an unique number."
(cl-incf ctbl:uid))
(defun ctbl:fill-keymap-property (begin end keymap)
"[internal] Put the given text property to the region between BEGIN and END.
If the text already has some keymap property, the text is skipped."
(save-excursion
(goto-char begin)
(cl-loop with pos = begin with nxt = nil
until (or (null pos) (<= end pos))
when (get-text-property pos 'keymap) do
(setq pos (next-single-property-change pos 'keymap))
else do
(setq nxt (next-single-property-change pos 'keymap))
(when (null nxt) (setq nxt end))
(put-text-property pos (min nxt end) 'keymap keymap))))
;; Model functions
(defun ctbl:model-column-length (model)
"[internal] Return the column number."
(length (ctbl:model-column-model model)))
(defun ctbl:model-row-length (model)
"[internal] Return the row number."
(length (ctbl:model-data model)))
(defun ctbl:model-modify-sort-key (model col-index)
"Modify the list of sort keys for the column headers."
(let* ((sort-keys (ctbl:model-sort-state model))
(col-key (1+ col-index)))
(cond
((eq (car sort-keys) col-key)
(setf (ctbl:model-sort-state model)
(cons (- col-key) (cdr sort-keys))))
((eq (car sort-keys) (- col-key))
(setf (ctbl:model-sort-state model)
(cons col-key (cdr sort-keys))))
(t
(setf (ctbl:model-sort-state model)
(cons col-key (delete (- col-key)
(delete col-key sort-keys))))))
(ctbl:model-sort-state model)))
(defun ctbl:cmodel-sort-action (cp col-index)
"Sorting action for click on the column headers.
If data is an instance of `ctbl:async-model', this function do nothing."
(let* ((model (ctbl:cp-get-model cp)))
(unless (ctbl:async-model-p (ctbl:model-data model))
(ctbl:model-modify-sort-key model col-index)
(ctbl:cp-update cp))))
;;; ctable framework
;; Component
(cl-defstruct ctbl:component
"Component
This structure defines attributes of the table component.
These attributes are internal use. Other programs should access
through the functions of the component interface.
dest : an object of `ctbl:dest'
model : an object of the table model
selected : selected cell-id: (row index . col index)
param : rendering parameter object
sorted-data : sorted data to display the table view.
see `ctbl:cp-get-selected-data-row' and `ctbl:cp-get-selected-data-cell'.
update-hooks : a list of hook functions for update event
selection-change-hooks : a list of hook functions for selection change event
click-hooks : a list of hook functions for click event
states : alist of arbitrary data for internal use"
dest model param selected sorted-data
update-hooks selection-change-hooks click-hooks states)
;; Rendering Destination
(cl-defstruct ctbl:dest
"Rendering Destination
This structure object is the abstraction of the rendering
destinations, such as buffers, regions and so on.
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. (number, nil or full)
height : height of the reference size. (number, nil or full)
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"
type buffer min-func max-func width height
clear-func before-update-func after-update-func select-ol)
(eval-when-compile
(defmacro ctbl:dest-with-region (dest &rest body)
(declare (debug (form &rest form)))
(let (($dest (gensym)))
`(let ((,$dest ,dest))
(with-current-buffer (ctbl:dest-buffer ,$dest)
(save-restriction
(narrow-to-region
(ctbl:dest-point-min ,$dest) (ctbl:dest-point-max ,$dest))
,@body))))))
(put 'ctbl:dest-with-region 'lisp-indent-function 1)
(defun ctbl:dest-point-min (c)
(funcall (ctbl:dest-min-func c)))
(defun ctbl:dest-point-max (c)
(funcall (ctbl:dest-max-func c)))
(defun ctbl:dest-clear (c)
(funcall (ctbl:dest-clear-func c)))
(defun ctbl:dest-before-update (c)
(when (ctbl:dest-before-update-func c)
(funcall (ctbl:dest-before-update-func c))))
(defun ctbl:dest-after-update (c)
(when (ctbl:dest-after-update-func c)
(funcall (ctbl:dest-after-update-func c))))
;; Buffer
(defconst ctbl:table-buffer-name "*ctbl-table*" "[internal] Default buffer name for the table view.")
(defun ctbl: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
`ctbl:table-mode' and the key map `ctbl:table-mode-map'. BUF is
a buffer name to render the table view. If BUF is nil, the
default buffer name is used. WIDTH and HEIGHT are reference size
of the table view. If those are nil, the size of table is
calculated from the window that shows BUF or the selected window.
The component object is stored at the buffer local variable
`ctbl:component'. CUSTOM-MAP is the additional keymap that is
added to default keymap `ctbl:table-mode-map'."
(let
((buffer (or buf (get-buffer-create (format "*Table: %d*" (ctbl:uid)))))
(window (or (and buf (get-buffer-window buf)) (selected-window)))
dest)
(setq dest
(make-ctbl:dest
:type 'buffer
:min-func 'point-min
:max-func 'point-max
:buffer buffer
:width width
:height height
:clear-func (lambda ()
(with-current-buffer buffer
(erase-buffer)))))
(with-current-buffer buffer
(unless (eq major-mode 'ctbl:table-mode)
(ctbl:table-mode custom-map)))
dest))
;; Region
(defun ctbl:dest-init-region (buf mark-begin mark-end &optional width height)
"Create a region destination. The table 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 ctable is responsible to manage the buffer and key maps."
(let
((mark-begin mark-begin) (mark-end mark-end)
(window (or (get-buffer-window buf) (selected-window))))
(make-ctbl:dest
:type 'region
:min-func (lambda () (marker-position mark-begin))
:max-func (lambda () (marker-position mark-end))
:buffer buf
:width width
:height height
:clear-func
(lambda ()
(ctbl:dest-region-clear (marker-position mark-begin)
(marker-position mark-end))))))
(defun ctbl: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 ctbl:dest-background-buffer " *ctbl:dest-background*")
(defun ctbl:dest-init-inline (width height)
"Create a text destination."
(let
((buffer (get-buffer-create ctbl:dest-background-buffer))
(window (selected-window))
dest)
(setq dest
(make-ctbl:dest
:type 'text
:min-func 'point-min
:max-func 'point-max
:buffer buffer
:width width
:height height
:clear-func (lambda ()
(with-current-buffer buffer
(erase-buffer)))))
dest))
;; private functions
(defun ctbl:dest-ol-selection-clear (dest)
"[internal] Clear the selection overlays on the current table view."
(cl-loop for i in (ctbl:dest-select-ol dest)
do (delete-overlay i))
(setf (ctbl:dest-select-ol dest) nil))
(defun ctbl:dest-ol-selection-set (dest cell-id)
"[internal] Put a selection overlay on CELL-ID. The selection overlay can be
put on some cells, calling this function many times. This
function does not manage the selections, just put the overlay."
(let (ols (row-id (car cell-id)) (col-id (cdr cell-id)))
(ctbl:dest-with-region dest
(ctbl:find-all-by-row-id
dest row-id
(lambda (tcell-id begin end)
(let ((overlay (make-overlay begin end)))
(overlay-put overlay 'face
(if (= (cdr tcell-id) col-id)
'ctbl:face-cell-select
'ctbl:face-row-select))
(push overlay ols)))))
(setf (ctbl:dest-select-ol dest) ols)))
;; Component implementation
(defun ctbl:cp-new (dest model param)
"[internal] Create a new component object.
DEST is a ctbl:dest object. MODEL is a model object. PARAM is a
rendering parameter object. This function is called by the
initialization functions, `ctbl:create-table-component-buffer',
`ctbl:create-table-component-region' and `ctbl:get-table-text'."
(let ((cp (make-ctbl:component
:selected '(0 . 0)
:dest dest
:model model
:param (or param ctbl:default-rendering-param))))
(ctbl:cp-update cp)
cp))
(defun ctbl:cp-get-component ()
"Return the component object on the current cursor position.
Firstly, getting a text property `ctbl:component' on the current
position. If no object is found in the text property, the buffer
local variable `ctbl:component' is tried to get. If no object is
found at the variable, return nil."
(let ((component (get-text-property (point) 'ctbl:component)))
(unless component
(unless (local-variable-p 'ctbl:component (current-buffer))
(error "Not found ctbl:component attribute..."))
(setq component (buffer-local-value 'ctbl:component (current-buffer))))
component))
;; Component : getters
(defun ctbl:cp-get-selected (component)
"Return the selected cell-id of the component."
(ctbl:component-selected component))
(defun ctbl:cp-get-selected-data-row (component)
"Return the selected row data. If no cell is selected, return nil."
(let* ((rows (ctbl:component-sorted-data component))
(cell-id (ctbl:component-selected component))
(row-id (car cell-id)) (col-id (cdr cell-id)))
(if row-id (nth row-id rows) nil)))
(defun ctbl:cp-get-selected-data-cell (component)
"Return the selected cell data. If no cell is selected, return nil."
(let* ((rows (ctbl:component-sorted-data component))
(cell-id (ctbl:component-selected component))
(row-id (car cell-id)) (col-id (cdr cell-id)))
(if row-id
(nth col-id (nth row-id rows))
nil)))
(defun ctbl:cp-get-model (component)
"Return the model object."
(ctbl:component-model component))
(defun ctbl:cp-set-model (component model)
"Replace the model object and update the destination."
(setf (ctbl:component-model component) model)
(ctbl:cp-update component))
(defun ctbl:cp-get-param (component)
"Return a rendering parameter object."
(ctbl:component-param component))
(defun ctbl:cp-get-buffer (component)
"Return a buffer object on which the component draws the content."
(ctbl:dest-buffer (ctbl:component-dest component)))
;; Component : setters
(defun ctbl:cp-move-cursor (dest cell-id)
"[internal] Just move the cursor onto the CELL-ID.
If CELL-ID is not found, return nil. This function
is called by `ctbl:cp-set-selected-cell'."
(let ((pos (ctbl:find-by-cell-id dest cell-id)))
(cond
(pos
(goto-char pos)
(unless (eql (selected-window) (get-buffer-window (current-buffer)))
(set-window-point (get-buffer-window (current-buffer)) pos))
t)
(t nil))))
(defun ctbl:cp-set-selected-cell (component cell-id)
"Select the cell on the component. If the current view doesn't contain the cell,
this function updates the view to display the cell."
(let ((last (ctbl:component-selected component))
(dest (ctbl:component-dest component))
(model (ctbl:component-model component)))
(when (ctbl:cp-move-cursor dest cell-id)
(setf (ctbl:component-selected component) cell-id)
(ctbl:dest-before-update dest)
(ctbl:dest-ol-selection-clear dest)
(ctbl:dest-ol-selection-set dest cell-id)
(ctbl:dest-after-update dest)
(unless (equal last cell-id)
(ctbl:cp-fire-selection-change-hooks component)))))
;; Hook
(defun ctbl:cp-add-update-hook (component hook)
"Add the update hook function to the component.
HOOK is a function that has no argument."
(push hook (ctbl:component-update-hooks component)))
(defun ctbl: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 (ctbl:component-selection-change-hooks component)))
(defun ctbl:cp-add-click-hook (component hook)
"Add the click hook function to the component.
HOOK is a function that has no argument."
(push hook (ctbl:component-click-hooks component)))
;; update
(defun ctbl:cp-update (component)
"Clear and re-draw the component content."
(let* ((buf (ctbl:cp-get-buffer component))
(dest (ctbl:component-dest component)))
(with-current-buffer buf
(ctbl:dest-before-update dest)
(ctbl:dest-ol-selection-clear dest)
(let (buffer-read-only)
(ctbl:dest-with-region dest
(ctbl:dest-clear dest)
(cond
;; asynchronous model
((ctbl:async-model-p
(ctbl:model-data (ctbl:component-model component)))
(let ((cp component))
(ctbl:async-state-on-update cp)
(ctbl:render-async-main
dest
(ctbl:component-model component)
(ctbl:component-param component)
(lambda (rows &optional astate)
(setf (ctbl:component-sorted-data cp) rows)
(when astate
(ctbl:cp-states-set cp 'async-state astate))))))
;; synchronous model
(t
(setf (ctbl:component-sorted-data component)
(ctbl:render-main
dest
(ctbl:component-model component)
(ctbl:component-param component)))))))
(ctbl:cp-set-selected-cell
component (ctbl:component-selected component))
(ctbl:dest-after-update dest)
(ctbl:cp-fire-update-hooks component))))
;; Component : privates
(defun ctbl:cp-states-get (component key)
"[internal] Get a value from COMPONENT with KEY."
(cdr (assq key (ctbl:component-states component))))
(defun ctbl:cp-states-set (component key value)
"[internal] Set a value with KEY."
(let ((pair (assq key (ctbl:component-states component))))
(cond
((null pair)
(push (cons key value) (ctbl:component-states component)))
(t
(setf (cdr pair) value)))))
(defun ctbl:cp-fire-click-hooks (component)
"[internal] Call click hook functions of the component with no arguments."
(cl-loop for f in (ctbl:component-click-hooks component)
do (condition-case err
(funcall f)
(error (message "CTable: Click / Hook error %S [%s]" f err)))))
(defun ctbl:cp-fire-selection-change-hooks (component)
"[internal] Call selection change hook functions of the component with no arguments."
(cl-loop for f in (ctbl:component-selection-change-hooks component)
do (condition-case err
(funcall f)
(error (message "CTable: Selection change / Hook error %S [%s]" f err)))))
(defun ctbl:cp-fire-update-hooks (component)
"[internal] Call update hook functions of the component with no arguments."
(cl-loop for f in (ctbl:component-update-hooks component)
do (condition-case err
(funcall f)
(error (message "Ctable: Update / Hook error %S [%s]" f err)))))
(defun ctbl:find-position-fast (dest cell-id)
"[internal] Find the cell-id position using bi-section search."
(let* ((row-id (car cell-id))
(row-id-lim (max (- row-id 10) 0))
(min (ctbl:dest-point-min dest))
(max (ctbl:dest-point-max dest))
(mid (/ (+ min max) 2)))
(save-excursion
(cl-loop for next = (next-single-property-change mid 'ctbl:cell-id nil max)
for cur-row-id = (and next (car (ctbl:cursor-to-cell next)))
do
(cond
((>= next max) (cl-return (point)))
((null cur-row-id) (setq mid next))
((= cur-row-id row-id)
(goto-char mid) (beginning-of-line)
(cl-return (point)))
((and (< row-id-lim cur-row-id) (< cur-row-id row-id))
(goto-char mid) (beginning-of-line) (forward-line)
(cl-return (point)))
((< cur-row-id row-id)
(setq min mid)
(setq mid (/ (+ min max) 2)))
((< row-id cur-row-id)
(setq max mid)
(setq mid (/ (+ min max) 2))))))))
(defun ctbl:find-by-cell-id (dest cell-id)
"[internal] Return a point where the text property `ctbl:cell-id'
is equal to cell-id in the current table view. If CELL-ID is not
found in the current view, return nil."
(cl-loop with pos = (ctbl:find-position-fast dest cell-id)
with end = (ctbl:dest-point-max dest)
for next = (next-single-property-change pos 'ctbl:cell-id nil end)
for text-cell = (and next (ctbl:cursor-to-cell next))
while (and next (< next end)) do
(if (and text-cell (equal cell-id text-cell))
(cl-return next))
(setq pos next)))
(defun ctbl:find-all-by-cell-id (dest cell-id func)
"[internal] Call the function FUNC in each regions where the
text-property `ctbl:cell-id' is equal to CELL-ID. The argument function FUNC
receives two arguments, begin position and end one. This function is
mainly used at functions for putting overlays."
(cl-loop with pos = (ctbl:find-position-fast dest cell-id)
with end = (ctbl:dest-point-max dest)
for next = (next-single-property-change pos 'ctbl:cell-id nil end)
for text-id = (and next (ctbl:cursor-to-cell next))
while (and next (< next end)) do
(if (and text-id (equal cell-id text-id))
(let ((cend (next-single-property-change
next 'ctbl:cell-id nil end)))
(cl-return (funcall func next cend))))
(setq pos next)))
(defun ctbl:find-all-by-row-id (dest row-id func)
"[internal] Call the function FUNC in each regions where the
row-id of the text-property `ctbl:cell-id' is equal to
ROW-ID. The argument function FUNC receives three arguments,
cell-id, begin position and end one. This function is mainly used
at functions for putting overlays."
(cl-loop with pos = (ctbl:find-position-fast dest (cons row-id nil))
with end = (ctbl:dest-point-max dest)
for next = (next-single-property-change pos 'ctbl:cell-id nil end)
for text-id = (and next (ctbl:cursor-to-cell next))
while (and next (< next end)) do
(when text-id
(cond
((equal row-id (car text-id))
(let ((cend (next-single-property-change
next 'ctbl:cell-id nil end)))
(funcall func text-id next cend)))
((< row-id (car text-id))
(cl-return nil))))
(setq pos next)))
(defun ctbl:find-first-cell (dest)
"[internal] Return the first cell in the current buffer."
(let ((pos (next-single-property-change
(ctbl:dest-point-min dest) 'ctbl:cell-id)))
(and pos (ctbl:cursor-to-cell pos))))
(defun ctbl:find-last-cell (dest)
"[internal] Return the last cell in the current buffer."
(let ((pos (previous-single-property-change
(ctbl:dest-point-max dest) 'ctbl:cell-id)))
(and pos (ctbl:cursor-to-cell (1- pos)))))
(defun ctbl:cursor-to-cell (&optional pos)
"[internal] Return the cell-id at the cursor. If the text does not
have the text-property `ctbl:cell-id', return nil."
(get-text-property (or pos (point)) 'ctbl:cell-id))
(defun ctbl:cursor-to-nearest-cell ()
"Return the cell-id at the cursor. If the point of cursor does
not have the cell-id, search the cell-id around the cursor
position. If the current buffer is not table view (it may be
bug), this function may return nil."
(or (ctbl:cursor-to-cell)
(let* ((r (lambda () (when (not (eolp)) (forward-char))))
(l (lambda () (when (not (bolp)) (backward-char))))
(u (lambda () (when (not (bobp)) (line-move 1))))
(d (lambda () (when (not (eobp)) (line-move -1))))
(dest (ctbl:component-dest (ctbl:cp-get-component)))
get)
(setq get (lambda (cmds)
(save-excursion
(if (null cmds) (ctbl:cursor-to-cell)
(ignore-errors
(funcall (car cmds)) (funcall get (cdr cmds)))))))
(or (cl-loop for i in `((,d) (,r) (,u) (,l)
(,d ,r) (,d ,l) (,u ,r) (,u ,l)
(,d ,d) (,r ,r) (,u ,u) (,l ,l))
for id = (funcall get i)
if id return id)
(cond
((> (/ (point-max) 2) (point))
(ctbl:find-first-cell dest))
(t (ctbl:find-last-cell dest)))))))
;; Commands
(defun ctbl:navi-move-gen (drow dcol)
"[internal] Move to the cell with the abstract position."
(let* ((cp (ctbl:cp-get-component))
(cell-id (ctbl:cursor-to-nearest-cell))
(row-id (car cell-id)) (col-id (cdr cell-id)))
(when (and cp cell-id)
(ctbl:navi-goto-cell (ctbl:cell-id (+ drow row-id)
(+ dcol col-id))))))
(defun ctbl:navi-move-up (&optional num)
"Move to the up neighbor cell."
(interactive "p")
(unless num (setq num 1))
(ctbl:navi-move-gen (- num) 0))
(defun ctbl:navi-move-down (&optional num)
"Move to the down neighbor cell."
(interactive "p")
(unless num (setq num 1))
(ctbl:navi-move-gen num 0))
(defun ctbl:navi-move-right (&optional num)
"Move to the right neighbor cell."
(interactive "p")
(unless num (setq num 1))
(ctbl:navi-move-gen 0 num))
(defun ctbl:navi-move-left (&optional num)
"Move to the left neighbor cell."
(interactive "p")
(unless num (setq num 1))
(ctbl:navi-move-gen 0 (- num)))
(defun ctbl:navi-move-left-most ()
"Move to the left most cell."
(interactive)
(let* ((cp (ctbl:cp-get-component))
(cell-id (ctbl:cursor-to-nearest-cell))
(row-id (car cell-id)))
(when (and cp cell-id)
(ctbl:navi-goto-cell (ctbl:cell-id row-id 0)))))
(defun ctbl:navi-move-right-most ()
"Move to the right most cell."
(interactive)
(let* ((cp (ctbl:cp-get-component))
(cell-id (ctbl:cursor-to-nearest-cell))
(row-id (car cell-id))
(model (ctbl:cp-get-model cp))
(cols (ctbl:model-column-length model)))
(when (and cp cell-id)
(ctbl:navi-goto-cell (ctbl:cell-id row-id (1- cols))))))
(defun ctbl:navi-goto-cell (cell-id)
"Move the cursor to CELL-ID and put selection."
(let ((cp (ctbl:cp-get-component)))
(when cp
(ctbl:cp-set-selected-cell cp cell-id))))
(defun ctbl:navi-on-click ()
"Action handler on the cells."
(interactive)
(let ((cp (ctbl:cp-get-component))
(cell-id (ctbl:cursor-to-nearest-cell)))
(when (and cp cell-id)
(ctbl:cp-set-selected-cell cp cell-id)
(ctbl:cp-fire-click-hooks cp))))
(defun ctbl:navi-jump-to-column ()
"Jump to a specified column of the current row."
(interactive)
(let* ((cp (ctbl:cp-get-component))
(cell-id (ctbl:cursor-to-nearest-cell))
(row-id (car cell-id))
(model (ctbl:cp-get-model cp))
(cols (ctbl:model-column-length model))
(col-names (mapcar 'ctbl:cmodel-title
(ctbl:model-column-model model)))
(completion-ignore-case t)
(col-name (funcall ctbl:completing-read "Column name: " col-names)))
(when (and cp cell-id)
(ctbl:navi-goto-cell
(ctbl:cell-id
row-id
(cl-position col-name col-names :test 'equal))))))
(defun ctbl:action-update-buffer ()
"Update action for the latest table model."
(interactive)
(let ((cp (ctbl:cp-get-component)))
(when cp
(ctbl:cp-update cp))))
(defun ctbl:action-column-header ()
"Action handler on the header columns. (for normal key events)"
(interactive)
(ctbl:fire-column-header-action
(ctbl:cp-get-component)
(get-text-property (point) 'ctbl:col-id)))
(defun ctbl:fire-column-header-action (cp col-id)
"[internal] Execute action handlers on the header columns."
(when (and cp col-id)
(cl-loop with cmodel = (nth col-id (ctbl:model-column-model (ctbl:cp-get-model cp)))
for f in (ctbl:cmodel-click-hooks cmodel)
do (condition-case err
(funcall f cp col-id)
(error (message "Ctable: Header Click / Hook error %S [%s]"
f err))))))
(defun ctbl:render-column-header-keymap (col-id)
"[internal] Generate action handler on the header columns. (for header-line-format)"
(let ((col-id col-id))
(let ((keymap (copy-keymap ctbl:column-header-keymap)))
(define-key keymap [header-line mouse-1]
(lambda ()
(interactive)
(ctbl:fire-column-header-action (ctbl:cp-get-component) col-id)))
keymap)))
(defvar ctbl:column-header-keymap
(ctbl:define-keymap
'(([mouse-1] . ctbl:action-column-header)
("C-m" . ctbl:action-column-header)
("RET" . ctbl:action-column-header)
))
"Keymap for the header columns.")
(defvar ctbl:table-mode-map
(ctbl:define-keymap
'(
("k" . ctbl:navi-move-up)
("j" . ctbl:navi-move-down)
("h" . ctbl:navi-move-left)
("l" . ctbl:navi-move-right)
("p" . ctbl:navi-move-up)
("n" . ctbl:navi-move-down)
("b" . ctbl:navi-move-left)
("f" . ctbl:navi-move-right)
("c" . ctbl:navi-jump-to-column)
("e" . ctbl:navi-move-right-most)
("a" . ctbl:navi-move-left-most)
("g" . ctbl:action-update-buffer)
("?" . ctbl:describe-bindings)
([mouse-1] . ctbl:navi-on-click)
("C-m" . ctbl:navi-on-click)
("RET" . ctbl:navi-on-click)
)) "Keymap for the table-mode buffer.")
(defun ctbl:table-mode-map (&optional custom-map)
"[internal] Return a keymap object for the table buffer."
(cond
(custom-map
(set-keymap-parent custom-map ctbl:table-mode-map)
custom-map)
(t ctbl:table-mode-map)))
(defvar ctbl:table-mode-hook nil
"This hook is called at end of setting up major mode `ctbl:table-mode'.")
(defun ctbl:table-mode (&optional custom-map)
"Set up major mode `ctbl:table-mode'.
\\{ctbl:table-mode-map}"
(kill-all-local-variables)
(setq truncate-lines t)
(use-local-map (ctbl:table-mode-map custom-map))
(setq major-mode 'ctbl:table-mode
mode-name "Table Mode")
(setq buffer-undo-list t
buffer-read-only t)
(add-hook 'post-command-hook 'ctbl:start-tooltip-timer nil t)
(run-hooks 'ctbl:table-mode-hook))
;; Rendering
(defun ctbl:render-check-cell-width (rows cmodels column-widths)
"[internal] Return a list of rows. This function makes side effects:
cell widths are stored at COLUMN-WIDTHS, longer cell strings are truncated by
maximum width of the column models."
(cl-loop for row in rows collect
(cl-loop for c in row
for cm in cmodels
for cwmax = (ctbl:cmodel-max-width cm)
for i from 0
for cw = (nth i column-widths)
for val = (format "%s" c)
collect
(progn
(when (and cwmax (< cwmax (string-width val)))
(setq val (truncate-string-to-width val cwmax)))
(when (< cw (string-width val))
(setf (nth i column-widths) (string-width val)))
val))))
(defun ctbl:render-adjust-cell-width (cmodels column-widths total-width)
"[internal] Adjust column widths and return a list of column widths.
If TOTAL-WIDTH is nil, this function just returns COLUMN-WIDTHS.
If TOTAL-WIDTHS is shorter than sum of COLUMN-WIDTHS, this
function expands columns. The residual width is distributed over
the columns. If TOTAL-WIDTHS is longer than sum of
COLUMN-WIDTHS, this function shrinks columns to reduce the
surplus width."
(let ((init-total (cl-loop for i in column-widths sum i)))
(cond
((or (null total-width)
(= total-width init-total)) column-widths)
((< total-width init-total)
(ctbl:render-adjust-cell-width-shrink
cmodels column-widths total-width init-total))
(t
(ctbl:render-adjust-cell-width-expand
cmodels column-widths total-width init-total)))))
(defun ctbl:render-adjust-cell-width-shrink (cmodels column-widths total-width init-total )
"[internal] shrink column widths."
(let* ((column-widths (copy-sequence column-widths))
(column-indexes (cl-loop for i from 0 below (length cmodels) collect i))
(residual (- init-total total-width)))
(cl-loop for cnum = (length column-indexes)
until (or (= 0 cnum) (= 0 residual))
do
(cl-loop with ave-shrink = (max 1 (/ residual cnum))
for idx in column-indexes
for cmodel = (nth idx cmodels)