git.fiddlerwoaroof.com
Raw Blame History
#!/usr/local/bin/sbcl --script
(declaim (optimize (speed 0) (safety 3) (debug 3)))
(load #p"~/quicklisp/setup.lisp")
(ql:quickload :qtools)
(ql:quickload :cells)
(ql:quickload :qtcore)
(ql:quickload :qtgui)
(ql:quickload :parse-number)
(ql:quickload :alexandria)
(ql:quickload :anaphora)

(cl:defpackage :tipcalc.models
  (:use :cl :cells)
  (:export
    service-rating fed-tip-calc tip-calc tax-calc item bill meal-expense-calculator
    rating rate cost tip tax kind item subtotal subtotal total))

(in-package :tipcalc.models)

(defmodel service-rating ()
          ((rating :cell t :accessor rating :initarg :rating :initform (c-in :normal))
           (rate   :cell t :accessor rate   :initform (c? (case (rating self)
                                                            (:normal 0.18)
                                                            (:excellent 0.25)
                                                            (:poor 0.15))))))

(defmodel tip-calc ()
          ((cost :cell t :accessor cost :initarg :cost :initform (c-in 0))
           (rate :cell t :accessor rate :initarg :rate :initform (c-in 0.18))
           (tip  :cell t :accessor tip  :initform (c? (* (cost self)
                                                         (rate self))))))
(defmodel fed-tip-calc (tip-calc)
          ((rate-feeder :cell t :accessor rate-feeder :initarg :rate-feeder)
           (rate :cell t :accessor rate :initarg :rate :initform (c? (rate (rate-feeder self))))))


(defmodel tax-calc ()
          ((cost :cell t :accessor cost :initarg :cost :initform (c-in 0))
           (rate :cell t :accessor rate :initarg :rate :initform (c-in 0.18))
           (tax  :cell t :accessor tax  :initform (c? (* (cost self)
                                                         (rate self))))))

(defmodel item ()
          ((kind :cell t :accessor kind :initarg :kind :initform (c-in :food))
           (cost :cell t :accessor cost :initarg :cost :initform (c-in 0))))

(defmodel bill ()
          ((items :accessor items :initarg :items :initform (c-in nil))
           (cost  :accessor cost  :initform (c? (apply #'+
                                                       (loop for item in (items self)
                                                             collect (cost item)))))))

(defmodel meal-expense-calculator ()
          ((subtotal :accessor subtotal :initarg :subtotal :initform (c-in 0))
           (tax      :accessor tax      :initarg :tax      :initform (c-in 0.08))
           (tip      :accessor tip      :initarg :tip      :initform (c-in 0))
           (total    :accessor total    :initform (c? (+   (subtotal self)     
                                                           (* (tax-rate self)
                                                              (subtotal self))
                                                           (tip self))))))

(defpackage tipcalc
  (:use :cl+qt :parse-number :anaphora :tipcalc.models))
(in-package :tipcalc)
(in-readtable :qtools)
;(defparameter *service-rating* (make-instance 'service-rating))
;(defparameter *tip-calc* (make-instance 'tip-calc :rate (rate *service-rating*)))

(define-widget edits (QWidget) ())
(define-widget service-quality (QWidget) ())
(define-widget main-window (QWidget) ())

(define-widget my-slider (QWidget) ())
(define-signal (my-slider val-changed) (float))
(define-subwidget (my-slider slider) (q+:make-qslider my-slider))
(define-subwidget (my-slider label) (q+:make-qlabel my-slider))
(define-subwidget (my-slider layout) (q+:make-qvboxlayout my-slider)
  (q+:add-widget slider)
  (q+:add-widget label))

(define-slot (my-slider slider) ((new-val integer))
  (declare (connected slider (value-changed integer)))
  (setf (q+:text label) (format nil "~a%" new-val))
  (signal! my-slider (val-changed float) (coerce (/ new-val 100) 'float)))

(defmethod set-value ((my-slider my-slider) (new-value float))
  (with-slots-bound (my-slider my-slider)
    (setf (q+:value slider) (round (* new-value 100)))))


(defmacro alambda (&body body)
  `(lambda (it) ,@body))

(defmacro with-no-signals (widget &body body)
  (alexandria:once-only (widget)
    `(progn
       (q+:block-signals ,widget t)
       (unwind-protect
         (progn
           ,@body)
         (q+:block-signals ,widget nil)))))

(defmacro connect-cell-to-widget ((widget sub-widget) (cell class) place widg-value-cb)
  "This connects a widget to an observable cell.  TODO: figure out the macro edge-cases here"
  `(cells:defobserver ,cell ((self ,class)) 
                      ; I want this to run:
                      ; EITHER When the old value is not bound
                      ; OR     When the old value is not equal to the new value
                      (format t "--> changed: ~a is not ~a~%" ,(symbol-name cell) cells:new-value)
                      (with-slots-bound (,widget ,widget)
                        (with-signals-blocked (,sub-widget) 
                          (setf ,place (funcall ,widg-value-cb cells:new-value))))))

(defmacro connect-widget-to-cell ((widget signal arg-type) place &key value-mod)
  (alexandria:with-gensyms (arg)
    `(define-slot (,widget ,signal) ((,arg ,arg-type))
       (declare (connected ,widget (,signal ,arg-type)))
       ,(when value-mod
          `(setf ,arg (funcall ,value-mod ,arg)))
       (setf ,place ,arg))))

(defmacro connecting ((widget sub-widget) (cell-slot-name cell-class) cell-instance &body body)
  `(macrolet ((cell->widget (place cb)
                `(connect-cell-to-widget (,',widget ,',sub-widget) (,',cell-slot-name ,',cell-class) ,place ,cb))
              (signal->cell (signal &key value-mod)
                `(connect-widget-to-cell (,',widget ,@signal) (,',cell-slot-name ,',cell-instance) :value-mod ,value-mod)))
     ,@body))

(defmacro define-connections (&rest connections)
  (list*
    'progn
    (loop for (widg-spec cell-spec cell-inst . body) in connections
          collect `(connecting ,widg-spec ,cell-spec ,cell-inst
                     ,@body))))  

(let* ((s-r (make-instance 'service-rating))
       (tc (make-instance 'tip-calc :rate (cells:c? (rate s-r)))))

  (defun stringify (obj) (format nil "~a" obj))
  (defun make-keyword (str)
    (alexandria:make-keyword (string-upcase str)))

  (define-signal (edits cost-changed)  (float))
  (define-signal (edits rate-changed)  (float))
  (define-signal (edits tip-changed)  (float))

  (define-subwidget (edits cost-widg) (q+:make-qlineedit edits)
    (setf (q+:placeholder-text cost-widg) (stringify (cost tc)))) 

  (connecting (edits cost-widg) (cost tip-calc) tc
    (signal->cell
      (cost-changed float)))

  (define-slot (edits cost-widg) ((new-text string))
    (declare (connected cost-widg (text-edited string)))
    (handler-case
      (let ((num (coerce (parse-number new-text) 'float)))
        (signal! edits (cost-changed float) num))
      ((or parse-error type-error) (c) (declare (ignore c)))))

  (define-subwidget (edits rate-widg) (q+:make-qslider edits)
    (setf (q+:minimum rate-widg) 6)
    (setf (q+:maximum rate-widg) 30)
    (setf (q+:value rate-widg) (round (* 100 (rate s-r)))))


  
  (connecting (edits rate-widg) (rate tip-calc) tc
    (cell->widget
      (q+:value rate-widg)
      (alambda
        (round (* 100 it))))
    (signal->cell
      (rate-changed float))) 


  (define-slot (edits rate-widg) ((new-value integer))
    (declare (connected rate-widg (value-changed integer)))
    (handler-case
      (signal! edits (rate-changed float) (coerce (/ new-value 100) 'float))
      ((or parse-error type-error) (c) (declare (ignore c)))))


  (define-subwidget (edits tip-widg) (q+:make-qlineedit edits)
    (setf (q+:placeholder-text tip-widg) (stringify (tip tc))))

  (connecting (edits tip-widg) (tip tip-calc) tc
    (cell->widget
      (q+:text tip-widg)
      (alambda
        (stringify it))))
  

  (define-subwidget (edits edit-layout) (q+:make-qhboxlayout edits)
    (q+:add-widget edit-layout cost-widg)
    (q+:add-widget edit-layout rate-widg)
    (q+:add-widget edit-layout tip-widg))

  (define-signal (service-quality quality-chosen) (string))

  (define-subwidget (service-quality choose-poor) (q+:make-qradiobutton service-quality)
    (setf (q+:text choose-poor) "Poor"))
  (define-slot (service-quality choose-poor) ()
    (declare (connected choose-poor (toggled bool)))
    (signal! service-quality (quality-chosen string) "poor"))

  (define-subwidget (service-quality choose-normal) (q+:make-qradiobutton service-quality)
    (setf (q+:text choose-normal) "Normal"))
  (define-slot (service-quality choose-normal) ()
    (declare (connected choose-normal (toggled bool)))
    (signal! service-quality (quality-chosen string) "normal"))


  (define-subwidget (service-quality choose-excellent) (q+:make-qradiobutton service-quality)
    (setf (q+:text choose-excellent) "Excellent"))
  (define-slot (service-quality choose-excellent) ()
    (declare (connected choose-excellent (toggled bool)))
    (signal! service-quality (quality-chosen string) "excellent"))

  (connecting (service-quality nil) (rating service-rating) s-r
    (signal->cell
      (quality-chosen string)
      :value-mod (alambda (make-keyword it))))

  (define-subwidget (service-quality quality-layout) (q+:make-qhboxlayout service-quality)
    (q+:add-widget quality-layout choose-poor)
    (q+:add-widget quality-layout choose-normal)
    (q+:add-widget quality-layout choose-excellent))

  (define-subwidget (main-window edits) (make-instance 'edits))
  (define-subwidget (main-window service-quality) (make-instance 'service-quality))
  (define-subwidget (main-window layout) (q+:make-qvboxlayout main-window)
    (q+:add-widget layout service-quality) 
    (q+:add-widget layout edits)))


(defun main ()
  (with-main-window (window (make-instance 'main-window))))

(ql:quickload  :swank)
(swank:create-server :port 4006)
(handler-case (main)
  (sb-sys:interactive-interrupt (c) (declare (ignore c)) (format t "~cExiting on interrupt...~%" #\return)))