
(module frame mzscheme
  (require (lib "name-message.ss" "mrlib")
           (lib "string-constant.ss" "string-constants")
           (lib "unitsig.ss")
           (lib "class.ss")
           (lib "string.ss")
           (lib "list.ss")
           "drsig.ss"
           (lib "mred.ss" "mred")
           (lib "framework.ss" "framework")
           (lib "url.ss" "net")
           (lib "head.ss" "net")
           (lib "plt-installer.ss" "setup")
           (lib "bug-report.ss" "help")
           (prefix mzlib:file: (lib "file.ss")) (lib "file.ss")
           (prefix mzlib:list: (lib "list.ss")))
  
  (provide frame@)
  (define frame@
    (unit/sig drscheme:frame^
      (import [drscheme:unit : drscheme:unit^]
              [drscheme:app : drscheme:app^]
              [help : drscheme:help-desk^]
              [drscheme:multi-file-search : drscheme:multi-file-search^]
              [drscheme:init : drscheme:init^])
      
      (rename [-mixin mixin])
      
      (define basics<%> (interface (frame:standard-menus<%>)))
      
      (define basics-mixin
        (mixin (frame:standard-menus<%>) (basics<%>)
          (inherit get-edit-target-window get-edit-target-object get-menu-bar)
          [define/private get-menu-bindings
            (λ ()
              (let ([name-ht (make-hash-table)])
                (let loop ([menu-container (get-menu-bar)])
                  (for-each
                   (λ (item)
                     (when (is-a? item selectable-menu-item<%>)
                       (let ([short-cut (send item get-shortcut)])
                         (when short-cut
                           (let ([keyname
                                  (string->symbol
                                   (keymap:canonicalize-keybinding-string
                                    (string-append
                                     (case (system-type)
                                       [(windows) "c:"]
                                       [(macosx macos) "d:"]
                                       [(unix)
                                        (case (send item get-x-shortcut-prefix)
                                          [(meta) "m:"]
                                          [(alt) "a:"]
                                          [(ctl) "c:"]
                                          [(ctl-m) "c:m;"])]
                                       [else ""])
                                     (case short-cut
                                       [(#\;) "semicolon"]
                                       [(#\:) "colon"]
                                       [(#\space) "space"]
                                       [else (string short-cut)]))))])
                             (hash-table-put! name-ht keyname (send item get-plain-label))))))
                     (when (is-a? item menu-item-container<%>)
                       (loop item)))
                   (send menu-container get-items)))
                name-ht))]
          
          [define/private copy-hash-table
            (λ (ht)
              (let ([res (make-hash-table)])
                (hash-table-for-each
                 ht
                 (λ (x y) (hash-table-put! res x y)))
                res))]
          [define/private can-show-keybindings?
            (λ ()
              (let ([edit-object (get-edit-target-object)])
                (and edit-object
                     (is-a? edit-object editor<%>)
                     (let ([keymap (send edit-object get-keymap)])
                       (is-a? keymap keymap:aug-keymap<%>)))))]
          
          [define/private show-keybindings
            (λ ()
              (if (can-show-keybindings?)
                  (let ([edit-object (get-edit-target-object)])
                    (let ([keymap (send edit-object get-keymap)])
                      (let*-values ([(menu-names) (get-menu-bindings)])
                        (let* ([table (send keymap get-map-function-table)]
                               [bindings (hash-table-map table list)]
                               [w/menus 
                                (append (hash-table-map menu-names list)
                                        (filter (λ (binding) (not (bound-by-menu? binding menu-names)))
                                                bindings))]
                               [structured-list
                                (mzlib:list:quicksort
                                 w/menus
                                 (λ (x y) (string-ci<=? (cadr x) (cadr y))))])
                          (show-keybindings-to-user structured-list this)))))
                  (bell)))]
          
          (define/private (bound-by-menu? binding menu-table)
            (ormap (λ (constituent)
                     (hash-table-get menu-table (string->symbol constituent) (λ () #f)))
                   (regexp-split #rx";" (symbol->string (car binding)))))
          
          (define/override (help-menu:before-about help-menu)
            (make-help-desk-menu-item help-menu)
            '(make-object menu-item%
               (format (string-constant welcome-to-something)
                       (string-constant drscheme))
               help-menu
               (λ (item evt)
                 (drscheme:app:invite-tour))))
          
          (define/override (help-menu:about-callback item evt) (drscheme:app:about-drscheme))
          (define/override (help-menu:about-string) (string-constant about-drscheme))
          (define/override (help-menu:create-about?) #t)
          
          (define/public (get-additional-important-urls) '())
          (define/override (help-menu:after-about menu)
            (drscheme:app:add-important-urls-to-help-menu
             menu 
             (get-additional-important-urls))
            (new menu-item%
                 (label (string-constant bug-report-submit-menu-item))
                 (parent menu)
                 (callback
                  (λ (x y)
                    (help-desk:report-bug))))
            
            (drscheme:app:add-language-items-to-help-menu menu))
          
          (define/override (file-menu:open-callback item evt) (handler:open-file))
          (define/override (file-menu:new-string) (string-constant new-menu-item))
          (define/override (file-menu:open-string) (string-constant open-menu-item))

          (define/override (file-menu:between-open-and-revert file-menu) 
            (make-object menu-item% 
              (string-constant install-plt-file-menu-item...)
              file-menu
              (λ (item evt)
                (install-plt-file this)))
            (super file-menu:between-open-and-revert file-menu))
          
          (define/override (file-menu:between-print-and-close menu)
            (super file-menu:between-print-and-close menu)
            (instantiate menu-item% ()
              (label (string-constant mfs-multi-file-search-menu-item))
              (parent menu)
              (callback
               (λ (_1 _2)
                 (drscheme:multi-file-search:multi-file-search))))
            (new separator-menu-item% (parent menu)))
          
          (define/override (edit-menu:between-find-and-preferences menu)
            (make-object separator-menu-item% menu)
            (let ([keybindings-on-demand
                   (λ (menu-item)
                     (let ([last-edit-object (get-edit-target-window)])
                       (send menu-item enable (can-show-keybindings?))))])
              (instantiate menu% ()
                (label (string-constant keybindings-menu-item))
                (parent menu)
                (demand-callback
                 (λ (keybindings-menu)
                   (for-each (λ (old) (send old delete)) 
                             (send keybindings-menu get-items))
                   (new menu-item%
                        (parent keybindings-menu)
                        (label (string-constant keybindings-show-active))
                        (callback (λ (x y) (show-keybindings)))
                        (help-string (string-constant keybindings-info))
                        (demand-callback keybindings-on-demand))
                   (new menu-item%
                        (parent keybindings-menu)
                        (label (string-constant keybindings-add-user-defined-keybindings))
                        (callback
                         (λ (x y) 
                           (let ([filename (finder:get-file #f
                                                            (string-constant keybindings-choose-user-defined-file)
                                                            #f "" this)])
                             (when filename
                               (add-keybindings-file filename))))))
                   (let ([ud (preferences:get 'drscheme:user-defined-keybindings)])
                     (unless (null? ud)
                       (new separator-menu-item% (parent keybindings-menu))
                       (for-each (λ (path)
                                   (new menu-item%
                                        (label (format (string-constant keybindings-menu-remove)
                                                       (path->string path)))
                                        (parent keybindings-menu)
                                        (callback
                                         (λ (x y) (remove-keybindings-file path)))))
                                 ud)))))))
            (unless (current-eventspace-has-standard-menus?)
              (make-object separator-menu-item% menu)))
          
          (super-new)))

      (define (add-keybindings-file path)
        (with-handlers ([exn? (λ (x)
                                (message-box (string-constant drscheme)
                                             (exn-message x)))])
          (keymap:add-user-keybindings-file path)
          (preferences:set 'drscheme:user-defined-keybindings
                           (cons path
                                 (preferences:get 'drscheme:user-defined-keybindings)))))
      (define (remove-keybindings-file path)
        (keymap:remove-user-keybindings-file path)
        (preferences:set
         'drscheme:user-defined-keybindings
         (remq path
               (preferences:get 'drscheme:user-defined-keybindings))))
      
      ;; install-plt-file : (union #f dialog% frame%) -> void
      ;; asks the user for a .plt file, either from the web or from
      ;; a file on the disk and installs it.
      (define (install-plt-file parent)
        (define dialog
          (instantiate dialog% ()
            (parent parent)
            (alignment '(left center))
            (label (string-constant install-plt-file-dialog-title))))
        (define tab-panel
          (instantiate tab-panel% ()
            (parent dialog)
            (callback (λ (x y) (update-panels)))
            (choices (list (string-constant install-plt-web-tab)
                           (string-constant install-plt-file-tab)))))
        (define outer-swapping-panel (instantiate horizontal-panel% ()
                                       (parent tab-panel)
                                       (stretchable-height #f)))
        (define spacing-panel (instantiate horizontal-panel% ()
                                (stretchable-width #f)
                                (parent outer-swapping-panel)
                                (min-width 20)))
        (define swapping-panel (instantiate panel:single% ()
                                 (parent outer-swapping-panel)
                                 (alignment '(left center))
                                 (stretchable-width #t)
                                 (stretchable-height #f)))
        (define file-panel (instantiate horizontal-panel% ()
                             (parent swapping-panel)
                             (stretchable-width #t)
                             (stretchable-height #f)))
        (define url-panel (instantiate horizontal-panel% ()
                            (parent swapping-panel)
                            (stretchable-height #f)))
        (define button-panel (instantiate horizontal-panel% ()
                               (parent dialog)
                               (stretchable-height #f)
                               (alignment '(right center))))
        (define file-text-field (instantiate text-field% ()
                                  (parent file-panel)
                                  (callback void)
                                  (min-width 300)
                                  (stretchable-width #t)
                                  (label (string-constant install-plt-filename))))
        (define file-button (instantiate button% ()
                              (parent file-panel)
                              (label (string-constant browse...))
                              (callback (λ (x y) (browse)))))
        (define url-text-field (instantiate text-field% ()
                                 (parent url-panel)
                                 (label (string-constant install-plt-url))
                                 (min-width 300)
                                 (stretchable-width #t)
                                 (callback void)))
        
        (define-values (ok-button cancel-button)
          (gui-utils:ok/cancel-buttons
           button-panel
           (λ (x y)
             (set! cancel? #f)
             (send dialog show #f))
           (λ (x y)
             (send dialog show #f))))
        
        ;; browse : -> void
        ;; gets the name of a file from the user and
        ;; updates file-text-field
        (define (browse)
          (let ([filename (get-file #f dialog)])
            (when filename
              (send file-text-field set-value (path->string filename)))))
        
        ;; from-web? : -> boolean
        ;; returns #t if the user has selected a web address
        (define (from-web?)
          (zero? (send tab-panel get-selection)))
        
        (define cancel? #t)
        
        (define (update-panels)
          (send swapping-panel active-child
                (if (from-web?)
                    url-panel
                    file-panel)))
        
        (update-panels)
        (send dialog show #t)
        
        (cond
          [cancel? (void)]
          [(from-web?)
           (install-plt-from-url (send url-text-field get-value) parent)]
          [else 
           (parameterize ([error-display-handler drscheme:init:original-error-display-handler])
             (run-installer (string->path (send file-text-field get-value))))]))

      ;; install-plt-from-url : string (union #f dialog%) -> void
      ;; downloads and installs a .plt file from the given url
      (define (install-plt-from-url s-url parent)
        (with-handlers ([(λ (x) #f)
                         (λ (exn)
                           (message-box (string-constant drscheme)
                                        (if (exn? exn)
                                            (format "~a" (exn-message exn))
                                            (format "~s" exn))))])
          (let* ([url (string->url s-url)]
                 [tmp-filename (make-temporary-file "tmp~a.plt")]
                 [port (get-impure-port url)]
                 [header (purify-port port)]
                 [size (let* ([content-header (extract-field "content-length" header)]
                              [m (and content-header
                                      (regexp-match "[0-9]+" content-header))])
                         (and m (string->number (car m))))]
                 [d (make-object dialog% (string-constant downloading) parent)] 
                 [message (make-object message% (string-constant downloading-file...) d)] 
                 [gauge (if size 
                            (make-object gauge% #f 100 d) 
                            #f)] 
                 [exn #f] 
                 ; Semaphores to avoid race conditions: 
                 [wait-to-start (make-semaphore 0)] 
                 [wait-to-break (make-semaphore 0)] 
                 ; Thread to perform the download: 
                 [t (thread 
                     (λ () 
                       (semaphore-wait wait-to-start) 
                       (with-handlers ([exn:fail?
                                        (λ (x) 
                                          (set! exn x))] 
                                       [exn:break? ; throw away break exceptions 
                                        void])
                         (semaphore-post wait-to-break) 
                         (with-output-to-file tmp-filename 
                           (λ () 
                             (let loop ([total 0]) 
                               (when gauge 
                                 (send gauge set-value  
                                       (inexact->exact 
                                        (floor (* 100 (/ total size)))))) 
                               (let ([s (read-string 1024 port)]) 
                                 (unless (eof-object? s) 
                                   (unless (eof-object? s) 
                                     (display s) 
                                     (loop (+ total (string-length s))))))))
                           'binary 'truncate))
                       (send d show #f)))]) 
            (send d center) 
            (make-object button% (string-constant &stop)
			 d
			 (λ (b e) 
			   (semaphore-wait wait-to-break) 
			   (set! tmp-filename #f) 
			   (send d show #f) 
			   (break-thread t))) 
            ; Let thread run only after the dialog is shown 
            (queue-callback (λ () (semaphore-post wait-to-start))) 
            (send d show #t) 
            (when exn (raise exn))
            (parameterize ([error-display-handler drscheme:init:original-error-display-handler])
              (run-installer tmp-filename
                             (λ ()
                               (delete-file tmp-filename)))))))
      
      
      (define keybindings-dialog%
        (class dialog%
          (override on-size)
          [define on-size
            (lambda (w h)
              (preferences:set 'drscheme:keybindings-window-size (cons w h))
              (super on-size w h))]
          (super-instantiate ())))
      
      (define (show-keybindings-to-user bindings frame)
        (letrec ([f (instantiate keybindings-dialog% ()
                      (label (string-constant keybindings-frame-title))
                      (parent frame)
                      (width (car (preferences:get 'drscheme:keybindings-window-size)))
                      (height (cdr (preferences:get 'drscheme:keybindings-window-size)))
                      (style '(resize-border)))]
                 [bp (make-object horizontal-panel% f)]
                 [b-name (make-object button% (string-constant keybindings-sort-by-name)
                           bp (λ x (update-bindings #f)))]
                 [b-key (make-object button% (string-constant keybindings-sort-by-key)
                          bp (λ x (update-bindings #t)))]
                 [lb
                  (make-object list-box% #f null f void)]
                 [bp2 (make-object horizontal-panel% f)]
                 [cancel (make-object button% (string-constant close)
                           bp2 (λ x (send f show #f)))]
                 [space (make-object grow-box-spacer-pane% bp2)]
                 [update-bindings
                  (λ (by-key?)
                    (let ([format-binding/name
                           (λ (b) (format "~a (~a)" (cadr b) (car b)))]
                          [format-binding/key
                           (λ (b) (format "~a (~a)" (car b) (cadr b)))]
                          [predicate/key
                           (λ (a b) (string-ci<=? (format "~a" (car a))
                                                       (format "~a" (car b))))]
                          [predicate/name
                           (λ (a b) (string-ci<=? (cadr a) (cadr b)))])
                      (send lb set
                            (if by-key?
                                (map format-binding/key (mzlib:list:quicksort bindings predicate/key))
                                (map format-binding/name (mzlib:list:quicksort bindings predicate/name))))))])
          (send bp stretchable-height #f)
          (send bp set-alignment 'center 'center)
          (send bp2 stretchable-height #f)
          (send bp2 set-alignment 'right 'center)
          (update-bindings #f)
          (send f show #t)))
      
      (define <%>
        (interface (frame:editor<%> basics<%> frame:text-info<%>)
          get-show-menu
          update-shown
          add-show-menu-items))
      
      (define -mixin
        (mixin (frame:editor<%> frame:text-info<%> basics<%>) (<%>)
          (inherit get-editor get-menu% get-menu-bar)
          (define show-menu #f)
          (define/public get-show-menu (λ () show-menu))
          (define/public update-shown (λ () (void)))
          (define/public (add-show-menu-items show-menu) (void))
          (super-new)
          (set! show-menu (make-object (get-menu%) (string-constant view-menu-label)
                            (get-menu-bar)))
          (add-show-menu-items show-menu)))
      
      
      (define (create-root-menubar)
        (let* ([mb (new menu-bar% (parent 'root))]
               [file-menu (new menu% 
                               (label (string-constant file-menu))
                               (parent mb))]
               [help-menu (new menu% 
                               (label (string-constant help-menu))
                               (parent mb))])
          (new menu-item%
               (label (string-constant new-menu-item))
               (parent file-menu)
               (shortcut #\n)
               (callback
                (λ (x y)
                  (handler:edit-file #f)
                  #t)))
	  (new menu-item%
               (label (string-constant open-menu-item))
               (parent file-menu)
               (shortcut #\o)
               (callback
                (λ (x y)
                  (handler:open-file)
                  #t)))
          (new menu%
               (label (string-constant open-recent-menu-item))
               (parent file-menu)
               (demand-callback
                (λ (menu)
                  (handler:install-recent-items menu))))
	  (instantiate menu-item% ()
            (label (string-constant mfs-multi-file-search-menu-item))
            (parent file-menu)
            (callback
             (λ (_1 _2)
               (drscheme:multi-file-search:multi-file-search))))
          (unless (current-eventspace-has-standard-menus?)
	    (new separator-menu-item% (parent file-menu))
	    (new menu-item%
                 (label (string-constant quit-menu-item-others))
                 (parent file-menu)
                 (shortcut #\q)
                 (callback
                  (λ (x y)
                    (when (exit:user-oks-exit)
                      (exit:exit))
                    #t))))
          (make-help-desk-menu-item help-menu)))
      
      (define (make-help-desk-menu-item help-menu)
        (make-object menu-item%
          (string-constant help-desk)
          help-menu
          (λ (item evt)
            (help:help-desk)
            #t)))
      
      
      )))
