-
Notifications
You must be signed in to change notification settings - Fork 6
/
text-selection.lisp
432 lines (367 loc) · 18.4 KB
/
text-selection.lisp
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
;;; -*- Mode: Lisp; Syntax: Common-Lisp; Package: CLIMI; -*-
;;; (c) copyright 2003 by Gilbert Baumann
;;; This library is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU Library General Public
;;; License as published by the Free Software Foundation; either
;;; version 2 of the License, or (at your option) any later version.
;;;
;;; This library 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
;;; Library General Public License for more details.
;;;
;;; You should have received a copy of the GNU Library General Public
;;; License along with this library; if not, write to the
;;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;;; Boston, MA 02111-1307 USA.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Cut and Paste
(in-package :climi)
;;;; Interaction implemented:
;; Shift-Mouse-L down: clear active selection and set the first point
;; Shift-Mouse-L drag: drag the second point
;; Shift-Mouse-L up: set the second point
;; Shift-Mouse-R down: pick the nearest point, if any
;; Shift-Mouse-R drag: drag said point
;; Shift-Mouse-R up: leave said point where it was dragged to.
;; Shift-Mouse-M: paste
;;;; Interaction to implement:
;; Shift-Mouse-L single click: (maybe) select current presentation, if any.
;; Shift-Mouse-L double click: select word
;; Shift-Mouse-L triple click: select "line".
;; TODO:
;; * Editor input (both active and old) is not currently highlighted.
;; * Selecting large regions gets slow.
;; * Structure of line breaks in the original text is not preserved (CLIM/McCLIM design issue)
;;;; Preferences
(defparameter *marking-border* 1)
(defparameter *marked-foreground* +white+
"Foreground ink to use for marked stuff.")
(defparameter *marked-background* +blue4+
"Background ink to use for marked stuff.")
;;;; Text Selection Protocol
(defgeneric release-selection (port &optional time)
(:documentation "Relinquish ownership of the selection."))
(defgeneric request-selection (port requestor time)
(:documentation "Request that the window system retrieve the selection from
its current owner. This should cause a selection-notify-event to be delivered."))
(defgeneric bind-selection (port window &optional time)
(:documentation "Take ownership of the selection."))
(defgeneric send-selection (port request-event string)
(:documentation "Send 'string' to a client in response to a selection-request-event."))
(defgeneric get-selection-from-event (port event)
(:documentation "Given a selection-notify event, return a string containing
the incoming selection."))
;; These events are probably very X11 specific.
;; Backends will likely produce subclasses of selection-notify-event
;; and selection-request-event.
(defclass selection-event (window-event)
((selection :initarg :selection
:reader selection-event-selection)))
(defclass selection-clear-event (selection-event) ())
(defclass selection-notify-event (selection-event) ())
(defclass selection-request-event (selection-event)
((requestor :initarg :requestor :reader selection-event-requestor)))
;;;; Random Notes
;; - McCLIM still has absolutely no idea of lines.
(defclass marking ()
()
(:documentation "A common super class for markings (= stuff marked)."))
(defgeneric marking-region (stream marking)
(:documentation "Region marked/affected."))
(defclass string-marking (marking)
((record :initarg :record
:documentation "The text output record this belongs to.")
(styled-string :initarg :styled-string
:documentation "The styled string sub-record of 'record'.")
(start :initarg :start :reader mark-start
:documentation "Start index within string.")
(end :initarg :end :reader mark-end
:documentation "End index within string. Caution: Could be one off the end to indicate a newline implied."))
(:documentation "Some part of a styled-string marked."))
(defmethod marking-region (stream (marking string-marking))
(with-slots (record styled-string start end) marking
(with-slots (baseline start-y) record
(if (= start end)
+nowhere+
(with-slots (start-x string text-style) styled-string
(make-rectangle* (+ start-x
(stream-string-width stream string
:start 0 :end start
:text-style text-style)
(- *marking-border*))
(+ start-y baseline
(- (text-style-ascent text-style stream))
(- *marking-border*))
(+ start-x
(stream-string-width stream string
:start 0 :end end
:text-style text-style)
*marking-border*)
(+ start-y baseline (text-style-descent text-style stream)
*marking-border*)))))))
;(defgeneric draw-marking (medium marking)
; (:documentation "Draw the marking to medium."))
;
;(defmethod draw-marking (stream (marking string-marking))
; (draw-design (sheet-medium stream) (marking-region marking)
; :ink +flipping-ink+))
;;;;
(defclass cut-and-paste-mixin ()
((markings :initform nil)
(point-1-x :initform nil)
(point-1-y :initform nil)
(point-2-x :initform nil)
(point-2-y :initform nil)
(dragging-p :initform nil)))
(defclass paste-as-keypress-mixin ()
()
(:documentation "Implements the old McCLIM behavior of pasting via a
sequence of key press events. You couldn't possibly want this."))
(defmethod handle-repaint :around ((pane cut-and-paste-mixin) region)
(with-slots (markings) pane
(cond ((null markings)
(call-next-method))
(t
(let ((marked-region
(reduce #'region-union (mapcar #'(lambda (x) (marking-region pane x)) (slot-value pane 'markings))
:initial-value +nowhere+)))
(with-sheet-medium (medium pane)
(let ((R (region-difference region marked-region)))
(with-drawing-options (medium :clipping-region R)
(call-next-method pane R))))
(with-sheet-medium (medium pane)
(let ((R (region-intersection region marked-region)))
(with-drawing-options (medium :clipping-region R)
(letf (((medium-foreground medium) *marked-foreground*)
((medium-background medium) *marked-background*))
(call-next-method pane R))))))))))
(defmethod dispatch-event :around ((pane cut-and-paste-mixin)
(event pointer-button-press-event))
(if (eql (event-modifier-state event) +shift-key+)
(eos/shift-click pane event)
(call-next-method)))
(defmethod dispatch-event :around ((pane cut-and-paste-mixin)
(event pointer-button-release-event))
(if (eql (event-modifier-state event) +shift-key+)
(eos/shift-release pane event)
(call-next-method)))
(defmethod dispatch-event :around ((pane cut-and-paste-mixin)
(event pointer-motion-event))
(with-slots (point-1-x dragging-p) pane
(if (and (eql (event-modifier-state event) +shift-key+))
(when dragging-p (eos/shift-drag pane event))
(call-next-method))))
(defun pane-clear-markings (pane &optional time)
(repaint-markings pane (slot-value pane 'markings)
(setf (slot-value pane 'markings) nil))
(release-selection (port pane) time))
(defmethod eos/shift-click ((pane extended-output-stream) event)
(with-slots (point-1-x point-1-y point-2-x point-2-y dragging-p) pane
(cond ((eql +pointer-left-button+ (pointer-event-button event))
(pane-clear-markings pane (event-timestamp event))
;; start dragging, set point-1 where the mouse is
(setf point-1-x (pointer-event-x event))
(setf point-1-y (pointer-event-y event))
(setf dragging-p t))
((eql +pointer-middle-button+ (pointer-event-button event))
;; paste
(request-selection (port pane) #|:UTF8_STRING|# (sheet-direct-mirror pane) (event-timestamp event)))
((eql +pointer-right-button+ (pointer-event-button event))
(when (and point-1-x point-1-y point-2-x point-2-y)
;; If point-1 and point-2 are set up pick the nearest (what metric?) and drag it around.
(when (< (+ (expt (- (pointer-event-x event) point-1-x) 2)
(expt (- (pointer-event-y event) point-1-y) 2))
(+ (expt (- (pointer-event-x event) point-2-x) 2)
(expt (- (pointer-event-y event) point-2-y) 2)))
(rotatef point-1-x point-2-x)
(rotatef point-1-y point-2-y))
(eos/shift-drag pane event)
(setf dragging-p t)))
(t (describe event)))))
(defmethod eos/shift-release ((pane extended-output-stream) event)
(with-slots (point-1-x point-1-y point-2-x point-2-y dragging-p) pane
(when dragging-p
(setf point-2-x (pointer-event-x event)
point-2-y (pointer-event-y event)
dragging-p nil)
;;
(let ((owner (selection-owner (port pane))))
(when (and owner (not (eq owner pane)))
(distribute-event (port pane)
(make-instance 'selection-clear-event
:sheet owner
:selection :primary))))
(when (bind-selection (port pane) pane (event-timestamp event))
(setf (selection-owner (port pane)) pane)
(setf (selection-timestamp (port pane)) (event-timestamp event))))))
(defun repaint-markings (pane old-markings new-markings)
(let ((old-region (reduce #'region-union (mapcar #'(lambda (x) (marking-region pane x)) old-markings)
:initial-value +nowhere+))
(new-region (reduce #'region-union (mapcar #'(lambda (x) (marking-region pane x)) new-markings)
:initial-value +nowhere+)))
(handle-repaint pane (region-exclusive-or old-region new-region))))
(defmethod eos/shift-drag ((pane extended-output-stream) event)
(with-slots (point-1-x point-1-y) pane
(let ((old-markings (slot-value pane 'markings)))
(setup-marked-extents pane (stream-output-history pane) +everywhere+
point-1-x point-1-y
(pointer-event-x event)
(pointer-event-y event))
(repaint-markings pane old-markings (slot-value pane 'markings)))))
(defun map-over-text (record function)
(cond ((typep record 'standard-text-displayed-output-record)
(with-slots (strings baseline max-height start-y wrapped x1 y1) record
(loop for substring in strings do
(with-slots (start-x string marked-extent text-style) substring
(funcall function start-x (+ start-y baseline) string text-style
substring record)))))
(t
(map-over-output-records-overlapping-region
#'(lambda (x)
(map-over-text x function))
record +everywhere+))))
(defun setup-marked-extents (stream record region bx1 by1 bx2 by2)
(declare (ignore region))
(when (> by1 by2)
(rotatef by1 by2)
(rotatef bx1 bx2))
(let ((*lines* nil)
(*all-lines* nil))
(map-over-text record
(lambda (x y string ts record full-record)
(let ((q (assoc y *lines*)))
(unless q
(push (setf q (cons y nil)) *lines*))
(push (list x y string ts record full-record)
(cdr q)))
(force-output *trace-output*)))
(setf *lines*
(sort (mapcar (lambda (line)
(cons (car line)
(sort (cdr line) #'< :key #'first)))
*lines*)
#'< :key #'car))
(setf *all-lines* *lines*)
;; Nuke every line that is above by1
(setf *lines* (remove-if (lambda (line) (< (+ (car line) 3) by1)) *lines*))
;; Also nuke all that are below by2
(setf *lines* (remove-if (lambda (line) (> (- (car line) 10) by2)) *lines*))
;; Special case:
(when (= 1 (length *lines*))
(psetf bx1 (min bx1 bx2)
bx2 (max bx1 bx2)))
;; Then, in the first line find the index farthest to the right
;; which is still less than bx1.
(let ((start-i 0)
(start-record (fifth (cadar *lines*)))
(end-i 0)
(end-record (fifth (cadar (last *lines*)))))
(loop for chunk in (cdr (first *lines*)) do
(destructuring-bind (x y string ts record full-record) chunk
(declare (ignorable x y string ts record full-record))
(loop for i to (length string) do
(when (< (+ x (stream-string-width stream string :start 0 :end i :text-style ts))
bx1)
(setf start-i i
start-record record)))))
;; Finally in the last line find the index farthest to the left
;; which still is greater than bx2. Or put differently: Search
;; from the left and while we are still in bounds maintain end-i
;; and end-record.
(loop for chunk in (cdr (car (last *lines*))) do
(destructuring-bind (x y string ts record full-record) chunk
(declare (ignorable x y string ts record full-record))
(loop for i to (length string) do
(when (< (+ x (stream-string-width stream string :start 0 :end i :text-style ts))
bx2)
(setf end-i i
end-record record)))))
;; Now grovel over the records, in order ...
(let ((in-p nil)
(marks nil))
(labels ((visit (chunk)
(destructuring-bind (x y string ts record full-record) chunk
(declare (ignorable x y string ts record full-record))
(let ((marked-extent nil))
(cond ((eq record start-record)
(cond ((eq record end-record)
(setf marked-extent
(cons start-i end-i)))
(t
(setf marked-extent
(cons start-i (length string)))
(setf in-p t))))
((eq record end-record)
(setf marked-extent
(cons 0 end-i))
(setf in-p nil))
(t
(setf marked-extent
(if in-p
(cons 0 (length string))
nil))) )
(when marked-extent
(push (destructuring-bind (x y string ts record full-record) chunk
(declare (ignorable x y string ts record full-record))
(make-instance 'string-marking
:record full-record
:styled-string record
:start (car marked-extent)
:end (cdr marked-extent)))
marks)) ))))
(loop for line in *all-lines* do
(loop for chunk in (cdr line) do
(visit chunk)) )
(setf (slot-value stream 'markings) (reverse marks)))))))
;;;; Selections Events
(defmethod dispatch-event :around ((pane cut-and-paste-mixin)
(event selection-clear-event))
(pane-clear-markings pane (event-timestamp event)))
(defmethod dispatch-event :around ((pane cut-and-paste-mixin)
(event selection-request-event))
(send-selection (port pane) event (fetch-selection pane)))
(define-condition selection-notify ()
((event :reader event-of :initarg :event)))
(defmethod handle-event ((pane cut-and-paste-mixin)
(event selection-notify-event))
(signal 'selection-notify :event event))
(defmethod dispatch-event :around ((pane paste-as-keypress-mixin)
(event selection-notify-event))
(let ((matter (get-selection-from-event (port pane) event)))
(loop for c across matter do
(dispatch-event pane
(make-instance 'key-press-event
:timestamp (event-timestamp event)
:sheet pane
:modifier-state 0
:x 0 :y 0 :graft-x 0 :graft-y 0
:key-name nil
:key-character c)))))
;; FIXME: Non-text target conversions.. (?)
(defun fetch-selection (pane)
(let (old-y2 old-x2)
(with-output-to-string (bag)
(map nil
(lambda (m)
(with-slots (record styled-string start end) m
(with-standard-rectangle*
(:x1 x1 :x2 x2 :y1 y1 :y2 y2) record
(cond ((and old-y2 (>= y1 old-y2))
(setf old-y2 nil
old-x2 0 ;<-- ### we should use the minimum of all x1 coordinates.
)
(terpri bag))
(t
(setf old-y2 (max y2 (or old-y2 y2)))))
(when old-x2
(loop repeat (round
(- x1 old-x2)
(text-style-width (slot-value styled-string 'text-style)
pane))
do
(princ " " bag)))
(setf old-x2 x2)
(princ (subseq (styled-string-string styled-string) start end) bag))))
(slot-value pane 'markings)))))