(module classes mzscheme (require mzlib/class mzlib/class100 (prefix mred: mred) mzlib/list mzlib/etc (prefix util: "utils.ss") "constants.ss" "make-cards.ss" "region.ss" string-constants "../show-help.ss" "../show-scribbling.ss") (provide pasteboard% table%) (define pasteboard% (class100 mred:pasteboard% () (inherit begin-edit-sequence end-edit-sequence get-admin invalidate-bitmap-cache find-next-selected-snip find-first-snip find-snip set-before set-after add-selected is-selected? no-selected set-selected remove-selected get-snip-location move-to dc-location-to-editor-location set-selection-visible) (private-field [select-one? #t] [select-backward? #f] [raise-to-front? #f] [button-map '((left #f #f #t) (middle #t #f #t) (right #f #t #f))] [do-on-double-click 'flip] [do-on-single-click void] [selecting? #f] [dragging? #f] [bg-click? #f] [click-base #f] [last-click #f] [regions null]) (private [get-snip-bounds (lambda (s) (let ([xbox (box 0)] [ybox (box 0)]) (get-snip-location s xbox ybox #f) (let ([l (unbox xbox)] [t (unbox ybox)]) (get-snip-location s xbox ybox #t) (values l t (unbox xbox) (unbox ybox)))))] [for-each-selected (lambda (f) (let loop ([snip (find-next-selected-snip #f)]) (when snip (f snip) (loop (find-next-selected-snip snip)))))] [make-overlapping-list (lambda (s so-far behind?) (let-values ([(sl st sr sb) (get-snip-bounds s)]) (let loop ([t (find-first-snip)][so-far so-far][get? (not behind?)]) (cond [(not t) so-far] [(eq? s t) (if behind? (loop (send t next) so-far #t) so-far)] [get? (let ([l (if (and (not (memq t so-far)) (let-values ([(tl tt tr tb) (get-snip-bounds t)]) (and (or (<= sl tl sr) (<= sl tr sr)) (or (<= st tt sb) (<= st tb sb))))) (make-overlapping-list t (cons t so-far) behind?) so-far)]) (loop (send t next) l #t))] [else (loop (send t next) so-far #f)]))))] [get-reverse-selected-list (lambda () (let loop ([s (find-next-selected-snip #f)][l null]) (if s (loop (find-next-selected-snip s) (cons s l)) l)))] [shuffle (lambda (selected-list) ; cards to shuffle in back->front order (let* ([permuted-list (util:shuffle-list selected-list 7)] [get-pos (lambda (s) (let ([xb (box 0)] [yb (box 0)]) (get-snip-location s xb yb) (cons (unbox xb) (unbox yb))))] [sel-loc-list (map get-pos selected-list)] [perm-loc-list (map get-pos permuted-list)]) (for-each (lambda (s start-pos end-pos) (let* ([sx (car start-pos)] [sy (cdr start-pos)] [ex (car end-pos)] [ey (cdr end-pos)] [steps (max 1 (floor (/ 50 (length selected-list))))]) (let loop ([i 1]) (unless (> i steps) (let ([x (+ sx (* (/ i steps) (- ex sx)))] [y (+ sy (* (/ i steps) (- ey sy)))]) (move-to s x y) (mred:flush-display) (loop (add1 i))))))) permuted-list perm-loc-list sel-loc-list) (let loop ([l permuted-list]) (unless (null? l) (set-before (car l) #f) (loop (cdr l)))) (no-selected)))] [update-region (lambda (region) (let-values ([(sx sy sw sh) (get-region-box region)]) (invalidate-bitmap-cache sx sy sw sh)))]) (public [only-front-selected (lambda () (let loop ([s (find-next-selected-snip #f)][ok (find-first-snip)]) (when s (if (eq? s ok) (loop (find-next-selected-snip s) (send ok next)) (let loop ([s s][l (list s)]) (let ([next (find-next-selected-snip s)]) (if next (loop next (cons s l)) (for-each (lambda (s) (remove-selected s)) l))))))))]) (override [on-paint (lambda (before? dc l t r b dx dy caret) (when before? (for-each (lambda (region) (when (region-paint-callback region) (let-values ([(sx sy sw sh) (get-region-box region)]) ((region-paint-callback region) dc (+ dx sx) (+ dy sy) sw sh))) (when (region-label region) (let ([old-b (send dc get-brush)] [old-p (send dc get-pen)]) (let-values ([(sx sy sw sh) (get-region-box region)]) (send dc set-brush white-brush) (send dc set-pen no-pen) (send dc draw-rectangle (+ dx sx) (+ dy sy) sw sh) (send dc set-pen dark-gray-pen) (draw-roundish-rectangle dc (+ dx sx) (+ dy sy) sw sh) (let ([text (region-label region)]) (if (string? text) (let ([old-f (send dc get-font)]) (send dc set-font nice-font) (let-values ([(x y d a) (send dc get-text-extent text)]) (send dc draw-text text (+ dx sx (/ (- sw x) 2)) (if (region-button? region) ;; Since we use size-in-pixels, the letters ;; should really be 12 pixels high (including ;; the descender), but the space above the letter ;; can vary by font; center on 12, splitting ;; the difference for the descender (+ dy sy (/ (- sh 12) 2) (- 12 y (/ d -2))) (+ dy sy 5)))) (send dc set-font old-f)) (send dc draw-bitmap text (+ dx sx (/ (- sw (send text get-width)) 2)) (+ dy sy (/ (- sh (send text get-height)) 2)) 'solid black-color (send text get-loaded-mask)))) (when (region-hilite? region) (send dc set-brush hilite-brush) (send dc set-pen no-pen) (send dc draw-rectangle (+ dx sx 1) (+ dy sy 1) (- sw 2) (- sh 2)))) (send dc set-brush old-b) (send dc set-pen old-p)))) regions)))]) (augment [after-select (lambda (s on?) (inner (void) after-select s on?) (unless (or (not on?) selecting?) (set! selecting? #t) (if select-one? (when raise-to-front? (set-before s #f)) (begin (begin-edit-sequence) (let ([l (make-overlapping-list s (list s) select-backward?)]) (for-each (lambda (i) (add-selected i)) l)) (when raise-to-front? (let loop ([snip (find-next-selected-snip #f)][prev #f]) (when snip (if prev (set-after snip prev) (set-before snip #f)) (loop (find-next-selected-snip snip) snip)))) (end-edit-sequence))) (set! selecting? #f)))] [on-interactive-move (lambda (e) (inner (void) on-interactive-move e) (for-each (lambda (region) (set-region-decided-start?! region #f)) regions) (for-each-selected (lambda (snip) (send snip remember-location this))) (set! dragging? #t))]) (override [interactive-adjust-move (lambda (snip xb yb) (super interactive-adjust-move snip xb yb) (let-values ([(l t r b) (get-snip-bounds snip)]) (let-values ([(rl rt rw rh) (let ([r (send snip stay-in-region)]) (if r (values (region-x r) (region-y r) (region-w r) (region-h r)) (let ([wb (box 0)][hb (box 0)]) (send (get-admin) get-view #f #f wb hb) (values 0 0 (unbox wb) (unbox hb)))))]) (let ([max-x (- (+ rl rw) (- r l))] [max-y (- (+ rt rh) (- b t))]) (when (< (unbox xb) rl) (set-box! xb rl)) (when (> (unbox xb) max-x) (set-box! xb max-x)) (when (< (unbox yb) rt) (set-box! yb rt)) (when (> (unbox yb) max-y) (set-box! yb max-y))))))]) (augment [after-interactive-move (lambda (e) (when dragging? (set! dragging? #f) (inner (void) after-interactive-move e) (for-each-selected (lambda (snip) (send snip back-to-original-location this))) (let ([cards (get-reverse-selected-list)]) (only-front-selected) ; in case overlap changed (for-each (lambda (region) (when (region-hilite? region) (mred:queue-callback ; Call it outside the current edit sequence (lambda () ((region-callback region) cards) (unhilite-region region))))) regions))))]) (override [on-default-event (lambda (e) (let ([click (let ([c (or (and (send e button-down? 'left) 'left) (and (send e button-down? 'right) 'right) (and (send e button-down? 'middle) 'middle))]) (cond [(eq? c last-click) c] [(not last-click) c] ;; Move/drag event has different mouse button, ;; and there was no mouse up. Don't accept the ;; click, yet. [else #f]))]) (set! last-click click) (when click (let* ([actions (cdr (assoc click button-map))] [one? (list-ref actions 0)] [backward? (list-ref actions 1)] [raise? (list-ref actions 2)]) (unless (and (eq? backward? select-backward?) (eq? one? select-one?) (eq? raise? raise-to-front?)) (set! select-one? one?) (set! select-backward? backward?) (set! raise-to-front? raise?) (no-selected)))) (let*-values ([(lx ly) (dc-location-to-editor-location (send e get-x) (send e get-y))] [(s) (find-snip lx ly)]) ; Clicking on a "selected" card unselects others ; in this interface (when (send e button-down?) (unless (or (not click-base) (not s) (eq? s click-base)) (no-selected)) (set! click-base s)) (when (and dragging? click-base (send click-base user-can-move)) (for-each (lambda (region) (when (and (not (region-button? region)) (region-callback region) (or (not (region-decided-start? region)) (region-can-select? region))) (let-values ([(sx sy sw sh) (get-region-box region)]) (let ([in? (and (<= sx lx (+ sx sw)) (<= sy ly (+ sy sh)))]) (unless (region-decided-start? region) (set-region-decided-start?! region #t) (set-region-can-select?! region (not in?))) (when (and (not (eq? in? (region-hilite? region))) (region-can-select? region)) (set-region-hilite?! region in?) (when (region-interactive-callback region) ((region-interactive-callback region) in? (get-reverse-selected-list))) (invalidate-bitmap-cache sx sy sw sh)))))) regions)) ; Can't move => no raise, either (unless (or (not click-base) (send click-base user-can-move)) (set! raise-to-front? #f)) (let ([was-bg? bg-click?]) (if (send e button-down?) (set! bg-click? (not s)) (when (and bg-click? (not (send e dragging?))) (set! bg-click? #f))) (unless bg-click? (super on-default-event e)) (when (and bg-click? dragging?) ;; We didn't call super on-default-event, so we need ;; to explicitly end the drag: (after-interactive-move e)) (when bg-click? ; Check for clicking on a button region: (for-each (lambda (region) (when (and (region-button? region) (region-callback region)) (let-values ([(sx sy sw sh) (get-region-box region)]) (let ([in? (and (<= sx lx (+ sx sw)) (<= sy ly (+ sy sh)))]) (unless (region-decided-start? region) (set-region-decided-start?! region #t) (set-region-can-select?! region in?)) (when (and (not (eq? in? (region-hilite? region))) (region-can-select? region)) (set-region-hilite?! region in?) (invalidate-bitmap-cache sx sy sw sh)))))) regions)) (when (and was-bg? (not bg-click?)) ; Callback hilighted button: (for-each (lambda (region) (when (region-button? region) (set-region-decided-start?! region #f) (when (region-hilite? region) (mred:queue-callback ; Call it outside the current edit sequence (lambda () ((region-callback region)) (unhilite-region region)))))) regions))) (when (and (send e button-down?) click-base (not (send click-base user-can-move))) (no-selected))) (when (and click click-base) (do-on-single-click click-base))))] [on-double-click (lambda (s e) (cond [(eq? do-on-double-click 'flip) (begin-edit-sequence) (let ([l (get-reverse-selected-list)]) (for-each (lambda (s) (when (send s user-can-flip) (send s flip))) l) (let loop ([l (reverse l)]) (unless (null? l) (set-before (car l) #f) (loop (cdr l))))) (no-selected) (end-edit-sequence)] [do-on-double-click (do-on-double-click s)] [else (void)]))]) (public [get-all-list (lambda () (let loop ([t (find-first-snip)][accum null]) (cond [(not t) (reverse accum)] [else (loop (send t next) (cons t accum))])))] [get-full-box (lambda () (let ([xb (box 0)][yb (box 0)] [wb (box 0)][hb (box 0)]) (send (get-admin) get-view xb yb wb hb) (values 0 0 (unbox wb) (unbox hb))))] [get-region-box (lambda (region) (values (region-x region) (region-y region) (region-w region) (region-h region)))] [add-region (lambda (r) (set! regions (append regions (list r))) (update-region r))] [remove-region (lambda (r) (set! regions (remq r regions)) (update-region r))] [unhilite-region (lambda (region) (set-region-hilite?! region #f) (update-region region))] [hilite-region (lambda (region) (set-region-hilite?! region #t) (update-region region))] [set-double-click-action (lambda (a) (set! do-on-double-click a))] [set-single-click-action (lambda (a) (set! do-on-single-click a))] [set-button-action (lambda (button action) (let ([map (case action [(drag/one) (list #t #f #f)] [(drag-raise/one) (list #t #f #t)] [(drag/above) (list #f #f #f)] [(drag-raise/above) (list #f #f #t)] [(drag/below) (list #f #t #f)] [(drag-raise/below) (list #f #t #t)] [else (error 'set-button-action "unknown action: ~s" action)])]) (set! button-map (cons (cons button map) (remq (assoc button button-map) button-map)))))]) (sequence (super-init) (set-selection-visible #f)))) (define table% (class mred:frame% (init title w h) (inherit reflow-container) (augment* [on-close (lambda () (exit))]) (public* [table-width (lambda () (reflow-container) (let-values ([(x y w h) (send pb get-full-box)]) w))] [table-height (lambda () (reflow-container) (let-values ([(x y w h) (send pb get-full-box)]) h))] [begin-card-sequence (lambda () (set! in-sequence (add1 in-sequence)) (send pb begin-edit-sequence))] [end-card-sequence (lambda () (send pb end-edit-sequence) (set! in-sequence (sub1 in-sequence)))] [add-card (lambda (card x y) (position-cards (list card) x y (lambda (p) (values 0 0)) add-cards-callback))] [add-cards (opt-lambda (cards x y [offset (lambda (p) (values 0 0))]) (position-cards cards x y offset add-cards-callback))] [add-cards-to-region (lambda (cards region) (position-cards-in-region cards region add-cards-callback))] [move-card (lambda (card x y) (position-cards (list card) x y (lambda (p) (values 0 0)) move-cards-callback))] [move-cards (opt-lambda (cards x y [offset (lambda (p) (values 0 0))]) (position-cards cards x y offset move-cards-callback))] [move-cards-to-region (lambda (cards region) (position-cards-in-region cards region (lambda (c x y) (send pb move-to c x y))))] [card-location (lambda (card) (let ([x (box 0)] [y (box 0)]) (unless (send pb get-snip-location card x y) (raise-mismatch-error 'card-location "card not on table: " card)) (values (unbox x) (unbox y))))] [all-cards (lambda () (send pb get-all-list))] [remove-card (lambda (card) (remove-cards (list card)))] [remove-cards (lambda (cards) (begin-card-sequence) (for-each (lambda (c) (send pb release-snip c)) cards) (end-card-sequence))] [flip-card (lambda (card) (flip-cards (list card)))] [flip-cards (lambda (cards) (if (or (not animate?) (positive? in-sequence)) (for-each (lambda (c) (send c flip)) cards) (let ([flip-step (lambda (go) (let ([start (current-milliseconds)]) (begin-card-sequence) (go) (end-card-sequence) (pause (max 0 (- (/ ANIMATION-TIME ANIMATION-STEPS) (/ (- (current-milliseconds) start) 1000))))))]) (flip-step (lambda () (for-each (lambda (c) (send c semi-flip)) cards))) (flip-step (lambda () (for-each (lambda (c) (send c flip)) cards))) (flip-step (lambda () (for-each (lambda (c) (send c semi-flip)) cards))))))] [rotate-card (lambda (card mode) (rotate-cards (list card) mode))] [rotate-cards (lambda (cards mode) (begin-card-sequence) (let ([tw (table-width)] [th (table-height)]) (map (lambda (c) (let ([w (send c card-width)] [h (send c card-height)]) (send c rotate mode) (let ([w2 (send c card-width)] [h2 (send c card-height)] [x (box 0)] [y (box 0)]) (send pb get-snip-location c x y) (send pb move-to c (min (max 0 (+ (unbox x) (/ (- w w2) 2))) (- tw w2)) (min (max 0 (+ (unbox y) (/ (- h h2) 2))) (- th h2)))))) cards) (end-card-sequence)))] [card-face-up (lambda (card) (cards-face-up (list card)))] [cards-face-up (lambda (cards) (flip-cards (filter (lambda (c) (send c face-down?)) cards)))] [card-face-down (lambda (card) (cards-face-down (list card)))] [cards-face-down (lambda (cards) (flip-cards (filter (lambda (c) (not (send c face-down?))) cards)))] [card-to-front (lambda (card) (send pb set-before card #f))] [card-to-back (lambda (card) (send pb set-after card #f))] [stack-cards (lambda (cards) (unless (null? cards) (send pb only-front-selected) ; in case overlap changes (begin-card-sequence) (let loop ([l (cdr cards)][behind (car cards)]) (unless (null? l) (send pb set-after (car l) behind) (loop (cdr l) (car l)))) (end-card-sequence)))] [add-region (lambda (r) (send pb add-region r))] [remove-region (lambda (r) (send pb remove-region r))] [hilite-region (lambda (r) (send pb hilite-region r))] [unhilite-region (lambda (r) (send pb unhilite-region r))] [set-button-action (lambda (button action) (send pb set-button-action button action))] [set-double-click-action (lambda (a) (send pb set-double-click-action a))] [set-single-click-action (lambda (a) (send pb set-single-click-action a))] [pause (lambda (duration) (let ([s (make-semaphore)] [a (alarm-evt (+ (current-inexact-milliseconds) (* duration 1000)))] [enabled? (send c is-enabled?)]) ;; Can't move the cards during this time: (send c enable #f) (mred:yield a) (when enabled? (send c enable #t))))] [animated (case-lambda [() animate?] [(on?) (set! animate? (and on? #t))])] [create-status-pane (lambda () (let ([p (make-object mred:horizontal-pane% this)]) (set! msg (new mred:message% [parent p] [label ""] [stretchable-width #t])) p))] [set-status (lambda (str) (when msg (send msg set-label str)))] [add-help-button (lambda (pane where title tt?) (new mred:button% (parent pane) (label (string-constant help-menu-label)) (callback (let ([show-help (show-help where title tt?)]) (lambda x (show-help))))))] [add-scribble-button (lambda (pane mod tag) (new mred:button% (parent pane) (label (string-constant help-menu-label)) (callback (let ([show-help (show-scribbling mod tag)]) (lambda x (show-help))))))]) (begin (define msg #f) (define add-cards-callback (lambda (card x y) (send pb insert card #f x y))) (define move-cards-callback (lambda (card x y) (send pb move-to card x y) (send card remember-location pb)))) (begin (define animate? #t) (define in-sequence 0)) (private* [position-cards (lambda (cards x y offset set) (let ([positions (let loop ([l cards][n 0]) (if (null? l) null (let-values ([(dx dy) (offset n)]) (cons (cons (+ x dx) (+ y dy)) (loop (cdr l) (add1 n))))))]) (if (or (not animate?) (positive? in-sequence) (eq? set add-cards-callback)) (begin (begin-card-sequence) (for-each (lambda (c p) (set c (car p) (cdr p))) cards positions) (end-card-sequence)) (let-values ([(moving-cards source-xs source-ys dest-xs dest-ys) (let loop ([cl cards][pl positions]) (if (null? cl) (values null null null null null) (let-values ([(mcl sxl syl dxl dyl) (loop (cdr cl) (cdr pl))] [(card) (car cl)] [(x y) (values (caar pl) (cdar pl))]) (let ([xb (box 0)][yb (box 0)]) (send pb get-snip-location card xb yb) (let ([sx (unbox xb)][sy (unbox yb)]) (if (and (= x sx) (= y sy)) (values mcl sxl syl dxl dyl) (values (cons card mcl) (cons sx sxl) (cons sy syl) (cons x dxl) (cons y dyl))))))))]) (let ([time-scale ;; An animation speed that looks good for ;; long moves looks too slow for short ;; moves. So scale the time back by as much ;; as 50% if the max distance for all cards ;; is short. (let ([max-delta (max (apply max 0 (map (lambda (sx dx) (abs (- sx dx))) source-xs dest-xs)) (apply max 0 (map (lambda (sy dy) (abs (- sy dy))) source-ys dest-ys)))]) (if (max-delta . < . 100) (/ (+ max-delta 100) 200.0) 1))]) (let loop ([n 1]) (unless (> n ANIMATION-STEPS) (let ([start (current-milliseconds)] [scale (lambda (s d) (+ s (* (/ n ANIMATION-STEPS) (- d s))))]) (begin-card-sequence) (for-each (lambda (c sx sy dx dy) (set c (scale sx dx) (scale sy dy))) moving-cards source-xs source-ys dest-xs dest-ys) (end-card-sequence) (pause (max 0 (- (/ (* time-scale ANIMATION-TIME) ANIMATION-STEPS) (/ (- (current-milliseconds) start) 1000)))) (loop (add1 n)))))))) ;; In case overlap changed: (send pb only-front-selected)))] [position-cards-in-region (lambda (cards r set) (unless (null? cards) (let-values ([(x y w h) (send pb get-region-box r)] [(len) (sub1 (length cards))] [(cw ch) (values (send (car cards) card-width) (send (car cards) card-height))]) (let* ([pretty (lambda (cw) (+ (* (add1 len) cw) (* len PRETTY-CARD-SEP-AMOUNT)))] [pw (pretty cw)] [ph (pretty ch)]) (let-values ([(x w) (if (> w pw) (values (+ x (/ (- w pw) 2)) pw) (values x w))] [(y h) (if (> h ph) (values (+ y (/ (- h ph) 2)) ph) (values y h))]) (position-cards cards x y (lambda (p) (if (zero? len) (values (/ (- w cw) 2) (/ (- h ch) 2)) (values (* (- len p) (/ (- w cw) len)) (* (- len p) (/ (- h ch) len))))) set))))))]) (super-new [label title] [style '(metal no-resize-border)]) (begin (define c (make-object mred:editor-canvas% this #f '(no-vscroll no-hscroll))) (define pb (make-object pasteboard%))) (send c min-client-width (+ 10 (inexact->exact (floor (* w (send back get-width)))))) (send c min-client-height (+ 10 (inexact->exact (floor (* h (send back get-height)))))) (send c stretchable-width #f) (send c stretchable-height #f) (send this stretchable-width #f) (send this stretchable-height #f) (send c set-editor pb))) (define (draw-roundish-rectangle dc x y w h) (send dc draw-line (+ x 1) y (+ x w -2) y) (send dc draw-line (+ x 1) (+ y h -1) (+ x w -2) (+ y h -1)) (send dc draw-line x (+ y 1) x (+ y h -2)) (send dc draw-line (+ x w -1) (+ y 1) (+ x w -1) (+ y h -2))))