diff --git a/gui-doc/scribblings/framework/text.scrbl b/gui-doc/scribblings/framework/text.scrbl index 7c363ed5..411bbb8e 100644 --- a/gui-doc/scribblings/framework/text.scrbl +++ b/gui-doc/scribblings/framework/text.scrbl @@ -1,6 +1,6 @@ #lang scribble/doc @(require scribble/manual scribble/extract) -@(require (for-label framework racket/gui)) +@(require (for-label framework racket/gui racket/contract/base)) @title{Text} @definterface[text:basic<%> (editor:basic<%> text%)]{ @@ -139,24 +139,41 @@ } @defmethod[(move/copy-to-edit [dest-text (is-a?/c text%)] - [start exact-integer?] - [end exact-integer?] - [dest-pos exact-integer?] + [start natural?] + [end (and/c natural? (>=/c start))] + [dest-pos natural?] [#:try-to-move? try-to-move? boolean? #t]) void?]{ - This moves or copies text and snips to another edit. + This moves or copies text and snips to @racket[dest-text]. - Moves or copies from the edit starting at @racket[start] and ending at + Moves or copies from @racket[this] starting at @racket[start] and ending at @racket[end]. It puts the copied text and snips in @racket[dest-text] - starting at location @racket[dest-pos]. + starting at location @racket[dest-pos]. If @racket[start] and @racket[end] + are equal then nothing is moved or copied. - If @racket[try-to-move] is @racket[#t], then the snips are removed; - and if it is @racket[#f], then they are copied. + If @racket[try-to-move?] is @racket[#t], then the snips are removed; + and if it is @racket[#f], then they are copied. If @racket[try-to-move?] is + @racket[#t] and @racket[dest-pos] is between @racket[start] and @racket[end] + then @racket[this] is unchanged. - If a snip refused to be moved, it will be copied and deleted from the editor, + If a snip refuses to be moved, it will be copied and deleted from the editor, otherwise it will be moved. A snip may refuse to be moved by returning @racket[#f] from @method[snip% release-from-owner]. } + @defmethod[(move-to [dest-text (is-a?/c text%)] + [start natural?] + [end (and/c natural? (>=/c start))] + [dest-pos natural?]) + void?]{ + Like @racket[move/copy-to-edit] when the @racket[#:try-to-move?] argument is @racket[#t]. + } + @defmethod[(copy-to [dest-text (is-a?/c text%)] + [start natural?] + [end (and/c natural? (>=/c start))] + [dest-pos natural?]) + void?]{ + Like @racket[move/copy-to-edit] when the @racket[#:try-to-move?] argument is @racket[#f]. + } @defmethod*[(((initial-autowrap-bitmap) (or/c #f (is-a?/c bitmap%))))]{ The result of this method is used as the initial autowrap bitmap. Override diff --git a/gui-lib/framework/private/text.rkt b/gui-lib/framework/private/text.rkt index a6860a82..c0008e02 100644 --- a/gui-lib/framework/private/text.rkt +++ b/gui-lib/framework/private/text.rkt @@ -530,7 +530,7 @@ delete find-snip get-style-list change-style position-line line-start-position - get-filename) + get-filename get-end-position) (define/public (get-fixed-style) (send (get-style-list) find-named-style "Standard")) @@ -585,29 +585,80 @@ (define/augment (after-delete start len) (set! edition (+ edition 1)) (inner (void) after-delete start len)) + + (define/public (move-to dest-edit start end dest-position) + (unless (and (<= 0 start) (<= 0 end) (<= 0 dest-position)) + (error 'move-to + "expected start, end, and dest-pos to be non-negative")) + (when (> start end) + (error 'move-to + "expected start position smaller than end position")) + (define (release-or-copy snip) + (cond + [(send snip release-from-owner) snip] + [else + (define copy (send snip copy)) + (define snip-start (get-snip-position snip)) + (define snip-end (+ snip-start (send snip get-count))) + (delete snip-start snip-end) + copy])) + (define move-to-self? (object=? this dest-edit)) + (unless (or (= start end) (and move-to-self? (<= start dest-position end))) + (let loop ([current-start start] + [current-end (min end (get-end-position))] + [current-dest dest-position]) + (split-snip current-start) + (split-snip current-end) + (define snip (find-snip current-end 'before-or-none)) + (cond + [(or (not snip) (< (get-snip-position snip) current-start)) (void)] + [else + (define released/copied (release-or-copy snip)) + (define snip-count (send released/copied get-count)) + (define new-start + (cond + [(or (not move-to-self?) (> current-dest current-start)) current-start] + [else (+ current-start snip-count)])) + (define new-end + (cond + [(and move-to-self? (< current-dest current-end)) current-end] + [else (- current-end snip-count)])) + (define new-dest + (cond + [(or (not move-to-self?) (< current-dest current-start)) current-dest] + [else (- current-dest snip-count)])) + (send dest-edit insert released/copied new-dest new-dest) + (loop new-start new-end new-dest)])))) + + (define/public (copy-to dest-edit start end dest-position) + (unless (and (<= 0 start) (<= 0 end) (<= 0 dest-position)) + (error 'copy-to + "expected start, end, and dest-pos to be non-negative")) + (when (> start end) + (error 'copy-to + "expected start position smaller than end position")) + (unless (= start end) + (split-snip start) + (split-snip end) + (define snips + (let loop ([snip (find-snip end 'before)] [snips '()]) + (cond + [(or (not snip) (< (get-snip-position snip) start)) (reverse snips)] + [else (loop (send snip previous) (cons (send snip copy) snips))]))) + (for ([snip (in-list snips)]) + (send dest-edit insert snip dest-position dest-position)))) (define/public (move/copy-to-edit dest-edit start end dest-position #:try-to-move? [try-to-move? #t]) - (split-snip start) - (split-snip end) - (let loop ([snip (find-snip end 'before)]) - (cond - [(or (not snip) (< (get-snip-position snip) start)) - (void)] - [else - (let ([prev (send snip previous)] - [released/copied - (if try-to-move? - (if (send snip release-from-owner) - snip - (let* ([copy (send snip copy)] - [snip-start (get-snip-position snip)] - [snip-end (+ snip-start (send snip get-count))]) - (delete snip-start snip-end) - snip)) - (send snip copy))]) - (send dest-edit insert released/copied dest-position dest-position) - (loop prev))]))) + (unless (and (<= 0 start) (<= 0 end) (<= 0 dest-position)) + (error 'move/copy-to-edit + "expected start, end, and dest-pos to be non-negative")) + (when (> start end) + (error 'move/copy-to-edit + "expected start position smaller than end position")) + (cond + [try-to-move? (move-to dest-edit start end dest-position)] + [else (copy-to dest-edit start end dest-position)])) (public initial-autowrap-bitmap) (define (initial-autowrap-bitmap) (icon:get-autowrap-bitmap)) diff --git a/gui-test/framework/tests/text.rkt b/gui-test/framework/tests/text.rkt index 3cff14bd..afbbb21f 100644 --- a/gui-test/framework/tests/text.rkt +++ b/gui-test/framework/tests/text.rkt @@ -571,6 +571,278 @@ (define after (get-colors)) (list before after))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; move/copy-to-edit tests +;; + +(define (get-snips text) + (define the-snips + (let loop ([current (send text find-first-snip)] + [snips '()]) + (if current + (loop (send current next) (cons current snips)) + (reverse snips)))) + (for/list ([snip (in-list the-snips)] + [i (in-naturals)]) + (send snip get-text 0 (send snip get-count)))) + +(define (edit-style t posns) + (define (maybe-cdr p) (if (empty? p) '() (cdr p))) + (for ([i (in-list posns)] + [j (in-list (append (maybe-cdr posns) '(end)))] + [s (in-naturals)]) + (define sd (make-object style-delta% + (if (even? s) 'change-normal 'change-normal-color))) + (send t change-style sd i j))) + +(check-equal? (let ([t (new text%)]) + (send t insert "ABCDEF") + (edit-style t '(3)) + (get-snips t)) + '("ABC" "DEF")) + +(check-equal? (let ([t (new text%)]) + (send t insert "ABCDEFGH") + (edit-style t '(3 5)) + (get-snips t)) + '("ABC" "DE" "FGH")) + +(define (edit-string/text str start end dest-pos [this? #f] [style #f] + #:try-to-move? [try-to-move? #f] + #:contract-this? [contract-this? #f]) + (define t1 (new text:basic%)) + (define t2 (if this? + (if contract-this? (contract (object/c) t1 'p 'n) t1) + (new text:basic%))) + (send t1 insert str) + (when style + (edit-style t1 style)) + (send t1 move/copy-to-edit t2 + start + end + (if this? dest-pos 0) + #:try-to-move? try-to-move?) + (unless this? + (send t2 move/copy-to-edit + t1 + 0 + (send t2 get-end-position) + (cond + [(and try-to-move? (>= dest-pos end)) + (- dest-pos (- end start))] + [(and try-to-move? (>= dest-pos start)) + start] + [else dest-pos]) + #:try-to-move? #t)) + (send t1 get-text)) + +(check-equal? (edit-string/text "lR" 0 2 1 #f '(1) #:try-to-move? #t) "lR") +(check-equal? (edit-string/text "DaMoe" 0 5 1 #:try-to-move? #t) "DaMoe") +(check-equal? (edit-string/text "lR" 0 2 1 #f '(1) #:try-to-move? #t) "lR") +(check-equal? (edit-string/text "ABC" 2 3 0 #t '(0 1 2)) "CABC") +(check-equal? (edit-string/text "ABC" 2 3 0 #t #f) "CABC") +(check-equal? (edit-string/text "ABC" 2 3 0 #f '(0 1 2)) "CABC") +(check-equal? (edit-string/text "ABC" 2 3 0 #f #f) "CABC") +(check-equal? (edit-string/text "ABC" 2 3 0 #t '(0 1 2) #:try-to-move? #t) "CAB") +(check-equal? (edit-string/text "ABC" 2 3 0 #t #f #:try-to-move? #t) "CAB") +(check-equal? (edit-string/text "ABC" 2 3 0 #f '(0 1 2) #:try-to-move? #t) "CAB") +(check-equal? (edit-string/text "ABC" 2 3 0 #f #f #:try-to-move? #t) "CAB") +(check-equal? (edit-string/text "X" 0 0 0 #t #f) "X") +(check-equal? (edit-string/text "ji" 0 0 0 #t #f) "ji") +(check-equal? (edit-string/text "ABCDE" 2 4 4 #t #f) "ABCDCDE") +(check-equal? (edit-string/text "ABCDE" 2 4 2 #t #f) "ABCDCDE") + +(check-equal? (edit-string/text "ABCDE" 2 4 3 #t #f) "ABCCDDE") +(check-equal? (edit-string/text "ABCDE" 2 4 3 #t '(0 1 2 3 4)) "ABCCDDE") +(check-equal? (edit-string/text "ABCDE" 2 4 3 #t #f #:try-to-move? #t) "ABCDE") +(check-equal? (edit-string/text "ABCDE" 2 4 3 #t '(0 1 2 3 4) #:try-to-move? #t) "ABCDE") + +(check-exn + #rx"expected start position smaller than end position" + (thunk (edit-string/text "ABCDE" 4 2 0 #t #f))) +(check-exn + #rx"expected start position smaller than end position" + (thunk (edit-string/text "ABCDE" 4 2 0 #t '(0 1 2 3 4)))) +(check-exn + #rx"expected start position smaller than end position" + (thunk (edit-string/text "ABCDE" 4 2 0 #t #f #:try-to-move? #t))) +(check-exn + #rx"expected start position smaller than end position" + (thunk (edit-string/text "ABCDE" 4 2 0 #t '(0 1 2 3 4) #:try-to-move? #t))) + +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 4 4 #t #f))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 4 4 #t '(0 1 2 3 4)))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 4 4 #t #f #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 4 4 #t '(0 1 2 3 4) #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 -1 4 #t #f))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 -1 4 #t '(0 1 2 3 4)))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 -1 4 #t #f #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 -1 4 #t '(0 1 2 3 4) #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 4 -1 #t #f))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 4 -1 #t '(0 1 2 3 4)))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 4 -1 #t #f #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" 0 4 -1 #t '(0 1 2 3 4) #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 -1 0 #t #f))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 -1 0 #t '(0 1 2 3 4)))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 -1 0 #t #f #:try-to-move? #t))) +(check-exn + #rx"expected start, end, and dest-pos to be non-negative" + (thunk (edit-string/text "ABCDE" -1 -1 0 #t '(0 1 2 3 4) #:try-to-move? #t))) + +;; non-string snips +(let ([t (new text:basic%)]) + (define snip (make-object image-snip% (collection-file-path "plt.gif" "icons"))) + (send t insert "ABCD") + (check-equal? (send t get-text) "ABCD") + (send t insert snip 0) + (check-equal? (send t get-text) ".ABCD") + (send t move/copy-to-edit t 0 3 5 #:try-to-move? #t) + (check-equal? (send t get-text) "CD.AB") + (check-equal? (send t get-snip-position snip) 2) + (check-equal? (send t find-snip 2 'after) snip)) + +(let ([t (new text:basic%)]) + (define snip (make-object image-snip% (collection-file-path "plt.gif" "icons"))) + (send t insert "ABCD") + (send t insert snip 0) + (send t move/copy-to-edit t 0 3 5 #:try-to-move? #f) + (check-equal? (send t get-text) ".ABCD.AB") + (check-equal? (send t get-snip-position snip) 0) + (check-equal? (send t find-snip 0 'after) snip)) + +(let ([t1 (new text:basic%)] + [t2 (new text:basic%)]) + (define snip (make-object image-snip% (collection-file-path "plt.gif" "icons"))) + (send t1 insert "ABCD") + (send t1 insert snip 0) + (send t1 move/copy-to-edit t2 0 3 0 #:try-to-move? #t) + (check-equal? (send t1 get-text) "CD") + (check-equal? (send t2 get-text) ".AB") + (check-equal? (send t2 get-snip-position snip) 0) + (check-equal? (send t2 find-snip 0 'after) snip)) + +(let ([t1 (new text:basic%)] + [t2 (new text:basic%)]) + (define snip (make-object image-snip% (collection-file-path "plt.gif" "icons"))) + (send t1 insert "ABCD") + (send t1 insert snip 0) + (send t1 move/copy-to-edit t2 0 3 0 #:try-to-move? #f) + (check-equal? (send t1 get-text) ".ABCD") + (check-equal? (send t2 get-text) ".AB")) + +;; Random Tests +(define (random-sequence n [div 2]) + (for/list ([i (in-range n)] + #:when (zero? (random div))) + i)) + +(define (edit-string str start end dest-pos move?) + (define strlen (string-length str)) + (define sub (substring str start end)) + (cond + [(and move? (<= start dest-pos end)) str] + [else + (string-append + (if (and move? (<= end dest-pos)) + (string-append + (substring str 0 start) + (substring str end dest-pos)) + (substring str 0 dest-pos)) + sub + (if (and move? (<= dest-pos start)) + (string-append + (substring str dest-pos start) + (substring str end strlen)) + (substring str dest-pos strlen)))])) + +(check-equal? (edit-string "ABCDEF" 1 3 0 #f) "BCABCDEF") +(check-equal? (edit-string "ABCDEF" 1 3 0 #t) "BCADEF") +(check-equal? (edit-string "ABCDEF" 1 3 5 #f) "ABCDEBCF") +(check-equal? (edit-string "ABCDEF" 1 3 5 #t) "ADEBCF") + +(define (random-string n) + (define letters "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + (list->string + (for/list ([i (in-range (add1 (random n)))]) + (define ix (random (string-length letters))) + (string-ref letters ix)))) + +(define (random-check-edit-string) + (define str (random-string 1000)) + (define strlen (string-length str)) + (define start (random strlen)) + (define end (+ start (random (add1 (- strlen start))))) + (define dest-pos (random (add1 strlen))) + (define random-style (random-sequence (add1 strlen))) + (define this? (zero? (random 2))) + (define contract-this? (zero? (random 2))) + (define try-to-move? (zero? (random 2))) + (check-equal? + (edit-string/text str start end dest-pos this? random-style + #:try-to-move? try-to-move? + #:contract-this? contract-this?) + (edit-string str start end dest-pos try-to-move?))) +(for ([i (in-range 1000)]) + (random-check-edit-string)) + +(define (check-move/copy+delete-property) + (define t1 (new text:basic%)) + (define t2 (new text:basic%)) + (define str (random-string 1000)) + (define strlen (string-length str)) + (define start (random strlen)) + (define end (+ start (random (add1 (- strlen start))))) + (define dest-pos (random (add1 strlen))) + (define random-style (random-sequence (add1 strlen))) + (send t1 insert str) + (send t2 insert str) + (edit-style t1 random-style) + (edit-style t2 random-style) + (send t1 move-to t1 start end dest-pos) + (send t2 copy-to t2 start end dest-pos) + (cond + [(<= start dest-pos end) + (send t2 delete (+ dest-pos (- end start)) (+ end (- end start))) + (send t2 delete start dest-pos)] + [(<= dest-pos start) + (send t2 delete (+ start (- end start)) (+ end (- end start)))] + [(<= end dest-pos) + (send t2 delete start end)]) + (check-equal? (send t1 get-text) (send t2 get-text))) +(for ([i (in-range 1000)]) + (check-move/copy+delete-property)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ascii art boxes