* Tested in GIMP 2.99.14 *

A simple tool that smartly expands or collapses nested groups. Detects the how it should behave depending on the groups content. Finds the average expanded state of a groups sub-groups, and conforms all groups to that. Once the sub-groups are aligned it toggles the expanded state of the sub groups whilst keeping the top group open. Works best when connected to a keyboard shortcut.

The plug-in should appear in the “Layer/Stack” menu.

To download expand-collapse.scm
…follow the link, right click the page, Save as expand-collapse.scm, in a folder called expand-collapse, in a GIMP plug-ins location. In Linux, set the file to be executable.

#!/usr/bin/env gimp-script-fu-interpreter-3.0

(define debug #f)

(define (script-fu-expand-collapse img drwbles)
  (let*
    (
      (i 0)(allGrpLsts 0)(actGrpLst 0)(actG 0)(expd 0)(topGrp 0)(expdT 0)
      (grnChld 0)
    )

    ; filter the active selection
    (set! drwbles (exclude-children img drwbles))
    (set! drwbles (only-groups drwbles))

    ; create a list of group lists, branches of the layer tree
    (set! allGrpLsts (list->vector (get-branches img drwbles)))

    (while (< i (vector-length allGrpLsts))
      (set! actGrpLst (vector-ref allGrpLsts i))
      (set! actGrpLst (list->vector actGrpLst))
      (set! grnChld (grandchildren actGrpLst))

      ; find the top group in this list and expand if it's closed
      (set! topGrp (vector-ref actGrpLst (- (vector-length actGrpLst) 1)))
      (set! expdT (car (gimp-item-get-expanded topGrp)))
      (if (= expdT 0) (gimp-item-set-expanded topGrp 1))

      ; collapse the top level group if it has no grandchildren
      (if (and (= expdT 1) (= grnChld 0)) (gimp-item-set-expanded topGrp 0))

      ; find the average expanded state the group list
      (set! expd (get-average-expanded-state actGrpLst))

      (if debug
        (gimp-message
          (string-append
            " top group -> " (car (gimp-item-get-name topGrp))
            "\n expanded state -> " (number->string expdT)
            "\n expanded state beneath -> " (number->string expd)
            "\n grandchildren? -> " (number->string grnChld)
          )
        )
      )

      (expand-collapse-branch actGrpLst expd)

      (set! i (+ i 1))
    )

  )
)


(script-fu-register-filter "script-fu-expand-collapse"
 "Expand Group" 
 "Recursively expands or collapses the selection" 
 "Mark Sweeney"
 "Under GNU GENERAL PUBLIC LICENSE Version 3"
 "2023"
 "*"
 SF-ONE-OR-MORE-DRAWABLE
)
(script-fu-menu-register "script-fu-expand-collapse" "<Image>/Layer/Stack")

; copyright 2023, Mark Sweeney, Under GNU GENERAL PUBLIC LICENSE Version 3

; utility functions
(define (boolean->string bool) (if bool "#t" "#f"))

(define (exit msg)
  (gimp-message-set-handler 0)
  (gimp-message (string-append " >>> " msg " <<<"))
  (gimp-message-set-handler 2)
  (quit)
)

(define (here x)(gimp-message(string-append " >>> " (number->string x) " <<<")))


; given a list of groups, it finds the average expanded state
(define (get-average-expanded-state grpLst)
 (let*
    (
       (i 0)(actG 0)(chldrn 0)(average 0)(total 0)(chldrn 0)(c 0)(expand 0)
    )

    ; don't count the top group or empty groups
    (while (< i (- (vector-length grpLst) 1))
      (set! actG (vector-ref grpLst i))
      (set! chldrn (gimp-item-get-children actG))
      (when (> (car chldrn) 0)
        (set! total (+ total (car(gimp-item-get-expanded actG))))
        (set! c (+ c 1))
      )
      (set! i (+ i 1))
    )
   
    (if (> c 0)(set! average (/ total c)))
    (if (>= average 0.5)(set! expand 0))
    (if (< average 0.5)(set! expand 1))
    (if (= average 0)(set! expand 0))
    (if (= average 1)(set! expand 1))
    (if debug
      (gimp-message
        (string-append
          " average expanded state -> " (number->string average)
          "\n state for toggle -> "(number->string (trunc expand))

        )
      )
    )

  expand
  )
)


; expands or collapses a list of groups
(define (expand-collapse-branch actGrpLst state)
  (let*
    (
       (i 0)(actG 0)(chldrn 0)
    )

    (while (< i (- (vector-length actGrpLst) 1))
      (set! actG (vector-ref actGrpLst i))

      (if debug
        (gimp-message
          (string-append " testing group -> "
                         (car (gimp-item-get-name actG)))
                         " set expand to ->  " (number->string (- state 1))
        )
      )

      (set! chldrn (gimp-item-get-children actG))
      (when (> (car chldrn) 0)
        (if (= state 1)(gimp-item-set-expanded actG 0))
        (if (= state 0)(gimp-item-set-expanded actG 1))
      )

      (set! i (+ i 1))
    )

  )
)


;returns a list of groups representing the stack structure
(define (get-branches img drwbles)
  (let*
    (
       (i 0)(allGrpLsts ())(actGrpLst 0)(actG 0)
    )

    ; make list of group lists, in the right order, deepest child first
    (while (< i (vector-length drwbles))
      (set! actG (vector-ref drwbles i))
      (set! actGrpLst (get-all-groups img actG))
      (set! actGrpLst (reverse actGrpLst))
      (set! allGrpLsts (append allGrpLsts (list actGrpLst)))
      (if #f ;debug
        (gimp-message
          (string-append
            " selected ->  " (car(gimp-item-get-name actG ))
            "\n number of groups ->  " (number->string (length actGrpLst))
            "\n number of group lists ->  " (number->string (length allGrpLsts))
          )
        )
      )
      (set! i (+ i 1))
    )
  
  allGrpLsts
  )
)


; are there any grandchildren in a list of layers and folders, returns 1 / 0
(define (grandchildren lst)
  (let*
    (
      (i 0)(actC 0)(grandchild 0)(chldrn 0)(j 0)(gchldrn 0)(actL 0)
    )

    (if (list? lst )(set! lst (list->vector lst)))
    (while (< i (vector-length lst))
      (set! actL (vector-ref lst i))
      (set! j 0)
      (when (> (car (gimp-item-is-group actL)) 0)
        (set! chldrn (gimp-item-get-children actL))
        (while (< j (car chldrn))
          (set! actC (vector-ref (cadr chldrn) j))
          (when (> (car (gimp-item-is-group actC)) 0)
            (set! gchldrn (gimp-item-get-children actC))
            (when (> (car gchldrn) 0)
              (if #f (gimp-message " this list has grandchildren "))
              (set! grandchild 1)
              (set! i (vector-length lst))
              (set! j (car chldrn))
            )
          )
          (set! j (+ j 1))
        )
      )
      (set! i (+ i 1))
    )

    grandchild
  )
)


; filters out children from a list of layers
; returns the top levels groups, or layers that are in the root and in the list
(define (exclude-children img lstL)
  (let*
    (
    (i 0)(actL 0)(excLst())(parent 0)(allParents 0)(j 0)(found 0)
    )

    (if (list? lstL) (set! lstL (list->vector lstL)))
    (while (< i (vector-length lstL))
      (set! actL (vector-ref lstL i))
      (set! j 0)
      (set! found 0)
      (set! allParents (get-all-parents img actL))

      (while (< j (length allParents))
        (set! parent (nth j allParents))
          (when (and (member parent (vector->list lstL))
                (car (gimp-item-is-group actL)) )
            (set! found 1)
          )
      (set! j (+ j 1))
      )

      (when (= found 0)
        (set! excLst (append excLst (list actL)))
      )

      (set! i (+ i 1))
    )

  (list->vector excLst)
  )
)


(define (get-all-parents img actL)
  (let*
    (
      (parent 0)(allParents ())(i 0)
    )

    (set! parent (car(gimp-item-get-parent actL)))

    (if debug 
      (gimp-message 
        (string-append 
          "found parent ID: " 
          (number->string parent)
        )
      )
    )
    
    (when (> parent 0)
      (while (> parent 0)

        (set! allParents (append allParents (list parent)))
        (if debug 
          (gimp-message 
            (string-append 
              "found parent: " 
              (car(gimp-item-get-name parent))
            )
          )
        )
        (set! parent (car(gimp-item-get-parent parent)))
      )
    )
    allParents
  )
)



; filters a vector list of drawables, returns a vector list of only the groups
(define (only-groups drwbls)
  (let*
    (
      (i 0)(actL 0)(grpLst())
    )

    (while (< i (vector-length drwbls))
      (set! actL (vector-ref drwbls i))
      (when (= (car (gimp-item-is-group actL)) 1)
        (if (= (car (gimp-item-id-is-layer-mask actL)) 1)
          (set! actL (car(gimp-layer-from-mask actL)))
        )
        (set! grpLst (append grpLst (list actL)))
      )
      (set! i (+ i 1))
    )

    (list->vector grpLst)
  )
)


; finds only the groups and not the layers in all the image or inside a group
; (source image, source group/all image) set last parameter to 0 for all image
; returns a list of all the groups found including the given group
(define (get-all-groups img actL)
  (let*
    (
    (allGrp (get-sub-groups img actL))
    )

    ;add an initial group
    (when (> actL 0)
      (when (= (car (gimp-item-is-group actL)) 1)
        (if #f ;debug
          (gimp-message
            (string-append " initial group ->  "
                            (car(gimp-item-get-name actL))
                          "\n number of sub groups -> " 
                          (number->string (length allGrp))
            )
          )
        )
        (if (> (length allGrp) 1)(set! allGrp (reverse allGrp)))
        (set! allGrp (append allGrp (list actL)))
        (set! allGrp (reverse allGrp))
        (if (null? allGrp) (set! allGrp (list actL)))
      )
    )
    
    (if #f ;debug
      (gimp-message 
        (string-append " returning group length ->  "
                        (number->string (length allGrp))
        )
      )
    )

    allGrp
  )
)


; also used by (get-all-groups)
; finds only the groups and not the layers in all the image or inside a group
; (source image, source group/all image) set last parameter to 0 for all image
; returns a list of all the groups found not including the given group
(define (get-sub-groups img actL) ; recursive function
  (let*
    (
      (chldrn (list 0 #()))(lstL 0)(i 0)(allL ())(allGrp ())
      (grpTru 0)(actC 0)
    )

    (if (> actL 0)(set! grpTru (car (gimp-item-is-group actL))))
    (if (= grpTru 1)(set! chldrn (gimp-item-get-children actL)))
    (if (= actL 0)(set! chldrn (gimp-image-get-layers img)))

    (when (> (car chldrn) 0)
      (set! lstL (cadr chldrn))
      (while (< i (car chldrn))
        (set! actC (vector-ref lstL i))

        (if #f ;debug
          (gimp-message
            (string-append
              " group ->  "(car(gimp-item-get-name actL))
              "\n child ->  "(car(gimp-item-get-name actC))
            )
          )
        )

        (when (equal? (car (gimp-item-is-group actC)) 1)
          (if #f (gimp-message " child was a group "))
          (set! allGrp (append allGrp (list actC)))
          (set! allGrp (append allGrp (get-sub-groups img actC)))
        )

        (set! i (+ i 1))
      )


      (when (= (car chldrn) 0) ;debug
        (if #f
          (gimp-message 
            (string-append " an empty group ->  "
                          (car(gimp-item-get-name actL))
            )
          )
        )
      )
    )

    allGrp
  )
)


<
Previous Post
Parasites
>
Next Post
Collapse All Groups