/ gui-lib / framework / gui-utils.rkt
gui-utils.rkt
  1  #lang at-exp racket/base
  2  
  3  (require string-constants racket/gui/base
  4           racket/contract/base racket/class)
  5  (require scribble/srcdoc)
  6  (require/doc racket/base scribble/manual)
  7  
  8  (define (trim-string str size)
  9    (let ([str-size (string-length str)])
 10      (cond
 11        [(<= str-size size)
 12         str]
 13        [else
 14         (let* ([between "..."]
 15                [pre-length (- (quotient size 2)
 16                               (quotient (string-length between) 2))]
 17                [post-length (- size
 18                                pre-length
 19                                (string-length between))])
 20           (cond
 21             [(or (<= pre-length 0)
 22                  (<= post-length 0))
 23              (substring str 0 size)]
 24             [else
 25              (string-append
 26               (substring str 0 pre-length)
 27               between
 28               (substring str
 29                          (- str-size post-length)
 30                          str-size))]))])))
 31  
 32  
 33  (define maximum-string-label-length 200)
 34  
 35  ;; format-literal-label: string any* -> string
 36  (define (format-literal-label format-str . args)
 37    (quote-literal-label (apply format format-str args)))
 38  
 39  ;; quote-literal-label: string -> string
 40  (define (quote-literal-label a-str #:quote-amp? [quote-amp? #t])
 41    (define quoted (if quote-amp?
 42                       (regexp-replace* #rx"(&)" a-str "\\1\\1")
 43                       a-str))
 44    (trim-string quoted maximum-string-label-length))
 45  
 46  ;; selected-text-color : color
 47  (define selected-text-color (send the-color-database find-color "black"))
 48  
 49  ;; unselected-text-color : color
 50  (define unselected-text-color (case (system-type)
 51                                  [(macosx) (make-object color% 75 75 75)]
 52                                  [else (send the-color-database find-color "black")]))
 53  
 54  ;; selected-brush : brush
 55  (define selected-brush (send the-brush-list find-or-create-brush "WHITE" 'solid))
 56  
 57  ;; unselected-brush : brush
 58  (define unselected-brush (send the-brush-list find-or-create-brush (get-panel-background) 'solid))
 59  
 60  ;; button-down/over-brush : brush
 61  (define button-down/over-brush 
 62    (case (system-type)
 63      [(macosx) (send the-brush-list find-or-create-brush
 64                      "light blue"
 65                      'solid)]
 66      [else
 67       (send the-brush-list find-or-create-brush
 68             (make-object color% 225 225 255)
 69             'solid)]))
 70  
 71  
 72  ;; name-box-pen : pen
 73  ;; this pen draws the lines around each individual item
 74  (define name-box-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
 75  
 76  ;; background-brush : brush
 77  ;; this brush is set when drawing the background for the control
 78  (define background-brush 
 79    (case (system-type)
 80      [(macosx) (send the-brush-list find-or-create-brush (get-panel-background) 'panel)]
 81      [else (send the-brush-list find-or-create-brush "white" 'solid)]))
 82  
 83  ;; background-pen : pen
 84  ;; this pen is set when drawing the background for the control
 85  (define background-pen (send the-pen-list find-or-create-pen "black" 1 'transparent))
 86  
 87  ;; label-font : font
 88  (define label-font (send the-font-list find-or-create-font
 89                           (if (eq? (system-type) 'windows) 10 12)
 90                           'system 'normal
 91                           (if (eq? (system-type) 'macosx) 'bold 'normal)
 92                           #f))
 93  
 94  ;; name-gap : number
 95  ;; the space between each name
 96  (define name-gap 4)
 97  
 98  ;; hang-over : number
 99  ;; the amount of space a single entry "slants" over
100  (define hang-over 8)
101  
102  ;; top-space : number
103  ;; the gap at the top of the canvas, above all the choices
104  (define top-space 4)
105  
106  ;; bottom-space : number
107  ;; the extra space below the words
108  (define bottom-space 2)
109  
110  ;; end choices-canvas%
111  
112  (define (cancel-on-right?) (system-position-ok-before-cancel?))
113  
114  (define (ok/cancel-buttons parent 
115                             confirm-callback
116                             cancel-callback 
117                             [confirm-str (string-constant ok)]
118                             [cancel-str (string-constant cancel)]
119                             #:confirm-style [confirm-style '(border)]) 
120    (let ([confirm (λ ()
121                     (instantiate button% ()
122                       (parent parent)
123                       (callback confirm-callback)
124                       (label confirm-str)
125                       (style confirm-style)))]
126          [cancel (λ ()
127                    (instantiate button% ()
128                      (parent parent)
129                      (callback cancel-callback)
130                      (label cancel-str)))])
131      (let-values ([(b1 b2)
132                    (cond
133                      [(cancel-on-right?)
134                       (values (confirm) (cancel))]
135                      [else
136                       (values (cancel) (confirm))])])
137        (let ([w (max (send b1 get-width)
138                      (send b2 get-width))])
139          (send b1 min-width w)
140          (send b2 min-width w)
141          (if (cancel-on-right?)
142              (values b1 b2)
143              (values b2 b1))))))
144  
145  
146  (define clickback-delta (make-object style-delta% 'change-underline #t))
147  (define white-on-black-clickback-delta (make-object style-delta% 'change-underline #t))
148  (let ()
149    (send clickback-delta set-delta-foreground "BLUE")
150    (send white-on-black-clickback-delta set-delta-foreground "deepskyblue")
151    (void))
152  (define get-clickback-delta
153    (lambda ([white-on-black? #f])
154      (if white-on-black? 
155          white-on-black-clickback-delta
156          clickback-delta)))
157  
158  (define clicked-clickback-delta (make-object style-delta%))
159  (define white-on-black-clicked-clickback-delta (make-object style-delta%))
160  (let ()
161    (send clicked-clickback-delta set-delta-background "BLACK")
162    (send white-on-black-clicked-clickback-delta set-delta-background "white")
163    (void))
164  (define get-clicked-clickback-delta
165    (lambda ([white-on-black? #f])
166      (if white-on-black? 
167          white-on-black-clicked-clickback-delta
168          clicked-clickback-delta)))
169  
170  (define next-untitled-name
171    (let ([n 1])
172      (λ ()
173        (begin0
174          (cond
175            [(= n 1) (string-constant untitled)]
176            [else (format (string-constant untitled-n) n)])
177          (set! n (+ n 1))))))
178  
179  (define cursor-delay
180    (let ([x 0.25])
181      (case-lambda
182        [() x]
183        [(v) (set! x v) x])))
184  
185  (define show-busy-cursor
186    (lambda (thunk [delay (cursor-delay)])
187      (local-busy-cursor #f thunk delay)))
188  
189  (define delay-action
190    (λ (delay-time open close)
191      (let ([semaphore (make-semaphore 1)]
192            [open? #f]
193            [skip-it? #f])
194        (thread 
195         (λ ()
196           (sleep delay-time)
197           (semaphore-wait semaphore)
198           (unless skip-it?
199             (set! open? #t)
200             (open))
201           (semaphore-post semaphore)))
202        (λ ()
203          (semaphore-wait semaphore)
204          (set! skip-it? #t)
205          (when open?
206            (close))
207          (semaphore-post semaphore)))))
208  
209  (define local-busy-cursor
210    (let ([watch (make-object cursor% 'watch)])
211      (case-lambda
212        [(win thunk) (local-busy-cursor win thunk (cursor-delay))]
213        [(win thunk delay)
214         (let* ([old-cursor #f]
215                [cursor-off void])
216           (dynamic-wind
217            (λ ()
218              (set! cursor-off
219                    (delay-action
220                     delay
221                     (λ ()
222                       (if win
223                           (begin (set! old-cursor (send win get-cursor))
224                                  (send win set-cursor watch))
225                           (begin-busy-cursor)))
226                     (λ ()
227                       (if win
228                           (send win set-cursor old-cursor)
229                           (end-busy-cursor))))))
230            (λ () (thunk))
231            (λ () (cursor-off))))])))
232  
233  (define (unsaved-warning filename action-anyway [can-save-now? #f] [parent #f] [cancel? #t]
234                           #:dialog-mixin [dialog-mixin values])
235    (define key-closed #f)
236    (define (unsaved-warning-mixin %)
237      (class %
238        (inherit show)
239        (define/override (on-subwindow-char receiver evt)
240          (define (is-menu-key? char)
241            (and (send evt get-meta-down)
242                 (equal? (send evt get-key-code) char)))
243          (cond
244            [(is-menu-key? #\d)
245             (set! key-closed 'continue)
246             (show #f)]
247            [(is-menu-key? #\s)
248             (set! key-closed 'save)
249             (show #f)]
250            [(is-menu-key? #\c)
251             (set! key-closed 'cancel)
252             (show #f)]
253            [else
254             (super on-subwindow-char receiver evt)]))
255        (super-new)))
256    (define mb-res 
257      (message-box/custom 
258       (string-constant warning)
259       (format (string-constant file-is-not-saved) filename)
260       (string-constant save)
261       (and cancel? (string-constant cancel))
262       action-anyway
263       parent
264       (if can-save-now?
265           '(default=1 caution)
266           '(default=2 caution))
267       2
268       #:dialog-mixin (if (equal? (system-type) 'macosx)
269                          (compose unsaved-warning-mixin dialog-mixin)
270                          dialog-mixin)))
271    (or key-closed
272        (case mb-res
273          [(1) 'save]
274          [(2) 'cancel]
275          [(3) 'continue])))
276  
277  (define (get-choice message
278                      true-choice
279                      false-choice
280                      [title (string-constant warning)]
281                      [default-result 'disallow-close]
282                      [parent #f]
283                      [style 'app]
284                      [checkbox-proc #f]
285                      [checkbox-label (string-constant dont-ask-again)]
286                      #:dialog-mixin [dialog-mixin values])
287    (let* ([check? (and checkbox-proc (checkbox-proc))]
288           [style  (if (eq? style 'app) `(default=1) `(default=1 ,style))]
289           [style  (if (eq? 'disallow-close default-result)
290                       (cons 'disallow-close style) style)]
291           [style  (if check? (cons 'checked style) style)]
292           [return (λ (mb-res) (case mb-res [(1) #t] [(2) #f] [else mb-res]))])
293      (if checkbox-proc
294          (let-values ([(mb-res checked)
295                        (message+check-box/custom title message checkbox-label
296                                                  true-choice false-choice #f
297                                                  parent style default-result
298                                                  #:dialog-mixin dialog-mixin)])
299            (checkbox-proc checked)
300            (return mb-res))
301          (return (message-box/custom title message true-choice false-choice #f
302                                      parent style default-result
303                                      #:dialog-mixin dialog-mixin)))))
304  
305  ;; manual renaming
306  (define gui-utils:trim-string trim-string)
307  (define gui-utils:quote-literal-label quote-literal-label)
308  (define gui-utils:format-literal-label format-literal-label)
309  (define gui-utils:next-untitled-name next-untitled-name)
310  (define gui-utils:show-busy-cursor show-busy-cursor)
311  (define gui-utils:delay-action delay-action)
312  (define gui-utils:local-busy-cursor local-busy-cursor)
313  (define gui-utils:unsaved-warning unsaved-warning)
314  (define gui-utils:get-choice get-choice)
315  (define gui-utils:get-clicked-clickback-delta get-clicked-clickback-delta)
316  (define gui-utils:get-clickback-delta get-clickback-delta)
317  (define gui-utils:ok/cancel-buttons ok/cancel-buttons)
318  (define gui-utils:cancel-on-right? cancel-on-right?)
319  (define gui-utils:cursor-delay cursor-delay)
320  
321  
322  (provide/doc
323   (proc-doc
324    gui-utils:trim-string
325    (->i ([str string?]
326          [size (and/c number? positive?)])
327         ()
328         [res (size)
329              (and/c string?
330                     (λ (str)
331                       ((string-length str) . <= . size)))])
332    @{Constructs a string whose size is less
333      than @racket[size] by trimming the @racket[str]
334      and inserting an ellispses into it.})
335  
336   (proc-doc/names
337    gui-utils:quote-literal-label
338    (->* (string?)
339         (#:quote-amp? any/c)
340         (and/c string?
341                (λ (str) ((string-length str) . <= . 200))))
342    ((string)
343     ((quote-amp? #t)))
344    @{Constructs a string whose length is less than @racket[200] and,
345      if @racket[quote-amp?] is not @racket[#f], then it also quotes
346      the ampersand in the result (making the string suitable for use in
347      @racket[menu-item%] label, for example).})
348  
349   (proc-doc
350    gui-utils:format-literal-label
351    (->i ([str string?])
352         ()
353         #:rest [rest (listof any/c)]
354         [res (str)
355              (and/c string?
356                     (lambda (str)
357                       ((string-length str) . <= . 200)))])
358    @{Formats a string whose ampersand characters are
359      mk-escaped; the label is also trimmed to <= 200
360      mk-characters.})
361  
362   (proc-doc/names
363    gui-utils:cancel-on-right?
364    (-> boolean?)
365    ()
366    @{Returns @racket[#t] if cancel should be on the right-hand side (or below)
367      in a dialog and @racket[#f] otherwise.
368      
369      Just returns what @racket[system-position-ok-before-cancel?] does.
370      
371      See also @racket[gui-utils:ok/cancel-buttons].})
372   (proc-doc/names
373    gui-utils:ok/cancel-buttons
374    (->* ((is-a?/c area-container<%>)
375          ((is-a?/c button%) (is-a?/c event%) . -> . any)
376          ((is-a?/c button%) (is-a?/c event%) . -> . any))
377         (string?
378          string?
379          #:confirm-style (listof symbol?))
380         (values (is-a?/c button%)
381                 (is-a?/c button%)))
382    ((parent
383      confirm-callback
384      cancel-callback)
385     ((confirm-label (string-constant ok))
386      (cancel-label (string-constant cancel))
387      (confirm-style '(border))))
388    @{Adds an Ok and a cancel button to a panel, changing the order
389      to suit the platform. Under Mac OS and unix, the confirmation action
390      is on the right (or bottom) and under Windows, the canceling action is on
391      the right (or bottom).
392      The buttons are also sized to be the same width.
393      
394      The first result is be the OK button and the second is
395      the cancel button.
396      
397      By default, the confirmation action button has the @racket['(border)] style,
398      meaning that hitting return in the dialog will trigger the confirmation action.
399      The @racket[confirm-style] argument can override this behavior, tho.
400      See @racket[button%] for the precise list of allowed styles.
401      
402      See also @racket[gui-utils:cancel-on-right?].})
403   
404   (proc-doc/names
405    gui-utils:next-untitled-name
406    (-> string?)
407    ()
408    @{Returns a name for the next opened untitled frame. The first
409      name is ``Untitled'', the second is ``Untitled 2'',
410      the third is ``Untitled 3'', and so forth.})
411   (proc-doc/names
412    gui-utils:cursor-delay
413    (case->
414     (-> real?)
415     (real? . -> . void?))
416    (() (new-delay))
417    @{This function is @italic{not} a parameter.
418      Instead, the state is just stored in the closure.
419      
420      The first case in the case lambda
421      returns the current delay in seconds before a watch cursor is shown,
422      when either @racket[gui-utils:local-busy-cursor] or
423      @racket[gui-utils:show-busy-cursor] is called.
424      
425      The second case in the case lambda
426      Sets the delay, in seconds, before a watch cursor is shown, when
427      either @racket[gui-utils:local-busy-cursor] or
428      @racket[gui-utils:show-busy-cursor] is called.})
429   (proc-doc/names
430    gui-utils:show-busy-cursor
431    (->* ((-> any/c))
432         (integer?)
433         any/c)
434    ((thunk)
435     ((delay (gui-utils:cursor-delay))))
436    @{Evaluates @racket[(thunk)] with a watch cursor. The argument
437      @racket[delay] specifies the amount of time before the watch cursor is
438      opened. Use @racket[gui-utils:cursor-delay] to set this value
439      to all calls.
440      
441      This function returns the result of @racket[thunk].})
442   (proc-doc/names
443    gui-utils:delay-action
444    (real?
445     (-> void?)
446     (-> void?)
447     . -> .
448     (-> void?))
449    (delay-time open close)
450    @{Use this function to delay an action for some period of time. It also
451      supports canceling the action before the time period elapses. For
452      example, if you want to display a watch cursor, but you only want it
453      to appear after 2 seconds and the action may or may not take more than
454      two seconds, use this pattern:
455      
456      @racketblock[(let ([close-down
457                          (gui-utils:delay-action
458                           2
459                           (λ () .. init watch cursor ...)
460                           (λ () .. close watch cursor ...))])
461                     ;; .. do action ...
462                     (close-down))]
463      
464      Creates a thread that waits @racket[delay-time]. After @racket[delay-time]
465      has elapsed, if the result thunk has @italic{not} been called, call
466      @racket[open]. Then, when the result thunk is called, call
467      @racket[close]. The function @racket[close] will only be called if
468      @racket[open] has been called.})
469   
470   (proc-doc/names
471    gui-utils:local-busy-cursor
472    (->*
473     ((is-a?/c window<%>)
474      (-> any/c))
475     (integer?)
476     any/c)
477    ((window thunk)
478     ((delay (gui-utils:cursor-delay))))
479    @{Evaluates @racket[(thunk)] with a watch cursor in @racket[window]. If
480      @racket[window] is @racket[#f], the watch cursor is turned on globally.
481      The argument @racket[delay] specifies the amount of time before the watch
482      cursor is opened. Use @racket[gui-utils:cursor-delay]
483      to set this value for all uses of this function.
484      
485      The result of this function is the result of @racket[thunk].})
486   
487   (proc-doc/names
488    gui-utils:unsaved-warning
489    (->*
490     (string?
491      string?)
492     (boolean?
493      (or/c false/c
494            (is-a?/c frame%)
495            (is-a?/c dialog%))
496      boolean?
497      #:dialog-mixin (make-mixin-contract dialog%))
498     (symbols 'continue 'save 'cancel))
499    ((filename action)
500     ((can-save-now? #f)
501      (parent #f)
502      (cancel? #t)
503      (dialog-mixin values)))
504    
505    @{This displays a dialog that warns the user of a unsaved file.
506      
507      The string, @racket[action], indicates what action is about to
508      take place, without saving. For example, if the application
509      is about to close a file, a good action is @racket["Close Anyway"].
510      The result symbol indicates the user's choice. If
511      @racket[can-save-now?] is @racket[#f], this function does not
512      give the user the ``Save'' option and thus will not return
513      @racket['save].
514      
515      If @racket[cancel?] is @racket[#t] there is a cancel button
516      in the dialog and the result may be @racket['cancel]. If it
517      is @racket[#f], then there is no cancel button, and @racket['cancel]
518      will not be the result of the function.
519  
520      The @racket[dialog-mixin] argument is passed to @racket[message-box/custom].
521  
522      @history[#:changed "1.29" @elem{Added the @racket[dialog-mixin] argument.}]
523      
524      })
525   
526   (proc-doc/names
527    gui-utils:get-choice
528    (->* (string?
529          string?
530          string?)
531         (string?
532          any/c
533          (or/c false/c (is-a?/c frame%) (is-a?/c dialog%))
534          (symbols 'app 'caution 'stop)
535          (or/c false/c (case-> (boolean? . -> . void?)
536                                (-> boolean?)))
537          string?
538          #:dialog-mixin (make-mixin-contract dialog%))
539         any/c)
540    ((message true-choice false-choice)
541     ((title (string-constant warning))
542      (default-result 'disallow-close)
543      (parent #f)
544      (style 'app)
545      (checkbox-proc #f)
546      (checkbox-label (string-constant dont-ask-again))
547      (dialog-mixin values)))
548    
549    @{Opens a dialog that presents a binary choice to the user. The user is
550      forced to choose between these two options, ie cancelling or closing the
551      dialog opens a message box asking the user to actually choose one of the
552      two options.
553      
554      The dialog will contain the string @racket[message] and two buttons,
555      labeled with the @racket[true-choice] and the @racket[false-choice].  If the
556      user clicks on @racket[true-choice] @racket[#t] is returned. If the user
557      clicks on @racket[false-choice], @racket[#f] is returned.
558      
559      The argument @racket[default-result] determines how closing the window is
560      treated. If the argument is @racket['disallow-close], closing the window
561      is not allowed. If it is anything else, that value is returned when
562      the user closes the window.
563      
564      If @racket[gui-utils:cancel-on-right?]
565      returns @racket[#t], the false choice is on the right.
566      Otherwise, the true choice is on the right.
567      
568      The @racket[style] parameter is (eventually) passed to
569      @racket[message]
570      as an icon in the dialog.
571      
572      If @racket[checkbox-proc] is given, it should be a procedure that behaves
573      like a parameter for getting/setting a boolean value.  The intention for
574      this value is that it can be used to disable the dialog.  When it is
575      given, a checkbox will appear with a @racket[checkbox-label] label
576      (defaults to the @racket[dont-ask-again] string constant), and that
577      checkbox value will be sent to the @racket[checkbox-proc] when the dialog
578      is closed.  Note that the dialog will always pop-up --- it is the
579      caller's responsibility to avoid the dialog if not needed.
580  
581      The @racket[dialog-mixin] argument is passed to @racket[message-box/custom]
582      or @racket[message+check-box/custom].
583  
584      @history[#:changed "1.29" @elem{Added the @racket[dialog-mixin] argument.}]
585  
586      })
587   
588   (proc-doc/names
589    gui-utils:get-clicked-clickback-delta
590    (->* ()
591         (boolean?)
592         (is-a?/c style-delta%))
593    (()
594     ((white-on-black? #f)))
595    @{This delta is designed for use with
596      @method[text set-clickback].
597      Use it as one of the @racket[style-delta%] argument to
598      @method[text% set-clickback].
599      
600      If @racket[white-on-black?] is true, the function returns
601      a delta suitable for use on a black background.
602      
603      See also @racket[gui-utils:get-clickback-delta].})
604   
605   (proc-doc/names
606    gui-utils:get-clickback-delta
607    (->* ()
608         (boolean?)
609         (is-a?/c style-delta%))
610    (()
611     ((white-on-black? #f)))
612    @{This delta is designed for use with @method[text% set-clickback].
613      Use the result of this function as the style
614      for the region text where the clickback is set.
615      
616      If @racket[white-on-black?] is true, the function returns
617      a delta suitable for use on a black background.
618      
619      See also
620      @racket[gui-utils:get-clicked-clickback-delta].}))