#| 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) net/sendurl ; for the help menu framework ; for preferences (too heavy a package?) ) (provide tool@) #| TODO: - better error checking/reporting - fix?: for the shortcuts to work, the menu must have been generated. (but seems that the menus are re-generated more often than this) |# #| (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 ;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 . transform-input-string) ;(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? -> string?") (for-each pretty-write '((provide transform-input-string) (define (transform-input-string str) str ))) (displayln ";; See the manual for more information") )) (edit-script f-script) (edit-script f-prop) )) ;; 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) (#:file . ,ed-file) (#:frame . ,this))]) (let-values ([(_ kws) (procedure-keywords f)]) ; TODO: use assoc and sort instead (let ([k-v (sort (map (λ(k)(list k (dict-ref kw-dict k))) kws) keywordstring 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) ))