-
Notifications
You must be signed in to change notification settings - Fork 3
/
utils.lisp
302 lines (263 loc) · 12.5 KB
/
utils.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
;;; -*- Mode: LISP; Syntax: COMMON-LISP; Package: FM-PLUGIN-TOOLS; Base: 10 -*-
;;; Copyright (c) 2006-2010, Dr. Edmund Weitz. All rights reserved.
;;; Copyright (c) 2022, Chun Tian (binghe). All rights reserved.
;;; Redistribution and use in source and binary forms, with or without
;;; modification, are permitted provided that the following conditions
;;; are met:
;;; * Redistributions of source code must retain the above copyright
;;; notice, this list of conditions and the following disclaimer.
;;; * Redistributions in binary form must reproduce the above
;;; copyright notice, this list of conditions and the following
;;; disclaimer in the documentation and/or other materials
;;; provided with the distribution.
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(in-package :fm-plugin-tools)
(defun check-plugin-id ()
"Checks whether *PLUGIN-ID* has a valid value. Should be called in
delivery script."
(unless (and (stringp *plugin-id*)
(= 4 (length *plugin-id*))
(every (lambda (char)
(or (char-not-greaterp #\A char #\Z)
(char<= #\0 char #\9)))
*plugin-id*))
(error "Plug-in ID must be a string of four characters each of ~
which is a letter or a digit.")))
(defun fm-log (control-string &rest format-args)
"Utility function which might be useful for debugging a plug-in.
Writes data to the file denoted by *FM-LOGFILE* unless this value
is NIL. CONTROL-STRING and FORMAT-ARGS are interpreted as by
FORMAT."
(when *fm-logfile*
(let ((fm-logfile (if (eq *fm-logfile* t)
(merge-pathnames "FM-PlugIn-Tools/fm-plugin-tools.log"
(sys:get-folder-path #-:macosx :local-appdata
#+:macosx :my-appsupport))
*fm-logfile*)))
(with-open-file (out (ensure-directories-exist fm-logfile)
:direction :output
:element-type 'lw:simple-char
:external-format '(:utf-8 :eol-style :lf)
:if-exists :append
:if-does-not-exist :create)
(apply #'format out control-string format-args)
(finish-output out))))
(values))
;; Characters 1-4 are the plug-in ID
;; Character 5 is always "1"
;; Character 6
;; use "Y" if you want to enable the Configure button for plug-ins in the Preferences dialog box.
;; Use "n" if there is no plug-in configuration needed.
;; If the flag is set to "Y" then make sure to handle the kFMXT_DoAppPreferences message.
;; Character 7 is always "n"
;; Character 8
;; Set to "Y" if you want to receive kFMXT_Init/kFMXT_Shutdown messages
;; In most cases, you want to set it to 'Y' since it's the best time to register/unregister your plugin functions.
;; Character 9
;; Set to "Y" if the kFMXT_Idle message is required.
;; For simple external functions this may not be needed and can be turned off by setting the character to "n"
;; Character 10
;; Set to "Y" to receive kFMXT_SessionShutdown and kFMXT_FileShutdown messages
;; Character 11 is always "n"
(defun create-options-string ()
"Creates an option string which can be sent back to FileMaker
on request. See FileMaker documentation."
(format nil "~A1~:[n~;Y~]nY~:[n~;Y~]~:[n~;Y~]n"
*plugin-id* ; char 1-4
#+:win32 *preferences-function* #+:macosx nil ; char 6
*enable-idle-messages* ; char 9
(when (<= +k150extn-version+ (extn-version))
*enable-shutdown-messages*))) ; char 10
(defun next-function-id ()
"Returns the next unused function ID, an integer."
(incf *function-counter*))
(defun next-callable-name ()
"Returns a symbol which will be used as internal name for a
foreign callable. Returns a different symbol each time it is
called."
(gensym "FM-PLUGIN-TOOLS-foreign-callable"))
(defun function-name (prototype)
"Extracts the function name from a function prototype string
like \"foo ( arg1 ; arg2 )\"."
(let ((paren-pos (position #\( prototype)))
(string-trim " " (if paren-pos
(subseq prototype 0 paren-pos)
prototype))))
(defun plugin-preference (path value-name)
"Returns preferences \(stored in the Windows registry)
corresponding to PATH and NAME."
(unless (listp path)
(setq path (list path)))
(user-preference path value-name :product :fm-plugin-tools))
(defun (setf plugin-preference) (new-value path value-name)
"Sets preferences \(stored in the Windows registry)
corresponding to PATH and NAME to the new value NEW-VALUE."
(unless (listp path)
(setq path (list path)))
(setf (user-preference path value-name :product :fm-plugin-tools)
new-value))
(defmacro remember-interface-geometry (interface-class-name)
"Convenience macro which sets up a user defined interface class
such that its geometry will automatically be stored in the
Windows registry between different invocations of the plug-in."
`(progn
(defmethod capi:top-level-interface-save-geometry-p ((interface ,interface-class-name))
t)
(defmethod capi:top-level-interface-geometry-key ((interface ,interface-class-name))
(values ',interface-class-name :fm-plugin-tools))
(pushnew ',interface-class-name *symbols-to-keep*)))
(defun get-backtrace ()
"Returns a full backtrace as a string. To be used in
handlers."
(with-output-to-string (out nil :element-type 'lw:simple-char)
(let ((dbg::*debugger-stack* (dbg::grab-stack nil :how-many most-positive-fixnum))
(*debug-io* out)
(dbg:*debug-print-level* nil)
(dbg:*debug-print-length* nil))
(dbg:bug-backtrace nil))))
(defun maybe-log-error (cond &optional prototype)
"Logs the condition COND using FM-LOG if *LOG-ERRORS* is true.
Also logs a backtrace if *LOG-BACKTRACES-P* is true as well."
(when *log-errors-p*
(fm-log "Error~:[~*~; in function ~A~]: ~A~%"
prototype (function-name prototype) cond)
(when *log-backtraces-p*
(fm-log "Backtrace:~% ~A~%" (get-backtrace)))))
(defun top-level-hook (fn interface)
"A function which can be used as a top-level hook for
interfaces to make them more robust against unhandled conditions.
See the reference entry for CAPI:INTERFACE."
(flet ((top-level-error-handler (cond)
(capi:display-message "~A" cond)
(maybe-log-error cond)
(ignore-errors
(capi:apply-in-pane-process interface
(if (capi:current-dialog-handle)
'capi:abort-dialog
'capi:destroy)
interface))))
(handler-bind ((error #'top-level-error-handler))
(funcall fn))))
(defun boolean-value (thing)
"Returns T if THING is not NIL, NIL otherwise."
(not (not thing)))
(defun set-product-name ()
"Sets *PRODUCT-NAME* from *PLUGIN-NAME* if it hasn't been set
explicitly."
(unless *product-name*
(setq *product-name* *plugin-name*))
*product-name*)
(defun version-string ()
"Returns a string representation of the plug-in version."
(format nil "~{~A~^.~}" *plugin-version*))
(defun convert-line-endings (string)
"Converts Mac line endings \(carriage returns, used internally
by FileMaker) to line feeds."
(with-output-to-string (out nil :element-type 'lw:simple-char)
(loop for char across string
do (write-char (case char
(#.#\Return #\Linefeed)
(otherwise char))
out))))
;; this is needed for higher delivery levels
(fli::define-precompiled-foreign-object-accessor-functions
(((:pointer :void) :no-alloc-p :error :size nil)))
(defmacro with-fmxcpt ((ptr) &body body)
"Executes BODY with PTR bound to a pointer to a FMXCPT C
struct. After execution of BODY this C struct's M-CODE slot is
checked for potential error message and a condition is signaled
if appropriate."
;; the +K-FOO+ constants will be defined by PREPARE-FM-PLUGIN-TOOLS
(declare (special +k-no-err+ +k-bad-alloc+ +k-unknown+))
`(with-dynamic-foreign-objects ((,ptr fmxcpt))
(setf (foreign-slot-value ,ptr 'm-vers) 1
(foreign-slot-value ,ptr 'm-code) ,+k-no-err+)
(prog1
(progn ,@body)
(ecase (foreign-slot-value ,ptr 'm-code)
(,+k-no-err+)
(,+k-bad-alloc+ (error "FileMaker: Bad allocation."))
(,+k-unknown+ (error "FileMaker: Exception."))))))
(defmacro define-fmxcpt-function ((lisp-name c-name) arg-list &rest keyword-args)
"This is basically equivalent to DEFINE-FOREIGN-FUNCTION except
that it implicitly adds a new last argument for a FMXCPT C
struct to the C function definition and wraps the body of the
corresponding Lisp function \(wrapper) with a WITH-FMXCPT for
automatic error handling."
(with-unique-names (fmxcpt-ptr)
;; use a gensym for INTERNAL-NAME that has some resemblance to C-NAME
(let ((internal-name (gensym c-name))
(untyped-arg-list (mapcar #'first arg-list)))
`(progn
(define-foreign-function (,internal-name ,c-name)
(,@arg-list
;; add pointer to FMXCPT C struct as last argument
(,fmxcpt-ptr (:pointer fmxcpt)))
,@(sys::remove-properties keyword-args '(:lambda-list))
#+:win32 #+:win32
:calling-convention :cdecl
:module :fm-wrapper)
;; use lambda list in Lisp function if provided - the
;; function LISP-NAME wraps the Lisp function INTERNAL-NAME
(defun ,lisp-name ,(or (getf keyword-args :lambda-list) untyped-arg-list)
;; add error handling
(with-fmxcpt (,fmxcpt-ptr)
(,internal-name ,@untyped-arg-list ,fmxcpt-ptr)))))))
;; help the LispWorks IDE to find these definitions
(define-form-parser define-fmxcpt-function (name)
`(,define-fmxcpt-function ,(first name)))
(define-dspec-alias define-fmxcpt-function (name)
`(defun ,name))
;; setup correct indentation of DEFINE-FMXCPT-FUNCTION
(editor:setup-indent "define-fmxcpt-function" 2 2 4)
(defun extn-version ()
"Shortcut to get at EXTN-VERSION slot of current parameter block."
(foreign-slot-value *parameter-block* 'extn-version))
(defun which-call ()
"Shortcut to get at WHICH-CALL slot of current parameter block."
(foreign-slot-value *parameter-block* 'which-call))
(defun parm1 ()
"Shortcut to get at PARM1 slot of current parameter block."
(foreign-slot-value *parameter-block* 'parm1))
(defun parm2 ()
"Shortcut to get at PARM2 slot of current parameter block."
(foreign-slot-value *parameter-block* 'parm2))
(defun parm3 ()
"Shortcut to get at PARM3 slot of current parameter block."
(foreign-slot-value *parameter-block* 'parm3))
(defun result ()
"Shortcut to get at RESULT slot of current parameter block."
(foreign-slot-value *parameter-block* 'result))
(defun (setf result) (new-value)
"Shortcut to change RESULT slot of current parameter block."
(setf (foreign-slot-value *parameter-block* 'result)
new-value))
(defun c-start-script ()
"Shortcut to get at C-START-SCRIPT slot of current parameter block."
(foreign-slot-value *parameter-block* 'c-start-script))
(defun c-current-env ()
"Shortcut to get at C-CURRENT-ENV slot of current parameter block."
(foreign-slot-value *parameter-block* 'c-current-env))
(defun get-environment ()
"Returns the value of *ENVIRONMENT* or, failing that, the value of
*GLOBAL-ENVIRONMENT*."
(or *environment*
;; use lock so we don't interfere with updates
(mp:with-lock (*global-environment-lock*)
*global-environment*)
(error "No environment available.")))
;; This is learnt from CXML web site
(defun write-xml (node &key indent)
(let ((sink (cxml:make-string-sink
:canonical nil :indentation indent)))
(cxml-xmls:map-node sink node :include-namespace-uri nil)))