chiark / gitweb /
Work in progress.
[jlisp] / ui-swing.lisp
CommitLineData
fc7489de
MW
1;;; -*-lisp-*-
2;;;
3;;; Pleasant Lisp interface to Swing functions
4;;;
5;;; (c) 2007 Mark Wooding
6;;;
7
8;;;----- Licensing notice ---------------------------------------------------
9;;;
10;;; This program is free software; you can redistribute it and/or modify
11;;; it under the terms of the GNU General Public License as published by
12;;; the Free Software Foundation; either version 2 of the License, or
13;;; (at your option) any later version.
14;;;
15;;; This program is distributed in the hope that it will be useful,
16;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;;; GNU General Public License for more details.
19;;;
20;;; You should have received a copy of the GNU General Public License
21;;; along with this program; if not, write to the Free Software Foundation,
22;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23
24(in-package #:dep-ui)
25
26;;;--------------------------------------------------------------------------
27;;; Utilities.
28
29(defun listify (thing)
30 "Answer THING if it's a list, else a singleton list containing THING."
31 (if (listp thing) thing (list thing)))
32
33;;;--------------------------------------------------------------------------
34;;; Basic stuff.
35
36(defclass widget ()
37 ((java :reader widget-java :initarg :java)))
38
39(defmethod widget-java ((widget t)) widget)
40
41(defmethod widget-insets ((widget t)) 2)
42
43;;;--------------------------------------------------------------------------
44;;; Grid-bag constraints.
45
46(defun make-insets (&rest arguments)
47 "Return a java.awt.*insets object from the given ARGUMENTS. The forms
48 accepted are:
49
50 * (make-insets) -> (0, 0, 0, 0)
51
52 * (make-insets N) -> (N, N, N, N)
53
54 * (make-insets &key :left :right :top :bottom) -> obvious thing"
55 (apply #'make :java.awt.*insets
56 (cond ((null arguments) '(0 0 0 0))
57 ((and (endp (cdr arguments))
58 (integerp (car arguments)))
59 (make-list 4 :initial-element (car arguments)))
60 (t (destructuring-bind (&key (left 0) (right 0) (top 0)
61 (bottom 0)) arguments
62 (list top left bottom right))))))
63
64(defun make-grid-bag-constraints
65 (&key grid-x grid-y grid-width grid-height weight-x weight-y
66 anchor fill insets internal-pad-x internal-pad-y)
67 "Return a java.awt.*grind-bag-constraints object. Arguments may be as
68 follows.
69
70 * GRID-X, GRID-Y -- an integer or :relative [default :relative]
71
72 * GRID-WIDTH, GRID-HEIGHT -- an integer, :relative or :remainder
73 [default 1]
74
75 * WEIGHT-X, WEIGHT-Y -- a float in [0, 1] [default 0.0]
76
77 * ANCHOR -- one of :center, :north, :northeast :northwest, :west, :east,
78 :south, :southwest, :southeast, :page-start, :line-start, :line-end,
79 :page-end, :last-line-start, :last-line-end, :first-line-start,
80 :first-line-end [default :center]
81
82 * FILL -- one of :none, :horizontal, :vertical, :both [default :none]
83
84 * INSETS -- something acceptable to make-insets (q.v.) [default 0]
85
86 * INTERNAL-PAD-X, INTERNAL-PAD-Y -- integers [default 0]"
87
88 (flet ((magic (x)
89 (if (keywordp x)
90 (magic-constant-case (x :java.awt.*grid-bag-constraints)
91 :first-line-start :first-line-end
92 :page-start :line-start :line-end :page-end
93 :last-line-start :last-line-end
94 :none :both :horizontal :vertical
95 :relative :remainder
96 :northwest :north :northeast
97 :west :center :east
98 :southwest :south :southeast)
99 x)))
100 (make :java.awt.*grid-bag-constraints
101 (magic (or grid-x :relative)) (magic (or grid-y :relative))
102 (magic (or grid-width 1)) (magic (or grid-height 1))
103 (or weight-x 0.0) (or weight-y 0.0)
104 (magic (or anchor :center)) (magic (or fill :none))
105 (apply #'make-insets (listify insets))
106 (or internal-pad-x 0) (or internal-pad-y 0))))
107
108;;;--------------------------------------------------------------------------
109;;; Colours.
110
111(let ((builtin-colours (make-hash-table)))
112
113 ;; Build a table of standard Java colours.
114 (dolist (colour '(:black :blue :cyan :dark-gray :gray :green :light-gray
115 :magenta :orange :pink :red :white :yellow))
116 (setf (gethash colour builtin-colours)
117 (class-field :java.awt.*color
118 (substitute #\_ #\- (string-upcase colour)))))
119
120 (defun make-colour (&rest arguments)
121 "Return a newly constructed colour object.
122
123 The ARGUMENTS may be one of the following.
124
125 * nil -- return a null reference, rather than a colour.
126
127 * JAVA-OBJECT -- return the JAVA-OBJECT unmolested.
128
129 * KEYWORD -- return the standard colour named by KEYWORD.
130
131 * STRING -- return the Java colour named by STRING.
132
133 * RGB &optional ALPHAP -- interpret the integer RGB as a 3-byte packed
134 RGB triple (logior (ash RED 16) (ash GREEN 8) (ash BLUE 0)); if
135 ALPHA-P is nil (the default) then apply full alpha; if it's t, then
136 read alpha from byte 3 of RGB; otherwise it's a raw alpha value (see
137 below).
138
139 * RED GREEN BLUE &optional (ALPHA 1.0) -- each of the RED, GREEN, BLUE
140 and ALPHA arguments is a number, either an integer in [0, 256)"
141 (let ((indicator (car arguments)))
142 (etypecase indicator
143 (null java-null)
144 (java-object indicator)
145 (keyword
146 (or (gethash indicator builtin-colours)
147 (error "Colour ~S not found." indicator)))
148 (string
149 (send-class :java.awt.*color :get-color indicator))
150 (number
151 (multiple-value-bind (red green blue alpha)
152 (if (and (integerp indicator) (null (cddr arguments)))
153 (destructuring-bind (rgb &key alpha) arguments
154 (values (ldb (byte 8 16) rgb)
155 (ldb (byte 8 8) rgb)
156 (ldb (byte 8 0) rgb)
157 (case alpha
158 ((t) (ldb (byte 8 24) rgb))
159 ((nil) 255)
160 (t alpha))))
161 (destructuring-bind (r g b &optional (a 1.0)) arguments
162 (values r g b a)))
163 (flet ((fixup (n)
164 (if (integerp n) n (round (* n 255)))))
165 (make :java.awt.*color
166 (fixup red)
167 (fixup green)
168 (fixup blue)
169 (fixup alpha)))))))))
170
171;;;--------------------------------------------------------------------------
172;;; Text fields.
173
174(defun make-text-field (&key readonly notify)
175 "Construct and reutrn a text entry field.
176
177 If READONLY is non-nil then don't allow user edits. If NOTIFY is non-nil,
178 then assume that it's a function of one argument, and call (funcall NOTIFY
179 FIELD) when the field's contents are changed."
180 (let ((field (make :javax.swing.*j-text-field)))
181 (when readonly
182 (send field :set-editable java-false))
183 (when notify
184 (flet ((kick (&optional ev)
185 (declare (ignore ev))
186 (funcall notify field)))
187 (send (send field :get-document) :add-document-listener
188 (jinterface-implementation
189 (java-name :javax.swing.event.*document-listener)
190 (java-name :insert-update) #'kick
191 (java-name :remove-update) #'kick
192 (java-name :changed-update) #'kick))
193 (kick)))
194 field))
195
196(defun field-text (field)
197 "Return the contents of the text field FIELD. This is a SETF-able place."
198 (send field :get-text))
199(defun (setf field-text) (text field)
200 "Modify the contents of the text field FIELD."
201 (send field :set-text text))
202
203(let ((good-colour
204 (send (make :javax.swing.*j-text-field) :get-background))
205 (bad-colour (make-colour 1.0 0.4 0.4)))
206 (defun set-field-highlight (field highlight)
207 "Highlight the text field FIELD according to HIGHLIGHT.
208
209 The HIGHLIGHT may currently be :good or :bad."
210 (send field :set-background (ecase highlight
211 (:good good-colour)
212 (:bad bad-colour)))))
213
214;;;--------------------------------------------------------------------------
215;;; Labels.
216
217(defun make-label (string)
218 "Create and return a label widget showing the STRING.
219
220 If an ampersand appears in the string, underline the following letter."
221 (let* ((amp (position #\& string))
222 (text (if amp
223 (concatenate 'string
224 (subseq string 0 amp)
225 (subseq string (1+ amp)))
226 string))
227 (widget (make :javax.swing.*j-label text
228 (class-field :javax.swing.*j-label
229 :*trailing*))))
230 (when amp
231 (send widget :set-displayed-mnemonic-index amp))
232 widget))
233
234;;;--------------------------------------------------------------------------
235;;; Group boxes.
236
237(defun make-group (label)
238 "Create and return a group box with a given LABEL (a string) as its title."
239 (let ((group (make :javax.swing.*j-panel)))
240 (send group :set-border
241 (make :javax.swing.border.*titled-border
242 (make :javax.swing.border.*etched-border
243 (class-field :javax.swing.border.*etched-border
244 :*lowered*))
245 label))
246 (send group :set-layout (make :java.awt.*grid-bag-layout))
247 group))
248
249;;;--------------------------------------------------------------------------
250;;; Radio buttons.
251
252(defclass radio-group (widget)
253 ((alist)))
254
255(defmethod widget-insets ((widget radio-group)) 0)
256
257(defun radio-notifier-hack (value notify)
258 ;; This would be an FLET function in MAKE-RADIO-GROUP, but ABCL is buggy.
259 (implementation :java.awt.event.*action-listener
260 (action-performed (ev)
261 (declare (ignore ev))
262 (format t "notify: ~A~%" value)
263 (funcall notify value))))
264
265(defun make-radio-group (notify plist &key default)
266 (let* ((button-group (make :javax.swing.*button-group))
267 (panel (make :javax.swing.*j-panel))
268 (alist (loop for (value label) on plist by #'cddr
269 for selectp = (jboolean (eq value default))
270 for button = (make :javax.swing.*j-radio-button
271 label selectp)
272 do (format t "establish ~A~%" value)
273 (send button :add-action-listener
274 (radio-notifier-hack value notify))
275 (send button-group :add button)
276 (send panel :add button
277 (make-grid-bag-constraints :fill :horizontal
278 :insets 2
279 :weight-x 1.0))
280 collect (cons value button))))
281 (make-instance 'radio-group
282 :java panel
283 :alist alist)))
284
285(defun radio-group-selected (group)
286 (loop for (value . button) in (slot-value group 'alist)
287 when (send button :is-selected) return value
288 finally (return nil)))
289
290(defun (setf radio-group-selected) (value group)
291 (send (or (assoc value (slot-value group 'alist))
292 (error "Invalid value ~S for this radio group." value))
293 :set-selected java-true)
294 value)
295
296;;;--------------------------------------------------------------------------
297;;; Widget packing.
298
299(defun pack-single-widget (panel widget)
300 (send panel :add (widget-java widget)
301 (make-grid-bag-constraints :fill :horizontal
302 :anchor :page-start
303 :insets (widget-insets widget)
304 :weight-x 1.0
305 :grid-width :remainder))
306 widget)
307
308(defun pack-labelled-widget (panel label widget)
309 (let ((label-widget (make-label label))
310 (other-widget (widget-java widget)))
311 (send panel :add label-widget
312 (make-grid-bag-constraints :fill :horizontal
313 :anchor :north
314 :insets 2))
315 (send panel :add other-widget
316 (make-grid-bag-constraints :fill :horizontal
317 :anchor :north
318 :weight-x 1.0
319 :insets 2
320 :grid-width :remainder))
321 (send label-widget :set-label-for other-widget)
322 widget))
323
324;;;--------------------------------------------------------------------------
325;;; Toplevel windows.
326
327(defclass toplevel (widget)
328 ((java :initform (make :javax.swing.*j-frame))))
329
330(defmethod toplevel-closing ((widget toplevel))
331 (send (widget-java widget) :set-visible java-false))
332(defmethod toplevel-closed ((widget toplevel)) (drop-reason))
333(defmethod toplevel-opened ((widget toplevel)) (add-reason))
334
335(defmethod shared-initialize ((widget toplevel) slot-names &key title)
336 (declare (ignore slot-names))
337 (unless (slot-boundp widget 'java)
338 (setf (slot-value widget 'java) (make :javax.swing.*j-frame)))
339 (let ((window (widget-java widget)))
340 (when title
341 (send window :set-title title))
342 (send window :set-layout (make :java.awt.*grid-bag-layout))
343 (send window :set-default-close-operation
344 (class-field :javax.swing.*j-frame :*do-nothing-on-close*))
345 (send window :add-window-listener
346 (implementation :java.awt.event.*window-listener
347 (:window-activated (ev) (declare (ignore ev)))
348 (:window-deactivated (ev) (declare (ignore ev)))
349 (:window-iconified (ev) (declare (ignore ev)))
350 (:window-deiconified (ev) (declare (ignore ev)))
351 (:window-opened (ev) (declare (ignore ev)))
352 (:window-closing (ev)
353 (declare (ignore ev))
354 (toplevel-closing widget))
355 (:window-closed (ev)
356 (declare (ignore ev))
357 (toplevel-closed widget))))))
358
359(defun show-toplevel (widget)
360 (let ((window (widget-java widget)))
361 (unless (send window :is-showing)
362 (toplevel-opened widget)
363 (send window :set-visible java-true))))
364
365(defun make-toplevel (title populate-func)
366 (let* ((widget (make-instance 'toplevel :title title)))
367 (let ((*panel* (widget-java widget)))
368 (funcall populate-func)
369 (send *panel* :pack))
370 (show-toplevel widget)
371 widget))
372
373;;;--------------------------------------------------------------------------
374;;; Other stuff.
375
376(unless (fboundp 'exit)
377 (defun exit (&optional (return-code 0))
378 (send-class :java.lang.*system :exit return-code)))
379
380;;;----- That's all, folks --------------------------------------------------