tool.rkt
#|
Copyright (c) 2012 Laurent Orseau (laurent orseau gmail com)

License: LGPL v3 or higher (http://www.gnu.org/copyleft/lesser.html)

|#
#lang racket/base

(require drracket/tool
         racket/class
         racket/gui/base
         racket/unit
         racket/file
         racket/pretty ; for pretty-write
         racket/path ; for filename-extension
         racket/dict
         racket/list
         racket/runtime-path ; for the help menu
         (for-syntax racket/base) ; for help menu
         net/sendurl ; for the help menu
         framework ; for preferences (too heavy a package?)
         )
(provide tool@)

#| TODO:
- better error checking/reporting
- automatic testing with the framework
- fix?: for the shortcuts to work, the menu must have been generated.  
  (but seems that the menus are re-generated more often than this)
|#

#| Package managing:
(require planet/util)
(add-hard-link "orseau" "script-plugin.plt" 1 0 (current-directory))
; once the package is ready for upload:
;(remove-hard-link "orseau" "script-plugin.plt" 1 0)
 
; then do a `raco setup`

; To build the package, in the parent directory:
$ raco planet create script-plugin
;|#

(define-runtime-path help-path
  (build-path "planet-docs" "manual" "index.html"))


(preferences:set-default 'user-script-dir
                         (path->string (build-path (find-system-path 'pref-dir) "user-scripts"))
                         path-string?)

(define (script-dir)
  (preferences:get 'user-script-dir))

;(displayln (script-dir))
(make-directory* (script-dir))

(define (set-script-dir dir)
  (preferences:set 'user-script-dir (if (path? dir) (path->string dir) dir)))

(define (choose-script-dir)
  (let ([d (get-directory "Choose a directory to store scripts" #f
                          (script-dir))])
;    (displayln d)
    (and d (set-script-dir d))))

(define (error-message-box filename e)
  (message-box filename
               (format "Error in script file ~s: ~a" filename (exn-message e))
               #f '(stop ok)))

(define-namespace-anchor a)

(define tool@
  (unit
    (import drracket:tool^)
    (export drracket:tool-exports^)
 
    (define script-menu-mixin
      (mixin (drracket:unit:frame<%>) ()
        (super-new)
        (inherit get-button-panel
                 get-definitions-text
                 get-interactions-text
                 ;register-toolbar-button
                 create-new-tab
                 )
        
        (define (get-the-text-editor)
          ; for a frame:text% :
          ;(define text (send frame get-editor))
          ; for DrRacket:
          (get-definitions-text)
          )
        
        (define frame this)
        
        (define props-default
          `((functions . item-callback)
            ;(sub-menu . #f)
            (shortcut . #f)
            (shortcut-prefix . #f)
            (help-string . "Help String")
            (output-to . selection) ; outputs the result in a new tab
            (active . #t)
            ))
        
        (define (prop-dict-ref props key)
          (dict-ref props key (dict-ref props-default key)))
        
        (define (new-script)
          (define name (get-text-from-user "Script name" "Enter the name of the script:"))
          (when name
            (define script-name (string-append name ".rkt"))
            (define f-script (build-path (script-dir) script-name))
            (define f-prop (build-path (script-dir) (string-append script-name "d")))
            
            (with-output-to-file f-prop
              (λ _ (pretty-write (cons `(label . ,name) props-default))))
            (with-output-to-file f-script
              (λ _ 
                (displayln "#lang racket/base\n\n;; Sample identity function:\n;; string? -> (or/c string? #f)")
                (for-each pretty-write 
                          '((provide item-callback)
                            (define (item-callback str)
                              str
                              )))
                (displayln "\n;; See the manual in the Script/Help menu for more information.")
                ))
            
            (edit-script f-prop)
            (edit-script f-script)
            ))
        
        ;; file: path?
        (define (edit-script file)
          (when file
            ; For frame:text% :
            ;(send (get-the-text-editor) load-file file)
            ; For DrRacket:
            (send this open-in-new-tab file)
            ))
        
        (define (open-script)
          (define file (get-file "Open a script" frame (script-dir) #f #f '() 
                                 '(("Racket" "*.rkt"))))
          (edit-script file)
          )
        
        (define (open-script-properties)
          (define file (get-file "Open a script properties" frame (script-dir) #f #f '() 
                                 '(("Property file" "*.rktd"))))
          (edit-script file)
          )
        
        ;; f: path?
        (define (run-script fun file output-to)
          ; For frame:text% :
          ;(define text (send frame get-editor))
          ; For DrRacket:
          (define text (get-the-text-editor))
          (define str (send text get-text 
                            (send text get-start-position) 
                            (send text get-end-position)))
          (define ns (make-base-empty-namespace))
          (for ([mod '(racket/class racket/gui/base)])
            (namespace-attach-module (namespace-anchor->empty-namespace a)
                                     mod
                                     ns))
          (define file-str (path->string file))
          (define ed-file (send text get-filename))
          (define str-out
            (with-handlers ([exn:fail? (λ(e)(error-message-box (path->string (file-name-from-path file)) e)
                                         #f)])
              ; SEE HelpDesk for "Manipulating namespaces"
              (parameterize ([current-namespace ns])
                (let ([f (dynamic-require file fun)]
                      [kw-dict `((#:editor . ,text)
                                 (#:interactions . ,(get-interactions-text))
                                 (#:file . ,ed-file)
                                 (#:frame . ,this))])
                  (let-values ([(_ kws) (procedure-keywords f)])
;                    (let ([k-v (sort (map (λ(k)(list k (dict-ref kw-dict k))) kws)
                    (let ([k-v (sort (map (λ(k)(assoc k kw-dict)) kws)
                                     keyword<? #:key car)])
                      (keyword-apply f (map car k-v) (map cdr k-v) str '())
                      )))
                )))
          (define (insert-to-text text)
            ; Inserts the text, possibly overwriting the selection:
            (send text begin-edit-sequence)
            (send text insert str-out)
            (send text end-edit-sequence))
          ; DrRacket specific:
          (when (or (string? str-out) (is-a? str-out snip%)) ; do not modify the file if no output
            (case output-to
              [(new-tab)
               (create-new-tab)
               (insert-to-text (get-the-text-editor))] ; get the newly created text
              [(selection)
               (insert-to-text text)]
              [(message-box)
               (message-box "Ouput" str-out this)]
              ))
          )
        
        (define (open-help)
          (send-url/file help-path))
        
        (define menu-bar (send this get-menu-bar))
        
        (define scripts-menu 
          (new menu% [parent menu-bar] [label "&Scripts"]
               [demand-callback
                (λ(m)
                  ;; remove all scripts items, after the persistent ones:
                  (for ([item (list-tail (send scripts-menu get-items) 5)])
                    (send item delete))
                  ;; add script items:
                  ; the menu-hash holds the submenus, to avoid creating them more than once
                  (define menu-hash (make-hash))
                  ;for all scripts in the script directory:
                  (for ([f (directory-list (script-dir))])
                    (let ([f-prop (build-path (script-dir) (string-append (path->string f) "d"))])
                      ; the script file must have an associated rktd file
                      (when (and (member (filename-extension f) '(#"rkt"))
                                 (file-exists? f-prop))
                        ; read from the property file
                        (with-input-from-file f-prop 
                          (λ _
                            ; for all dictionaries in the file:
                            (let loop ([props (read)]) 
                              (when (and (dict? props) (prop-dict-ref props 'active))
                                (let*([label           (dict-ref props 'label (path->string f))]
                                      [functions       (prop-dict-ref props 'functions)]
                                      [shortcut        (prop-dict-ref props 'shortcut)]
                                      [shortcut-prefix (or (prop-dict-ref props 'shortcut-prefix) 
                                                           (get-default-shortcut-prefix))]
                                      [help-string     (prop-dict-ref props 'help-string)]
                                      [output-to       (prop-dict-ref props 'output-to)]
                                      [parent-menu (if (list? functions)
                                                       (hash-ref! menu-hash label
                                                                  ; create a sub-menu if necessary:
                                                                  (λ _ (new menu% [parent scripts-menu]
                                                                            [label label])))
                                                       scripts-menu)]
                                      [label-functions (if (list? functions) functions (list (list functions label)))]
                                      )
                                  ; for all functions in the dictionary:
                                  (for ([fun   (map first  label-functions)]
                                        [label (map second label-functions)])
                                    ; create an item for this function:
                                    (new menu-item% [parent parent-menu] [label label]
                                         [shortcut shortcut] 
                                         [shortcut-prefix shortcut-prefix]
                                         [help-string help-string]
                                         [callback (λ(it ev)
                                                     (run-script fun
                                                                 (build-path (script-dir) f)
                                                                 output-to))])
                                    ))
                                ; next dict:
                                (loop (read))
                                ))))))))]))
        
        (new menu-item% [parent scripts-menu] [label "New Script"]
             [callback (λ _ (new-script))])
        (new menu-item% [parent scripts-menu] [label "Open Script"]
             [callback (λ _ (open-script))])
        (new menu-item% [parent scripts-menu] [label "Open Script Properties"]
             [callback (λ _ (open-script-properties))])
        (new menu-item% [parent scripts-menu] [label "Help"]
             [callback (λ _ (open-help))])
        (new separator-menu-item% [parent scripts-menu])

        
        ;; the preference panel is automatically added by DrRacket (nice feature!)
        (preferences:add-panel 
         "Scripts"
         (λ(parent)
           (define pref-panel (new vertical-panel% [parent parent] [alignment '(center center)]
                                   [spacing 10]
                                   [horiz-margin 10]
                                   [vert-margin 10]
                                   ))
           (define dir-panel (new horizontal-panel% [parent pref-panel]))
           (define text-dir (new text-field% [parent dir-panel] [label "Script directory:"]
                                 [init-value (script-dir)]
                                 [enabled #f]))
           (new button% [parent dir-panel] [label "Change script directory"]
                [callback (λ _ (choose-script-dir))])
           (preferences:add-callback 'user-script-dir
                                     (λ(p v)(send text-dir set-value v)))
           pref-panel))
 
        ))
 
    (define (phase1) (void))
    (define (phase2) (void))
 
    (drracket:get/extend:extend-unit-frame script-menu-mixin)
    
    ))