(in-package :inspector)

#| ######################################################################

 MCL Inspector extensions

Copyright © 1994-97 Michael Travers 

Permission is given to use and modify this code
as long as the copyright notice is preserved.

Send questions, comments, and fixes to mt@media.mit.edu.

-------------------------------------------------------------------------

 Extends the inspector to provide display of hash tables, class properties,
 pixmaps, adds a few commands for strings and pathnames.

 Requires: pixmap-utils (loaded on demand), mt-utils

 See also: read-eval-inspect

Todos:
- hash table inspector: sorting, and it would be nice if there was a way
  to inspect the keys as well as the values.
- display methods for slots of structures and objects (ie, control base
  of numbers)
- see file inspector todos below

History:  
6/24/95 14:51  Backtrace windows include process name 
7/10/95 22:02  Fix problem with inspecting a wildcarded pathname
8/22/95 23:32  leave out some stuff for MCL2
9/13/95 16:14  file info inspector
10/14/95 18:28 region inspect
11/7/95 12:45  pict inspect
5/13/97 17:23  inspect key command for ht inspector
7/6/97 11:06   cleanup for release
###################################################################### |#

;;; Fonts 

;(based on alanr's inspector-defaults.lisp)

(defvar *inspector-default-font-spec* '("geneva" 9))

(defclass inspector-window (undo-view-mixin window)
  ((selected-pane :initform nil :accessor selected-pane)
   (user-title :accessor user-title))
  (:default-initargs :window-title nil :view-font *inspector-default-font-spec*))

(defclass backtrace-view (view)
  ((info :initarg :info :accessor info))
  (:default-initargs :view-font *inspector-default-font-spec*))

;;; we have to reverse the font change for the process inspector, since it
;;; expects a fixed-width font. 
#+CCL-3
(defun ccl::inspect-processes ()
  (let* ((title "Processes")
         (w (find-window title 'inspector-window)))
    (flet ((get-process-list (&optional ignore)
             (declare (ignore ignore))
             (cons #.(format nil "Name~30tState~50tPriority~60tIdle~67t% Utilization")
                   (copy-list ccl::*all-processes*))))
      (if w
        (progn (window-select w) (resample w))
        (make-instance 'inspector-window
          :edit-value-button nil
          :view-font '("Monaco" 9)
          :inspector (make-instance 'processes-inspector
                       :window-title title
                       :object (get-process-list)
                       :print-function 'inspect-processes-print-function
                       :line-n-function #'(lambda (i n)
                                            (let ((o (inspector-object i)))
                                              (if (eql n 0)
                                                (values nil (elt o 0) '(:comment (:underline)))
                                                (values (elt o n) nil :static))))
                       :resample-function #'get-process-list
                       :replace-object-p nil
                       :setf-line-n-p nil))))))

;;; Pixmaps

;;; include a hook to RECORD-SPECIFIC-COMMANDS
(defmethod inspector-commands ((i record-inspector))
  (let ((macptr (inspector-object i)))
    `(("Choose different record type"
       ,#'(lambda ()
            (let ((record-type (choose-record-type macptr t)))
              (make-inspector-window
               (if record-type
                 (make-instance 'record-inspector 
                                :object macptr :record-type record-type)
                 (make-instance 'uvector-inspector :object macptr))))))
      ,@(record-specific-commands macptr (record-type i))
      ,@(macptr-commands macptr)
      ("Inspect as a MACPTR"
       ,#'(lambda () (make-inspector-window
                      (make-instance 'uvector-inspector :object macptr)))))))

;;; Just handles pixmaps now, but there is room for more.
;;; +++ yes, this should be data driven.
(defun record-specific-commands (record type)
  (case type
    (:pixmap `(("View the Pixmap" ,#'(lambda () 
                                       (require :pixmap-utils)
                                       (funcall 'inspect-pm record)))))
    (:picture `(("View the PICT" ,#'(lambda () 
                                       (require :pixmap-utils)
                                       (funcall 'inspect-pict record)))))
    (:region `(("View the Region" ,#'(lambda () 
                                       (inspect-region record)))))
    (t nil)))

(defclass region-inspect-window (window)
  ((region :initarg :region))
  )

(defmethod view-draw-contents ((w region-inspect-window))
  (with-slots (region) w
    (#_PaintRgn region)))

(defun inspect-region (region)
  (let* ((w (max 50 (mt:rref* region :region.rgnbbox.right)))
         (h (max 50 (mt:rref* region :region.rgnbbox.bottom))))
    (make-instance 'region-inspect-window
      :region region :view-size (make-point w h) :window-title (princ-to-string region))))


;;; Hash tables

;;; we have to copy the contents to avoid the table getting rearranged out from
;;; under us.

;;; sorting might be nice...

(defclass hash-table-inspector (inspector)
  ((ht-contents)))

(defmethod compute-line-count ((i hash-table-inspector))
  (with-slots (ht-contents object) i
    (setf ht-contents (ht-contents object))
    (+ 4 (length ht-contents))))

(defun ht-contents (ht)
  (let ((result nil))
    (maphash #'(lambda (key value)
                 (push (cons key value) result))
           ht)
    result))

#|
;;; Sorted output, experimental 
(defun universal-< (a b)
  (cond ((and (numberp a) (numberp b))
         (< a b))
        ((and (string a) (string b))
         (string-lessp (string a) (string b)))
        ((eq (type-of a) (type-of b))
         (< (sxhash a) (sxhash b)))
        (t (string-lessp (string (type-of a)) (string (type-of b))))))
             
(defun ht-contents (ht)
  (let ((result nil))
    (maphash #'(lambda (key value)
                 (push (cons key value) result))
           ht)
    (sort result #'universal-< :key #'car)))
|#

(defmethod line-n ((i hash-table-inspector) n)
  (let* ((hash-table (inspector-object i)))
    (cond ((eql 0 n) (values hash-table))
          ((eql 1 n) (values (hash-table-test hash-table) "Test: " :static))
          ((eql 2 n) (values (hash-table-count hash-table) "Count: " :static))
          ((eql 3 n) (values  (hash-table-size hash-table) "Size: " :static))
          (t (let ((item (nth (- n 4) (slot-value i 'ht-contents))))
               (values (cdr item) (car item) :colon))))))

(defmethod (setf line-n) (new-value (i hash-table-inspector) n)
  (with-slots (ht-contents object) i
    (if (eql n 0)
      (replace-object i new-value)
      (if (< n 4)
        (setf-line-n-out-of-range i n)
        (let ((item (nth (- n 4) ht-contents)))
          (setf (cdr item) new-value)
          (setf (gethash (car item) object) new-value)
          (resample-it))))))

(defmethod inspector-class ((obj hash-table))
  'hash-table-inspector)

(defmethod inspector-commands ((ht hash-table-inspector))
  (with-slots (ht-contents object) ht
    (let* ((view (inspector-view ht))
           (selection (selection view))
           (item (and selection
                      (>= selection 4)
                      (elt ht-contents (- selection 4)))))
      (when item
        `(("Inspect Key"
           ,#'(lambda () 
                (inspect (car item)))))))))


;;; CLOS class inspector and misc 

(require :clos-hacks)

(defmethod inspector-commands ((c class))
  `(("All Method Names"
     ,#'(lambda () (inspect (ccl::class-generic-function-names c))))
    ("All Methods"
     ,#'(lambda () (inspect (ccl::class-methods c))))
    ("Direct Methods"
     ,#'(lambda () (inspect (ccl::class-direct-methods c))))
    ("Superclasses"
     ,#'(lambda () (inspect (mt:superclasses c))))
    ("Direct Superclasses"
     ,#'(lambda () (inspect (class-direct-superclasses c))))
    ("Subclasses"
     ,#'(lambda () (inspect (mt:subclasses c))))
    ("Direct Subclasses"
     ,#'(lambda () (inspect (ccl::class-direct-subclasses c))))
    ("Initargs"
     ,#'(lambda () (inspect (ccl::class-make-instance-initargs c))))))

(defmethod inspector-commands ((m standard-method))
  `(,@(if (edit-definition-p m)
        `(("Edit Definition"
           ,#'(lambda () (edit-definition m)))))))

;;; Needed for the above to work
(defmethod inspector-commands ((i standard-object-inspector))
  (let ((*inspector* i))
    (inspector-commands (inspector-object i))))


;;; the following functions used to be in clos-hacks.lisp; I moved
;;; them here to make public release easier.

;;; This magic variable must be on to record class -> method relations (it's
;;; not retroactive).
(setq ccl::*maintain-class-direct-methods* t)

(defmethod ccl::class-methods ((c standard-class))
  (mt:mapappend #'ccl::class-direct-methods (mt:superclasses c)))

(defmethod ccl::class-generic-function-names ((c standard-class))
  (remove-duplicates (mapcar #'method-name (ccl::class-methods c))))

;;; this is a MOP function (missing in MCL right now)
(defmethod ccl::class-direct-methods ((c class))
  (mt:mapappend #'(lambda (x) (ccl::population-data x))
                (ccl::%class-direct-methods c)))

(defmethod ccl::class-direct-methods ((c (eql (find-class t))))
  nil)

;;; Strings and Pathnames

(defmethod inspector-commands ((s string))
  `(("Display in a Fred Window"
     ,#'(lambda () (let ((w (make-instance 'fred-window
                              :window-show nil
                              :wrap-p t
                              :scratch-p t)))
                     (stream-write-string w s 0 (length s))
                     (window-select w))))
    ("Open named file in Fred"
     ,#'(lambda () 
          (let ((file (ccl::find-file-methods s)))
            (if file 
              (ed file)
              (beep)))))
    ("File Info"
     ,#'(lambda () (inspect-file-info s)))
    ))

(defmethod inspector-commands ((s pathname))
  (if (ignore-errors (probe-file s))
    `(("Open named file in Fred"
       ,#'(lambda () (ed s)))
      ("File Info"
       ,#'(lambda () (inspect-file-info s)))
    )))


;;; File inspector (unfinished)

(defclass file-inspector (inspector)
  ())

(defmethod line-n ((i file-inspector) n)
  (let* ((f (inspector-object i))
         (directory? (directory-pathname-p f)))
    (case n
      (0 f)
      (1 (values (mt:date-time-string (file-create-date f)) "Created" :colon))
      (2 (values (mt:date-time-string (file-write-date f)) "Written" :colon))
      (3 (if directory?
           (values :n/a "Creator: " :static)
           (values (mac-file-creator f) "Creator" :colon)))
      (4 (if directory?
           (values :n/a "Type: " :static)
           (values (mac-file-type f) "Type" :colon)))
      (5 (values (make-pathname :directory
                                (if directory?
                                  (butlast (pathname-directory f))
                                  (pathname-directory f)))
                 "Directory: " :static)))))

(defmethod compute-line-count ((i file-inspector))
  6)

(defmethod (setf line-n) (new-value (i file-inspector) n)
  (with-slots (object) i
    (case n
      (0 (replace-object i new-value))
      (3 (set-mac-file-creator object new-value))
      (4 (set-mac-file-type object new-value)))))


(defun inspect-file-info (f)
  (if (ignore-errors (probe-file f))
    (make-inspector-window (make-instance 'file-inspector :object f))
    (error "~A is not a real file" f)))

(defvar *file-info-menu-item* (make-instance 'window-menu-item
                                :menu-item-title "File Info"
                                :menu-item-action 'window-file-info
                                :update-function 'file-info-item-update))

(defmethod window-file-info ((w fred-window))
  (inspect-file-info (window-filename w)))

(defun file-info-item-update (item)
  (let ((front-window (front-window)))
    (if (and front-window
             (method-exists-p 'window-file-info front-window)
             (window-filename front-window))
      (menu-item-enable item)
      (menu-item-disable item))))

(mt:add-menu-item-at *file-menu* *file-info-menu-item* 9)       

#|
More info
- files sizes (data and resource fork)
- version
- directory
  DONE (but goes through pathname...have to fix that up)
- locked bit etc

Hooks:
- pathname/string inspector

Commands: 
- delete
- rename
- edit in Fred
- directory (ie list files)
|#

