(module client-gui mzscheme
  (require mzlib/class mzlib/unit mzlib/file net/sendurl mred
           mrlib/bitmap-label drscheme/tool framework
           "info.ss" "client.ss" "this-collection.ss")

  (provide tool@)

  (define uninstalled? #f)

  (define server:port (#%info-lookup 'server:port (lambda () #f)))
  (define-values (server port-no)
    (if server:port
      (let ([m (regexp-match #rx"^([^:]+):([0-9]+)$" server:port)])
        (unless m
          (error 'handin-client
                 "Bad configuration ~s, expecting \"server:port\""
                 server:port))
        (values (cadr m) (string->number (caddr m))))
      (values #f #f)))

  (define handin-name   (#%info-lookup 'name))
  (define web-menu-name (#%info-lookup 'web-menu-name (lambda () #f)))
  (define web-address   (#%info-lookup 'web-address   (lambda () #f)))

  (define password-keep-minutes
    (#%info-lookup 'password-keep-minutes (lambda () #f)))

  (define handin-dialog-name (string-append handin-name " Handin"))
  (define button-label/h     (string-append handin-name " Handin"))
  (define button-label/r     (string-append handin-name " Retrieve"))
  (define manage-dialog-name (string-append handin-name " Handin Account"))

  (define updater?
    (#%info-lookup 'enable-auto-update (lambda () #f)))
  (define multifile?
    (#%info-lookup 'enable-multifile-handin (lambda () #f)))

  (define preference-key (make-my-key 'submit:username))

  (preferences:set-default preference-key "" string?)
  (define (remembered-user)
    (preferences:get preference-key))
  (define (remember-user user)
    (preferences:set preference-key user))

  (define (connect) (handin-connect server port-no))

  ;; parameter-like procedure that keeps the password cached for a few minutes
  (define cached-password
    (let ([passwd #f]
          [timer #f])
      (define protect
        (let ([s (make-semaphore 1)])
          (lambda (thunk)
            (dynamic-wind (lambda () (semaphore-wait s))
                          thunk
                          (lambda () (semaphore-post s))))))
      (case-lambda
        [() passwd]
        [(new)
         (protect (lambda ()
                    (when (and password-keep-minutes
                               (not (equal? 0 password-keep-minutes))
                               (not (equal? passwd new)))
                      (when timer (kill-thread timer))
                      (set! passwd new)
                      (set! timer (thread
                                   (lambda ()
                                     (sleep (* 60 password-keep-minutes))
                                     (protect (lambda ()
                                                (set! passwd #f)
                                                (set! timer #f)))))))))])))
  ;; a password entry box that uses the one cached above
  (define cached-passwd%
    (class text-field%
      (define cached (cached-password))
      ;; use this instead of a cached password -- to avoid copy/pastes
      ;; of a password, and to hide its length
      (define fake-value "CACHED PASSWD")
      (define/override (get-value)
        (or cached (super get-value)))
      (define/override (on-focus on?)
        (if on?
          ;; got focus -- clear out bogus contents, if any
          (when cached (send this set-value "") (set! cached #f))
          ;; lost focus -- remember a new password, or restore it
          (let ([p (super get-value)])
            (cond [(and p (not (string=? "" p)))
                   ;; don't behave as if we have a cache: don't clear
                   ;; the value now, or if we get the focus later
                   (set! cached #f)
                   (cached-password p)]
                  [(cached-password)
                   => (lambda (p)
                        (set! cached p)
                        (send this set-value fake-value))]))))
      (super-new [init-value (if cached fake-value "")]
                 [style '(single password)])))

  (provide handin-frame%)
  (define handin-frame%
    (class dialog%
      (inherit show is-shown? center)
      (super-new [label handin-dialog-name])

      (init-field content on-retrieve)
      (define mode
        (cond [(and content on-retrieve) #f]
              [content     'submit]
              [on-retrieve 'retrieve]
              [else (error 'handin-frame "bad initial values")]))

      (define status (new message%
			  [label (format "Making secure connection to ~a..." server)]
			  [parent this]
			  [stretchable-width #t]))
      (define username (new text-field%
			    [label "Username:"]
			    [init-value (remembered-user)]
			    [parent this]
			    [callback (lambda (t e) (activate-ok))]
			    [stretchable-width #t]))
      (define passwd (new cached-passwd%
                          [label "Password:"]
                          [parent this]
                          [callback (lambda (t e) (activate-ok))]
                          [stretchable-width #t]))
      (define assignment (new choice%
			      [label "Assignment:"]
			      [choices null]
			      [parent this]
			      [callback void]
			      [stretchable-width #t]))

      (define button-panel (new horizontal-pane%
				[parent this]
				[stretchable-height #f]))
      (make-object vertical-pane% button-panel) ; spacer

      (define retrieve?
        (new check-box%
             [label "Retrieve"]
             [parent button-panel]
             [callback (lambda _
                         (define r? (send retrieve? get-value))
                         (send ok set-label
                               (if r? button-label/r button-label/h)))]
             [value (eq? 'retrieve mode)]
             [style (if mode '(deleted) '())]))

      (define (submit-file)
        (define final-message "Handin successful.")
        (submit-assignment
         connection
         (send username get-value)
         (send passwd get-value)
         (send assignment get-string (send assignment get-selection))
         content
         ;; on-commit
         (lambda ()
           (semaphore-wait commit-lock)
           (send status set-label "Committing...")
           (set! committing? #t)
           (semaphore-post commit-lock))
         ;; message/message-final/message-box handlers
         (lambda (msg) (send status set-label msg))
         (lambda (msg) (set! final-message msg))
         (lambda (msg styles) (message-box "Handin" msg this styles)))
        (queue-callback
         (lambda ()
           (when abort-commit-dialog (send abort-commit-dialog show #f))
           (send status set-label final-message)
           (set! committing? #f)
           (done-interface))))
      (define (retrieve-file)
        (let ([buf (retrieve-assignment
                    connection
                    (send username get-value)
                    (send passwd get-value)
                    (send assignment get-string (send assignment get-selection)))])
          (queue-callback
           (lambda ()
             (done-interface)
             (do-cancel-button)
             (on-retrieve buf)))))

      (define ok
        (new button%
          [label (case mode
                   [(submit)   button-label/h]
                   [(retrieve) button-label/r]
                   [else (string-append " " button-label/h " ")])] ; can change
          [parent button-panel]
          [style '(border)]
          [callback
           (lambda (b e)
             (disable-interface)
             (send status set-label "Handing in...")
             (parameterize ([current-custodian comm-cust])
               (thread
                (lambda ()
                  (remember-user (send username get-value))
                  (with-handlers ([void (lambda (exn)
                                          (report-error "Handin failed." exn))])
                    (if (send retrieve? get-value)
                      (retrieve-file)
                      (submit-file)))))))]))

      (define ok-can-enable? #f)
      (define (activate-ok)
	(send ok enable (and ok-can-enable?
			     (not (string=? "" (send username get-value)))
			     (not (string=? "" (send passwd get-value))))))

      (define cancel  (new button%
			   [label "Cancel"]
			   [parent button-panel]
			   [callback (lambda (b e) (do-cancel-button))]))
      (define (do-cancel-button)
	(let ([go? (begin
		     (semaphore-wait commit-lock)
		     (if committing?
			 (begin
			   (semaphore-post commit-lock)
			   (send abort-commit-dialog show #t)
			   continue-abort?)
			 #t))])
	  (when go?
	    (custodian-shutdown-all comm-cust)
	    (show #f))))

      (define continue-abort? #f)
      (define abort-commit-dialog
	(let ([d (make-object dialog% "Commit in Progress")])
	  (make-object message% "The commit action is in progress." d)
	  (make-object message% "Cancelling now may or may not work." d)
	  (make-object message% "Cancel anyway?" d)
	  (let ([b (new horizontal-panel%
			[parent d]
			[stretchable-height #f]
			[alignment '(center center)])])
	    (make-object button% "Continue Commit" d (lambda (b e) (send d show #f)))
	    (make-object button% "Try to Cancel" d (lambda (b e)
						     (set! continue-abort? #t)
						     (send d show #f))))))

      (define interface-widgets
        (list ok username passwd assignment retrieve?))
      (define (disable-interface)
        (for-each (lambda (x) (send x enable #f)) interface-widgets))
      (define (enable-interface)
        (for-each (lambda (x) (send x enable #t)) interface-widgets))
      (define (done-interface)
        (send cancel set-label "Close")
        (send cancel focus))

      (define (report-error tag exn)
	(queue-callback
	 (lambda ()
	   (let* ([msg (if (exn? exn)
                         (let ([s (exn-message exn)])
                           (if (string? s)
                             s
                             (format "~e" s)))
                         (format "~e" exn))]
                  [retry? (regexp-match #rx"bad username or password for" msg)])
             (custodian-shutdown-all comm-cust)
	     (set! committing? #f)
             (disable-interface)
             (send status set-label tag)
             (when (is-shown?)
               (message-box "Server Error" msg this)
               (if retry?
                 (begin (init-comm) (semaphore-post go-sema) (enable-interface))
                 (done-interface)))))))

      (define go-sema #f)
      (define commit-lock #f)
      (define committing? #f)

      (define connection #f)

      (define comm-cust #f)
      (define (init-comm)
        (set! go-sema (make-semaphore 1))
        (set! commit-lock (make-semaphore 1))
        (set! comm-cust (make-custodian))
	(parameterize ([current-custodian comm-cust])
	  (thread
           (lambda ()
             (let/ec escape
               (with-handlers ([void
                                (lambda (exn)
                                  (report-error "Connection failed." exn)
                                  (escape))])
                 (semaphore-wait go-sema)
                 (let* ([h (connect)]
                        [l (retrieve-active-assignments h)])
                   (when (null? l)
                     (handin-disconnect h)
                     (error 'handin "there are no active assignments"))
                   (set! connection h)
                   (for-each (lambda (assign) (send assignment append assign))
                             l)
                   (send assignment enable #t)
                   (set! ok-can-enable? #t)
                   (activate-ok)
                   (send status set-label
                         (format "Connected securely for ~a." handin-name)))))))))

      (define/augment (on-close)
	(inner (void) on-close)
	(do-cancel-button))

      (init-comm)

      (send (cond [(string=? "" (send username get-value)) username]
                  [(string=? "" (send passwd get-value)) passwd]
                  [else ok])
            focus)
      (send ok enable #f) ; disable after focus possibly sent to it
      (send assignment enable #f)

      (center)
      (show #t)))

  (provide manage-handin-dialog%)
  (define manage-handin-dialog%
    (class dialog% (init [parent #f])

      (inherit show is-shown? center)
      (super-new [label manage-dialog-name]
                 [alignment '(left center)]
                 [parent parent])

      (define user-fields (get-user-fields parent))

      (define status
        (new message%
             [label (if user-fields
                      (format "Manage ~a handin account at ~a."
                              handin-name server)
                      "No connection to server!")]
             [parent this]
             [stretchable-width #t]))

      (define tabs
        (let* ([names (list (if multifile? "Un/Install" "Uninstall"))]
               [names (if user-fields
                        `("New User" "Change Info" ,@names) names)]
               [callback (lambda _
                           (send single active-child
                                 (if user-fields
                                   (case (send tabs get-selection)
                                     [(0) new-user-box]
                                     [(1) old-user-box]
                                     [(2) un/install-box]
                                     [else (error "internal error")])
                                   un/install-box)))])
          (new tab-panel% [parent this] [choices names] [callback callback])))

      (define single (new panel:single% [parent tabs]))

      (define (mk-txt label parent activate-ok)
        (new text-field%
             [label label]
             [parent parent]
             [callback (lambda (t e) (activate-ok))]
             [stretchable-width #t]))

      (define (mk-passwd label parent activate-ok)
        (new text-field%
             [label label]
             [parent parent]
             [callback (lambda (t e) (activate-ok))]
             [style '(single password)]
             [stretchable-width #t]))

      (define (non-empty? . ts)
        (andmap (lambda (t) (not (string=? "" (send t get-value)))) ts))

      (define (same-value t1 t2)
        (string=? (send t1 get-value) (send t2 get-value)))

      (define (activate-change)
        (define an-extra-non-empty? (ormap non-empty? change-user-fields))
        (send retrieve-old-info-button enable
              (non-empty? old-username old-passwd))
        (send change-button enable
              (and (same-value new-passwd new-passwd2)
                   (non-empty? old-username old-passwd)
                   (or (non-empty? new-passwd) an-extra-non-empty?)))
        (send change-button set-label
              (if an-extra-non-empty? "Change Info" "Set Password")))

      (define old-user-box (new vertical-panel%
                                [parent single] [alignment '(center center)]))
      (define old-username (mk-txt "Username:" old-user-box activate-change))
      (send old-username set-value (remembered-user))

      (define old-passwd
        (new cached-passwd%
             [label "Old Password:"]
             [parent old-user-box]
             [callback (lambda (t e) (activate-change))]
             [stretchable-width #t]))
      (define change-user-fields
        (map (lambda (f)
               (mk-txt (string-append f ":") old-user-box activate-change))
             (or user-fields '())))
      (define new-passwd
        (mk-passwd "New Password:" old-user-box activate-change))
      (define new-passwd2
        (mk-passwd "New Password again:" old-user-box activate-change))

      (define-values (retrieve-old-info-button change-button)
        (let ([p (new horizontal-pane%
                      [parent old-user-box]
                      [stretchable-height #f]
                      [alignment '(center center)])])
          (make-object vertical-pane% p)
          (values
           (begin0 (new button%
                        [label "Get Current Info"] [parent p]
                        [callback (lambda (b e) (do-retrieve old-username))])
             (make-object vertical-pane% p))
           (begin0 (new button%
                        [label "Set Password"] [parent p] [style '(border)]
                        [callback (lambda (b e)
                                    (do-change/add #f old-username))])
             (make-object vertical-pane% p)))))

      (define (activate-new)
        (send new-button enable
              (and (apply non-empty? new-username add-passwd add-passwd2
                          add-user-fields)
                   (same-value add-passwd add-passwd2))))
      (define new-user-box (new vertical-panel%
                                [parent single] [alignment '(center center)]))
      (define new-username (mk-txt "Username:" new-user-box activate-new))
      (send new-username set-value (remembered-user))
      (define add-user-fields
        (map (lambda (f)
               (mk-txt (string-append f ":") new-user-box activate-new))
             (or user-fields '())))
      (define add-passwd
        (mk-passwd "Password:" new-user-box activate-new))
      (define add-passwd2
        (mk-passwd "Password again:" new-user-box activate-new))
      (define new-button (new button%
                              [label "Add User"] [parent new-user-box]
                              [callback (lambda (b e)
                                          (do-change/add #t new-username))]
                              [style '(border)]))

      (define un/install-box
        (new vertical-panel% [parent single] [alignment '(center center)]))
      (define uninstall-button
        (new button%
          [label (format "Uninstall ~a Handin" handin-name)]
          [parent un/install-box]
          [callback
           (lambda (b e)
             (with-handlers ([void (lambda (exn)
                                     (report-error "Uninstall failed." exn))])
               (delete-directory/files (in-this-collection))
               (set! uninstalled? #t)
               (send uninstall-button enable #f)
               (message-box "Uninstall"
                 (format "The ~a tool has been uninstalled. ~a~a"
                         handin-name
                         "The Handin button and associated menu items will"
                         " not appear after you restart DrScheme.")
                 this)
               (send this show #f)))]))
      (send uninstall-button enable (not uninstalled?))

      (define install-standalone-button
        (and multifile?
             (new button%
               [label (format "Install Standalone ~a Handin" handin-name)]
               [parent un/install-box]
               [callback
                (lambda (b e)
                  (define (launcher sym)
                    (dynamic-require `launcher sym))
                  (let* ([exe (let-values
                                  ([(dir name dir?)
                                    (split-path
                                     ((launcher 'mred-program-launcher-path)
                                      (format "~a Handin" handin-name)))])
                                (path->string name))]
                         [dir (get-directory
                               (format "Choose a directory to create the ~s~a"
                                       exe " executable in")
                               #f)])
                    (when (and dir (directory-exists? dir))
                      (parameterize ([current-directory dir])
                        (when (or (not (file-exists? exe))
                                  (eq? 'ok
                                       (message-box
                                        "File Exists"
                                        (format
                                         "The ~s executable already exists, ~a"
                                         exe "it will be overwritten")
                                        this '(ok-cancel caution))))
                          ((launcher 'make-mred-launcher)
                           (list "-mvLe-" "handin-multi.ss"
                                 this-collection-name
                                 "(multifile-handin)")
                           (build-path dir exe))
                          (message-box "Standalone Executable"
                                       (format "~s created" exe)
                                       this)
                          (send this show #f))))))])))

      (define (report-error tag exn)
        (queue-callback
         (lambda ()
           (custodian-shutdown-all comm-cust)
           (send status set-label tag)
           (when (is-shown?)
             (message-box
              "Server Error"
              (if (exn? exn)
                (let ([s (exn-message exn)])
                  (if (string? s) s (format "~e" s))))
              this)
             (set! comm-cust (make-custodian))))))

      (define comm-cust (make-custodian))
      (define/augment (on-close)
        (inner (void) on-close)
        (custodian-shutdown-all comm-cust))

      (define button-panel
        (new horizontal-pane% [parent this] [stretchable-height #f]))
      (make-object vertical-pane% button-panel) ; spacer
      (define cancel
        (new button%
             [label "Cancel"] [parent button-panel]
             [callback (lambda (b e)
                         (custodian-shutdown-all comm-cust)
                         (show #f))]))

      ;; Too-long fields can't damage the server, but they might result in
      ;;  confusing errors due to safety cut-offs on the server side.
      (define (check-length field size name k)
        (when ((string-length (send field get-value)) . > . size)
          (message-box "Error"
                       (format "The ~a must be no longer than ~a characters."
                               name size))
          (k (void))))

      (define (do-change/add new? username)
        (let/ec k
          (check-length username 50 "Username" k)
          (let* ([pw1 (if new? new-passwd  add-passwd)]
                 [pw2 (if new? new-passwd2 add-passwd2)]
                 [l1 (regexp-replace #rx" *:$" (send pw1 get-label) "")]
                 [l2 (regexp-replace #rx" *:$" (send pw2 get-label) "")])
            (check-length pw1 50 l1 k)
            ;; not really needed, but leave just in case
            (unless (string=? (send pw1 get-value) (send pw2 get-value))
              (message-box "Password Error"
                (format "The \"~a\" and \"~a\" passwords are not the same."
                        l1 l2))
              (k (void))))
          (for-each (lambda (t f) (check-length t 100 f k))
                    (if new? add-user-fields change-user-fields)
                    (or user-fields '()))
          (send tabs enable #f)
          (parameterize ([current-custodian comm-cust])
            (thread
             (lambda ()
               (with-handlers ([void (lambda (exn)
                                       (send tabs enable #t)
                                       (report-error
                                        (format "~a failed."
                                                (if new? "Creation" "Update"))
                                        exn))])
                 (remember-user (send username get-value))
                 (send status set-label "Making secure connection...")
                 (let ([h (connect)])
                   (define (run proc . fields)
                     (apply proc h
                            (let loop ([x fields])
                              (if (list? x) (map loop x) (send x get-value)))))
                   (send status set-label
                         (if new? "Creating user..." "Updating server..."))
                   (if new?
                     (run submit-addition username add-passwd add-user-fields)
                     (run submit-info-change username old-passwd new-passwd
                          change-user-fields)))
                 (send status set-label "Success.")
                 (send cancel set-label "Close")))))))

      (define (do-retrieve username)
        (let/ec k
          (send tabs enable #f)
          (parameterize ([current-custodian comm-cust])
            (thread
             (lambda ()
               (with-handlers ([void (lambda (exn)
                                       (send tabs enable #t)
                                       (report-error "Retrieve failed." exn))])
                 (remember-user (send username get-value))
                 (send status set-label "Making secure connection...")
                 (let ([h (connect)])
                   (define (run proc . fields)
                     (apply proc h
                            (let loop ([x fields])
                              (if (list? x) (map loop x) (send x get-value)))))
                   (send status set-label "Retrieving information...")
                   (let ([vals (run retrieve-user-info username old-passwd)])
                     (send status set-label "Success, you can now edit fields.")
                     (send tabs enable #t)
                     (for-each (lambda (f val) (send f set-value val))
                               change-user-fields vals)
                     (activate-change)))))))))

      (send new-user-box show #f)
      (send old-user-box show #f)
      (send un/install-box show #f)
      (let ([new? (equal? "" (remembered-user))])
        (if user-fields
          (send* single (active-child (if new? old-user-box new-user-box))
                        (active-child (if new? new-user-box old-user-box)))
          (send single active-child un/install-box))
        (send tabs set-selection (if user-fields (if new? 0 1) 0)))
      (activate-new)
      (activate-change)
      (center)
      (show #t)))

  ;; A simple dialog during connection, with an option to cancel (used
  ;; by `get-user-fields' below, since its value is needed to
  ;; construct the above dialog).
  (define connection-dialog%
    (class dialog% (init receiver [parent #f])
      (inherit show is-shown? center)
      (super-new [label manage-dialog-name]
                 [alignment '(right center)]
                 [parent parent])
      (define status
        (new message% [label "Connecting to server..."]
                      [parent this]
                      [stretchable-width #t]))
      (define comm-cust (make-custodian))
      (define/augment (on-close)
        (inner (void) on-close)
        (custodian-shutdown-all comm-cust))
      (define button
        (new button% [label "Cancel"] [parent this]
                     [callback (lambda (b e)
                                 (custodian-shutdown-all comm-cust)
                                 (show #f))]
                     [style '(border)]))
      (send button focus)
      (parameterize ([current-custodian comm-cust])
        (thread
         (lambda ()
           (unless (with-handlers ([void (lambda (_) #f)])
                     (receiver (connect)) #t)
             (begin (send status set-label "Connection failure!")
                    ;; (send button enable #f)
                    (sleep 5)))
           (queue-callback (lambda () (show #f))))))
      (center)
      (show #t)))

  (define cached-user-fields #f)
  (define (get-user-fields parent)
    (unless cached-user-fields
      (new connection-dialog%
           [receiver (lambda (h)
                       (set! cached-user-fields (retrieve-user-fields h)))]
           [parent parent]))
    cached-user-fields)

  (define (scale-by-half file)
    (let* ([bm (make-object bitmap% file 'unknown/mask)]
	   [w (send bm get-width)]
	   [h (send bm get-height)]
	   [bm2 (make-object bitmap% (quotient w 2) (quotient h 2))]
	   [mbm2 (and (send bm get-loaded-mask)
		      (make-object bitmap% (quotient w 2) (quotient h 2)))]
	   [mdc (make-object bitmap-dc% bm2)])
      (send mdc draw-bitmap-section-smooth bm 
	    0 0 (quotient w 2) (quotient h 2)
	    0 0 w h)
      (send mdc set-bitmap #f)
      (when mbm2
	(send mdc set-bitmap mbm2)
	(send mdc draw-bitmap-section-smooth (send bm get-loaded-mask)
	      0 0 (quotient w 2) (quotient h 2)
	      0 0 w h)
	(send mdc set-bitmap #f)
	(send bm2 set-loaded-mask mbm2))
      bm2))

  (define handin-icon (scale-by-half (in-this-collection "icon.png")))

  (define (editors->string editors)
    (let* ([base (make-object editor-stream-out-bytes-base%)]
	   [stream (make-object editor-stream-out% base)])
      (write-editor-version stream base)
      (write-editor-global-header stream)
      (for-each (lambda (ed)
		  (send ed write-to-file stream))
		editors)
      (write-editor-global-footer stream)
      (send base get-bytes)))

  (define (string->editor! str defs)
    (let* ([base (make-object editor-stream-in-bytes-base% str)]
           [stream (make-object editor-stream-in% base)])
      (read-editor-version stream base #t)
      (read-editor-global-header stream)
      (send defs read-from-file stream)
      (read-editor-global-footer stream)))

  (define tool@
    (unit
      (import drscheme:tool^)
      (export drscheme:tool-exports^)

      (define phase1 void)
      (define phase2
        (if updater?
          (dynamic-require `(lib "updater.ss" ,this-collection-name) 'bg-update)
          void))

      (define tool-button-label (bitmap-label-maker button-label/h handin-icon))

      (define (make-new-unit-frame% super%)
	(class super%
	  (inherit get-button-panel
		   get-definitions-text
		   get-interactions-text)
	  (super-instantiate ())

          (define/override (file-menu:between-open-and-revert file-menu)
            ;; super adds a separator, add this and another sep after that
            (super file-menu:between-open-and-revert file-menu)
            (new menu-item%
              [label (format "Manage ~a Handin Account..." handin-name)]
              [parent file-menu]
              [callback (lambda (m e)
                          (new manage-handin-dialog% [parent this]))])
            (when multifile?
              (new menu-item%
                [label (format "Submit multiple ~a Files..." handin-name)]
                [parent file-menu]
                [callback (lambda (m e)
                            ((dynamic-require
                              `(lib "handin-multi.ss" ,this-collection-name)
                              'multifile-handin)))]))
            (when updater?
              (new menu-item%
                [label (format "Update ~a plugin..." handin-name)]
                [parent file-menu]
                [callback
                 (lambda (m e)
                   ((dynamic-require `(lib "updater.ss" ,this-collection-name)
                                     'update)
                    #f #t))])) ; no parent
            (new separator-menu-item% [parent file-menu]))

          (define/override (help-menu:after-about menu)
	    (when web-menu-name
	      (new menu-item%
		   (label web-menu-name)
		   (parent menu)
		   (callback (lambda (item evt)
			       (send-url web-address)))))
            (super help-menu:after-about menu))

	  (define button
            (new button%
              [label (tool-button-label this)]
              [parent (get-button-panel)]
              [style '(deleted)]
              [callback
               (lambda (button evt)
                 (let ([content (editors->string
                                 (list (get-definitions-text)
                                       (get-interactions-text)))])
                   (new handin-frame%
                     [parent this]
                     [content content]
                     [on-retrieve
                      (lambda (buf)
                        (string->editor!
                         buf
                         (send (drscheme:unit:open-drscheme-window)
                               get-editor)))])))]))

	  (send (get-button-panel) change-children
		(lambda (l) (cons button l)))))

      (when (and server port-no)
        (drscheme:get/extend:extend-unit-frame make-new-unit-frame% #f)))))
