-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathftw.lisp
463 lines (402 loc) · 17.5 KB
/
ftw.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
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
;;;; Copyright (c) Frank James 2016 <frank.a.james@gmail.com>
;;;; This code is licensed under the MIT license.
;;; This file defines useful utility functions and macros to simplify some common tasks.
(in-package #:ftw)
(defvar *accel* nil
"FTW's global accelerator table.")
(defun set-accelerator-table (&optional entries)
"Destroy the existing accelerator table (if any) and set new table.
ENTRIES ::= new accelerator table to set.
The existing accelerator table is always destroyed. If ENTRIES is non-nil
then a new table is set.
"
(when *accel*
(destroy-accelerator-table *accel*)
(setf *accel* nil))
(when entries
(setf *accel* (create-accelerator-table entries))))
(defun default-message-loop (wndproc &key class-name title width height background icon icon-small styles)
"Standard message loop. Defines a new window class with :arrow cursor and 3d-face background,
creates an overlapped, visible window of this class. Shows, updates and sets this window to
the foreground. Then loops, processing messages, until a WM_QUIT message is received.
Also processes accelerator keys set using SET-ACCELERATOR-TABLE.
"
(let ((cname (or class-name "FTW_MAIN_CLASS")))
(register-class cname
wndproc
:icon icon
:icon-small icon-small
:cursor (load-cursor :arrow)
:background (or background (get-sys-color-brush :3d-face)))
(let ((hwnd (create-window cname
:window-name (or title cname)
:ex-styles (logior-consts +ws-ex-appwindow+)
:styles (or styles
(logior +ws-overlappedwindow+ +ws-visible+))
:x 100 :y 100 :width (or width 400) :height (or height 300)))
(msg (make-msg)))
(unless hwnd (return-from default-message-loop nil))
(show-window hwnd)
(update-window hwnd)
(set-foreground-window hwnd)
(do ((done nil))
(done)
(let ((r (get-message msg)))
(cond
((zerop r) (setf done t))
((or (null *accel*)
(zerop (translate-accelerator hwnd *accel* msg)))
(translate-message msg)
(dispatch-message msg))))))))
(defun default-message-loop-multiple (wndproc &key class-name title width height background icon icon-small styles handle-procs)
"Message loop with multiple handles. Defines a new window class with :arrow cursor and 3d-face background,
creates an overlapped, visible window of this class. Shows, updates and sets this window to
the foreground. Then loops, processing messages, until a WM_QUIT message is received.
Also processes accelerator keys set using SET-ACCELERATOR-TABLE.
"
(let ((cname (or class-name "FTW_MAIN_CLASS"))
(handles (mapcar #'first handle-procs))
(procs (mapcar #'second handle-procs)))
(register-class cname
wndproc
:icon icon
:icon-small icon-small
:cursor (load-cursor :arrow)
:background (or background (get-sys-color-brush :3d-face)))
(let ((hwnd (create-window cname
:window-name (or title cname)
:ex-styles (logior-consts +ws-ex-appwindow+)
:styles (or styles
(logior +ws-overlappedwindow+ +ws-visible+))
:x 100 :y 100 :width (or width 400) :height (or height 300)))
(msg (make-msg)))
(unless hwnd (return-from default-message-loop-multiple nil))
(show-window hwnd)
(update-window hwnd)
(set-foreground-window hwnd)
(do ((done nil))
(done)
(let ((sts (msg-wait-for-multiple-objects :handles handles :timeout 500)))
(cond
((= sts (length handles))
;; messages pending
(do ((b (peek-message msg :error-p nil) (peek-message msg :error-p nil)))
((or (not b) done))
(let ((r (get-message msg)))
(cond
((zerop r) (setf done t))
((or (null *accel*)
(zerop (translate-accelerator hwnd *accel* msg)))
(translate-message msg)
(dispatch-message msg))))))
((and (>= sts 0) (< sts (length handles)))
;; handle signaled - invoke specified callback
(apply (nth sts procs)
(cddr (nth sts handle-procs))))))))))
(defun message-poll (&optional timeout)
"Wait for messages to be available in the message queue."
(msg-wait-for-multiple-objects :timeout timeout
:mask (logior-consts +qs-allevents+)))
(defun create-bitmap-resource (width height planes bits-per-pixel data)
"Create a colour bitmap. Data should be a vector of (unsigned-byte 8) of
the correct length and alignment for the colour data."
(let ((bm (create-dib-section nil width height planes bits-per-pixel)))
(set-di-bits nil bm width height planes bits-per-pixel data)
bm))
(defun generate-bitmap-resource (filename &optional (stream *standard-output*) name)
"Parse a bitmap file and generate Lisp code so that the resource can be embedded
within a Lisp program rather than having to deliver the image separately.
The function prints out the code to be inserted into your project.
FILENAME ::= path to a .bmp bitmap file on your system.
This function loads the data and parses it for width/height information. It then
prints out a Lisp form which should be pasted into your code for use as a bitmap handle.
This allows the programmer to embed images without having to deliver them as separate files.
"
(with-open-file (f filename :direction :input :element-type '(unsigned-byte 8))
(let ((len (file-length f)))
(let ((bmp (make-array len :element-type '(unsigned-byte 8))))
(read-sequence bmp f)
;; extract dimensions from header -- see MSDN page for more info on bitmap stuctures
;; https://msdn.microsoft.com/en-us/library/windows/desktop/dd183391(v=vs.85).aspx
(let ((offset (nibbles:ub32ref/le bmp 10))
(width (nibbles:sb32ref/le bmp 18))
(height (nibbles:sb32ref/le bmp 22))
(planes (nibbles:ub16ref/le bmp 26))
(bits-per-pixel (nibbles:ub16ref/le bmp 28)))
(format stream "(defvar ~A~%" (or name "NAME"))
(format stream " (create-bitmap-resource ~A ~A ~A ~A~%#( "
width height planes bits-per-pixel)
;; bitmap stores it as aa rr gg bb
;; we want it as bb gg rr aa
;; BUT: we need to use premultiplied alpha
(do ((i 0 (+ i 4)))
((= i (- (length bmp) offset)))
(let ((aa (aref bmp (+ offset i 0)))
(bb (aref bmp (+ offset i 1)))
(gg (aref bmp (+ offset i 2)))
(rr (aref bmp (+ offset i 3))))
(setf (aref bmp (+ offset i 0))
(truncate (* bb aa) #xff)
(aref bmp (+ offset i 1))
(truncate (* gg aa) #xff)
(aref bmp (+ offset i 2))
(truncate (* rr aa) #xff)
(aref bmp (+ offset i 3))
aa)))
(dotimes (i (- (length bmp) offset))
(when (and (not (zerop i)) (zerop (mod i 16)))
(when (zerop (mod i 256))
(terpri stream))
(format stream "~% "))
(when (zerop (mod i 4))
(format stream " "))
(format stream "#x~2,'0X " (aref bmp (+ offset i))))
(format stream ")))~%"))
nil))))
(defun get-default-font ()
"Returns the default system message font."
(create-font-indirect (nonclientmetrics-message-font (system-parameters-info (const +spi-getnonclientmetrics+)))))
(defun set-default-font (hwnd &optional font)
"Send a WM_SETFONT message to the window with the specified font.
If FONT is not specified, the default system message font is used.
"
(send-message hwnd (const +wm-setfont+) (or font (get-default-font)) 0))
(defun generate-icon-resource (filename &optional (stream *standard-output*) name)
"Generate Lisp code for a given icon so that it can be embedded into
Lisp code. This means you don't have to deliver the icon file separately.
This is equivalent to the .rc resources you link with when writing C.
FILENAME ::= path to a .ico file containing the icon you want to use.
Prints out code which should be included into your project.
"
(with-open-file (f filename :direction :input :element-type '(unsigned-byte 8))
(let ((len (file-length f)))
(let ((ico (make-array len :element-type '(unsigned-byte 8))))
(read-sequence ico f)
;; icon file header -- see wikipedia entry for details
(let ((type (nibbles:ub16ref/le ico 2))
(width (aref ico 6))
(height (aref ico 7))
(planes (nibbles:ub16ref/le ico 10))
(bits-per-pixel (nibbles:ub16ref/le ico 12))
;; (size (nibbles:ub32ref/le ico 14))
(offset (+ (nibbles:ub32ref/le ico 18) 40)))
(unless (= type 1) (error "Expected type 2 got ~A" type))
(format stream "(defvar ~A~%" (or name "NAME"))
(format stream " (create-icon ~A ~A ~A ~A~%"
width height planes bits-per-pixel)
(format stream " (make-array ~A :element-type '(unsigned-byte 8))~%"
(- (length ico) offset))
(format stream " #(")
(dotimes (i (- (length ico) offset))
(when (and (not (zerop i)) (zerop (mod i 16)))
(format stream "~% "))
(format stream "#x~2,'0X " (aref ico (+ offset i))))
(format stream ")))~%"))
nil))))
(defun generate-cursor-resource (filename &optional (stream *standard-output*) name)
"Generate Lisp code for a given cursor so that it can be embedded into
Lisp code. This means you don't have to deliver the cursor file separately.
This is equivalent to the .rc resources you link with when writing C.
FILENAME ::= path to a .cur file containing the cursor you want to use.
Prints out code which should be included into your project.
"
(with-open-file (f filename :direction :input :element-type '(unsigned-byte 8))
(let ((len (file-length f)))
(let ((ico (make-array len :element-type '(unsigned-byte 8))))
(read-sequence ico f)
;; icon file header -- see wikipedia entry for details
(let ((type (nibbles:ub16ref/le ico 2))
(width (aref ico 6))
(height (aref ico 7))
(x (nibbles:ub16ref/le ico 10))
(y (nibbles:ub16ref/le ico 12))
;; (size (nibbles:ub32ref/le ico 14))
(offset (+ (nibbles:ub32ref/le ico 18) 40)))
(unless (= type 2) (error "Expected type 2 got ~A" type))
(format stream "(defvar ~A~%" (or name "NAME"))
(format stream " (create-cursor ~A ~A ~A ~A~%"
x y width height )
(format stream " (make-array ~A :element-type '(unsigned-byte 8))~%"
(- (length ico) offset))
(format stream " #(")
(dotimes (i (- (length ico) offset))
(when (and (not (zerop i)) (zerop (mod i 16)))
(format stream "~% "))
(format stream "#x~2,'0X " (aref ico (+ offset i))))
(format stream ")))~%"))
nil))))
(defun generate-resource-file (filename resources &key package)
(with-open-file (stream filename :direction :output :if-exists :supersede)
(format stream "~%")
(format stream "(in-package #:~A)~%" (or package (package-name *package*)))
(format stream "~%")
(dolist (resource resources)
(destructuring-bind (res-type &rest res-args) resource
(ecase res-type
(:icon
(destructuring-bind (name icon-filename) res-args
(generate-icon-resource icon-filename stream name)))
(:cursor
(destructuring-bind (name cursor-filename) res-args
(generate-cursor-resource cursor-filename stream name)))
(:bitmap
(destructuring-bind (name bitmap-filename) res-args
(generate-bitmap-resource bitmap-filename stream name))))))
(format stream "~%")))
(defun get-client-size (hwnd)
"Get width and height of the hwnd. Returns (values width height)."
(let ((r (get-client-rect hwnd)))
(values (getf r :right 0)
(getf r :bottom 0))))
(defun add-menu-bar (hwnd menus)
"Add menu bar to the window.
MENUS ::= MENU*
MENU ::= type flags &key name id children
where
TYPE ::= :menu | :item | :check | :radio
FLAGS ::= list of flags to be passed to append-menu
NAME ::= string naming the item
ID ::= integer identifier
CHILDREN ::= MENU* menu children
"
;; Example
;; (add-menu-bar `((:menu (:popup) :name "&File"
;; :children
;; ((:item (:string)
;; :name ,(format nil "&Find~ACtrl+F" #\tab)
;; :id 1)
;; (:item (:separator))
;; (:item (:string)
;; :name ,(format nil "&Quit~ACtrl+Q" #\tab)
;; :id 2)))))
(labels ((process-menu (parent menu)
(destructuring-bind (type flags &key name id children) menu
(ecase type
(:menu
(let ((m (create-menu)))
(dolist (child children)
(process-menu m child))
(append-menu parent flags m name)))
(:item
(append-menu parent flags (or id 0) name))
(:check
(check-menu-item parent (or id 0) (member :checked flags)))
(:radio
(check-menu-radio-item parent
(first flags) (second flags)
(or id 0)))))))
(let ((bar (create-menu)))
(dolist (menu menus)
(process-menu bar menu))
(set-menu hwnd bar))))
(defun set-window-to-center (hwnd)
(let ((rect (get-window-rect hwnd)))
(destructuring-bind (&key (left 0) (right 0) (top 0) (bottom 0)) rect
(set-window-pos hwnd
:topmost
(truncate (- (get-system-metrics :cx-screen)
(- right left))
2)
(truncate (- (get-system-metrics :cy-screen)
(- bottom top))
2)
0
0
'(:no-size)))))
(defmacro with-double-buffering ((var hwnd) &body body)
"Evaluate body in a WITH-PAINT context where VAR is bound to an in-memory HDC
which is blitted onto the hwnd's DC as the final step. This prevents flickering
when drawing lots of small items on the screen."
(alexandria:with-gensyms (gbm gold gwidth gheight ghdc gps)
`(with-paint (,hwnd ,ghdc ,gps)
(let ((,gwidth (rect-right (paintstruct-paint ,gps)))
(,gheight (rect-bottom (paintstruct-paint ,gps))))
(with-compatible-dc (,var ,ghdc)
(let* ((,gbm (create-compatible-bitmap ,ghdc ,gwidth ,gheight))
(,gold (select-object ,var ,gbm)))
(unwind-protect (progn ,@body)
(bit-blt ,ghdc 0 0 ,var 0 0
:width ,gwidth
:height ,gheight
:raster-op :srccopy)
(select-object ,var ,gold)
(delete-object ,gbm))))))))
(defmacro with-printer-dc ((var device-name &optional document-name) &body body)
"Evaluate the body in a context with VAR bound to an HDC for the printer named by DEVICE-NAME.
The body should consist of a series of PRINT-PAGE forms. Any other forms in body are evaulated but
do not contribute to the page to be printed.
For examples see examples/printer.
"
`(let ((,var (create-dc ,device-name)))
(unwind-protect
(macrolet ((print-page (&body body)
`(progn (start-page ,',var)
,@body
(end-page ,',var))))
(start-doc ,var ,(or document-name "Document"))
,@body
(end-doc ,var))
(delete-dc ,var))))
(defun create-static (text &key parent styles font x y width height)
(let ((h (create-window :static
:window-name text
:styles (cond
(styles styles)
(parent (logior ftw::+ws-visible+ ftw::+ws-child+)))
:x x :y y :width width :height height
:parent parent)))
(set-default-font h font)
h))
(defun create-edit (&key text parent styles font x y width height)
(let ((h (create-window :edit
:window-name (or text "")
:styles (cond
(styles styles)
(parent (logior ftw::+ws-visible+ ftw::+ws-child+)))
:ex-styles (logior ftw::+ws-ex-clientedge+)
:x x :y y :width width :height height
:parent parent)))
(set-default-font h font)
h))
(defun create-button (text &key parent styles font x y width height menu)
(let ((h (create-window :button
:window-name text
:styles (cond
(styles styles)
(parent (logior ftw::+ws-visible+ ftw::+ws-child+)))
:x x :y y :width width :height height
:parent parent
:menu menu)))
(set-default-font h font)
h))
(defparameter *hwnd-list* (make-hash-table))
(defun register-hwnd (name hwnd &optional (id 0))
(setf (gethash name *hwnd-list*)
(list hwnd id))
name)
(defun unregister-hwnd (name &rest more-names)
(let ((nlist (cons name more-names)))
(dolist (n nlist)
(remhash n *hwnd-list*)))
nil)
(defun hwnd-by-name (name)
(let ((n (gethash name *hwnd-list*)))
(first n)))
(defun hwnd-by-id (id)
(maphash (lambda (key n)
(declare (ignore key))
(when (= (second n) id)
(return-from hwnd-by-id (first n))))
*hwnd-list*)
nil)
(defun hwnd-name-by-id (id)
(maphash (lambda (key n)
(when (= (second n) id)
(return-from hwnd-name-by-id key)))
*hwnd-list*))
(defun hwnd-list ()
(let ((hlist nil))
(maphash (lambda (key n)
(push (cons key n) hlist))
*hwnd-list*)
hlist))