;;; ---------------------------------------------------------------------------
;;; TagTweak.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; EDIT ONE ATTRIBUTE ACROSS EVERY BLOCK AT ONCE
;;;
;;; Pick a single attribute -- one door number, one pipe label, one revision
;;; tag -- and TagTweak applies your change to that SAME attribute in every
;;; matching block in the drawing.
;;;
;;; This is the fix for the classic attribute problem: someone changes the
;;; block definition, and now three hundred existing insertions have their
;;; tag sitting in slightly the wrong place, at the wrong height, or in the
;;; wrong style. ATTSYNC would reset all the values along with the geometry.
;;; TagTweak changes only what you ask it to change, and leaves the text
;;; content of each block alone unless you explicitly tick that box.
;;;
;;; Three commands share the same selection front end:
;;;
;;;   TAGMOVE  drag one attribute and every matching attribute moves by the
;;;            same displacement, keeping its own position relative to its
;;;            own block.
;;;   TAGSPIN  set a rotation angle and every matching attribute turns to it.
;;;   TAGEDIT  open a dialog and change any combination of content, text
;;;            style, justification, height, oblique angle and rotation.
;;;
;;; ---------------------------------------------------------------------------
;;; HOW THE SELECTION WORKS
;;;
;;; You are first asked to click an attribute. That single click establishes
;;; two things: the TAG NAME to operate on, and the BLOCK NAME that owns it.
;;;
;;; You are then asked to select blocks. Press Enter to take every insertion
;;; of that block in the drawing, or window a subset to limit the change.
;;; Either way the filter is restricted to that one block name, so a tag
;;; called "NUMBER" in your door blocks is never confused with a tag called
;;; "NUMBER" in your window blocks.
;;;
;;; The block name used is the EFFECTIVE name, so dynamic blocks that AutoCAD
;;; has renamed internally to "*U27" are still matched by the name you see.
;;;
;;; ---------------------------------------------------------------------------
;;; LOCKED LAYERS
;;;
;;; Attributes on locked layers cannot be modified. Rather than silently
;;; skipping them, all layers are unlocked for the duration of the edit and
;;; relocked afterwards -- including if the command errors or is cancelled.
;;; The list of which layers were locked is captured before anything changes,
;;; so the original lock state is restored exactly.
;;;
;;; ---------------------------------------------------------------------------
;;; A NOTE ON CONSTANT ATTRIBUTES
;;;
;;; TAGMOVE and TAGSPIN also act on constant attributes. Those live in the
;;; block DEFINITION rather than in each insertion, so changing one changes
;;; every insertion of that block everywhere in the drawing -- including any
;;; you did not select. This is intentional (there is no other way to move a
;;; constant attribute) but worth knowing before running it on a subset.
;;;
;;; TAGEDIT does not touch constant attributes, because their content and
;;; formatting properly belong to the block definition.
;;;
;;; ---------------------------------------------------------------------------
;;;   TAGMOVE - move one attribute in every matching block
;;;   TAGSPIN - rotate one attribute in every matching block
;;;   TAGEDIT - change formatting of one attribute in every matching block
;;; ---------------------------------------------------------------------------

(vl-load-com)

;;; ---------------------------------------------------------------------------
;;; *TagTweak:Mode*
;;;
;;; Bit flags recording which TAGEDIT check boxes were ticked last time, so
;;; the dialog reopens the way it was left. Global because it must survive
;;; between commands.
;;;
;;;    2  content        16  height
;;;    4  text style     32  oblique angle
;;;    8  justification  64  rotation
;;; ---------------------------------------------------------------------------

(or *TagTweak:Mode* (setq *TagTweak:Mode* 2))

;;; ---------------------------------------------------------------------------
;;; TagTweak:Doc
;;;
;;; Returns the active document, caching itself after the first call.
;;; ---------------------------------------------------------------------------

(defun TagTweak:Doc nil
    (eval (list 'defun 'TagTweak:Doc 'nil (vla-get-activedocument (vlax-get-acad-object))))
    (TagTweak:Doc)
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:StartUndo / TagTweak:EndUndo
;;;
;;; Undo group control. EndUndo LOOPS on bit 8 of UNDOCTL rather than
;;; testing it once, so that a group left open by an interrupted operation
;;; is also closed -- otherwise the user's next undo would swallow unrelated
;;; work as well.
;;; ---------------------------------------------------------------------------

(defun TagTweak:EndUndo ( )
    (while (= 8 (logand 8 (getvar 'undoctl)))
        (vla-endundomark (TagTweak:Doc))
    )
    (princ)
)

(defun TagTweak:StartUndo ( )
    (TagTweak:EndUndo)
    (vla-startundomark (TagTweak:Doc))
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:UnlockLayers
;;;
;;; Unlocks every locked layer and returns the list of layer objects that
;;; were unlocked, so they can be put back exactly as they were.
;;;
;;; Returning the list rather than a simple flag is what makes the restore
;;; precise: layers that were already unlocked are never touched, so the
;;; drawing's lock state is unchanged by a successful run.
;;; ---------------------------------------------------------------------------

(defun TagTweak:UnlockLayers ( / lst )
    (vlax-for lay (vla-get-layers (TagTweak:Doc))
        (if (= :vlax-true (vla-get-lock lay))
            (setq lst (cons lay lst))
        )
    )
    (foreach lay lst (vla-put-lock lay :vlax-false))
    lst
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:RelockLayers
;;;
;;; Relocks every layer in the supplied list. Each is caught individually so
;;; that a layer deleted or purged mid-command cannot prevent the rest from
;;; being restored.
;;; ---------------------------------------------------------------------------

(defun TagTweak:RelockLayers ( lst )
    (foreach lay lst
        (vl-catch-all-apply 'vla-put-lock (list lay :vlax-true))
    )
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:BlockName
;;;
;;; Returns the name a block reference shows in the user interface.
;;;
;;; A dynamic block whose parameters have been changed is stored internally
;;; under an anonymous name such as "*U27"; EffectiveName is the visible one.
;;; Older releases lack that property, so it is tested for.
;;; ---------------------------------------------------------------------------

(defun TagTweak:BlockName ( obj )
    (vlax-get-property obj
        (if (vlax-property-available-p obj 'effectivename)
            'effectivename
            'name
        )
    )
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:SelectIf
;;;
;;; Repeats a selection prompt until the user picks something that satisfies
;;; the supplied test, or presses Enter to give up.
;;;
;;; ERRNO 7 means the pick found nothing at all, which deserves a different
;;; message from picking the wrong kind of object.
;;;
;;;   test - predicate taking an entity name; nil to accept anything
;;;   fn   - selection function, e.g. nentsel
;;;   msg  - prompt string
;;;
;;; Returns the chosen entity name, or nil.
;;; ---------------------------------------------------------------------------

(defun TagTweak:SelectIf ( test fn msg / ent )
    (while
        (progn
            (setvar 'errno 0)
            (setq ent (car (fn msg)))
            (cond
                (   (= 7 (getvar 'errno))
                    (princ "\nMissed, try again.")
                )
                (   (= 'ename (type ent))
                    (if (and test (not (test ent)))
                        (princ "\nThat is not an attribute - try again.")
                    )
                )
            )
        )
    )
    ent
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:GetSelection
;;;
;;; The shared front end for all three commands.
;;;
;;; Asks for one attribute, works out its tag and owning block, then asks for
;;; the blocks to change. Pressing Enter at the block prompt takes every
;;; insertion in the drawing.
;;;
;;; nentsel is used rather than entsel because an attribute is nested inside
;;; its block reference and entsel would return the block, not the attribute.
;;;
;;; NOMUTT is set to 1 around the ssget call. Without it AutoCAD prints its
;;; own "Select objects:" prompt on top of ours, and the "<All>" hint that
;;; tells the user Enter means everything would scroll away unseen. It is
;;; restored by the caller, which holds it in its saved system variable list
;;; so that an error cannot leave AutoCAD silent.
;;;
;;;   allowmtext - nil to reject multiline attributes, which do not support
;;;                the single-line properties TAGMOVE and TAGSPIN rely on
;;;
;;; Returns (attribute-object selection-set tag-string), or nil.
;;; ---------------------------------------------------------------------------

(defun TagTweak:GetSelection ( allowmtext / bnm ent obj sel tag )
    (if
        (and
            (setq ent
                (TagTweak:SelectIf
                   '(lambda ( x / o )
                        (and (= "ATTRIB" (cdr (assoc 0 (entget x))))
                             (or allowmtext
                                 (not (vlax-property-available-p
                                          (setq o (vlax-ename->vla-object x)) 'mtextattribute))
                                 (= :vlax-false (vla-get-mtextattribute o))
                             )
                        )
                    )
                    nentsel "\nSelect attribute: "
                )
            )
            (princ (strcat "\nTag '" (cdr (assoc 2 (entget ent))) "' selected."))
            ;; The attribute's owner is the block reference containing it.
            (setq obj (vlax-ename->vla-object ent)
                  bnm (TagTweak:BlockName
                          (vla-objectidtoobject (TagTweak:Doc) (vla-get-ownerid obj))
                      )
                  tag (vla-get-tagstring obj)
            )
            (progn
                (setvar 'nomutt 1)
                (princ "\nSelect blocks <All>: ")
                ;; DXF 66 = 1 restricts the filter to blocks that actually
                ;; carry attributes, which keeps the "All" fallback from
                ;; sweeping up plain insertions of the same name.
                (setq sel
                    (cond
                        (   (ssget      (list '(0 . "INSERT") (cons 2 bnm) '(66 . 1))))
                        (   (ssget "_X" (list '(0 . "INSERT") (cons 2 bnm) '(66 . 1))))
                    )
                )
                (setvar 'nomutt 0)
                sel
            )
        )
        (list obj sel tag)
    )
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:ForEachTag
;;;
;;; Walks every block in a selection set and calls the supplied function on
;;; each attribute whose tag matches.
;;;
;;; The attribute list is the concatenation of GetAttributes (the per-
;;; insertion attributes) and, optionally, GetConstantAttributes (those held
;;; in the block definition). See the header for why constant attributes are
;;; included for some commands and not others.
;;;
;;;   sel      - selection set of block references
;;;   tag      - tag string to match
;;;   constant - non-nil to include constant attributes
;;;   fn       - function taking one attribute object
;;;
;;; Returns the number of attributes acted on.
;;; ---------------------------------------------------------------------------

(defun TagTweak:ForEachTag ( sel tag constant fn / cnt idx obj )
    (setq cnt 0
          idx -1
    )
    (while (setq obj (ssname sel (setq idx (1+ idx))))
        (setq obj (vlax-ename->vla-object obj))
        (foreach att
            (append
                (vlax-invoke obj 'getattributes)
                (if constant (vlax-invoke obj 'getconstantattributes))
            )
            (if (= tag (vla-get-tagstring att))
                (progn (fn att) (setq cnt (1+ cnt)))
            )
        )
    )
    cnt
)

;;; ===========================================================================
;;; TAGMOVE  -  move one attribute in every matching block
;;; ===========================================================================
;;;
;;; The two picked points define a DISPLACEMENT, not a destination. Every
;;; matching attribute shifts by that same vector, so each keeps its own
;;; position relative to its own block -- which is what you want when the
;;; blocks are scattered all over the drawing.
;;; ---------------------------------------------------------------------------

(defun c:TagMove ( / *error* asel base cnt disp lck sel tag vals vars )

    (setq vars '("CMDECHO" "NOMUTT")
          vals (mapcar 'getvar vars)
    )

    (defun *error* ( msg )
        (TagTweak:RelockLayers lck)
        (TagTweak:EndUndo)
        (mapcar 'setvar vars vals)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** TAGMOVE error: " msg " **"))
        )
        (princ)
    )

    (setvar 'cmdecho 0)

    (if
        (and
            (setq asel (TagTweak:GetSelection t))
            (setq base (getpoint "\nSpecify base point: "))
            (setq disp (getpoint "\nSpecify second point: " base))
        )
        (progn
            ;; Points are picked in the current UCS but vla-Move works in
            ;; world coordinates, so both are transformed before use.
            (setq base (vlax-3d-point (trans base 1 0))
                  disp (vlax-3d-point (trans disp 1 0))
                  sel  (cadr  asel)
                  tag  (caddr asel)
            )
            (TagTweak:StartUndo)
            (setq lck (TagTweak:UnlockLayers))

            (setq cnt
                (TagTweak:ForEachTag sel tag t
                   '(lambda ( att ) (vla-move att base disp))
                )
            )

            (TagTweak:RelockLayers lck)
            (setq lck nil)
            (TagTweak:EndUndo)
            (princ (strcat "\n" (itoa cnt) " attribute" (if (= 1 cnt) "" "s") " moved."))
        )
        (princ "\nCancelled.")
    )

    (mapcar 'setvar vars vals)
    (princ)
)

;;; ===========================================================================
;;; TAGSPIN  -  rotate one attribute in every matching block
;;; ===========================================================================
;;;
;;; Unlike TAGMOVE this sets an ABSOLUTE angle: every matching attribute ends
;;; up at the same rotation, which is normally the point of the exercise.
;;; ---------------------------------------------------------------------------

(defun c:TagSpin ( / *error* asel base cnt lck obj rot sel tag vals vars )

    (setq vars '("CMDECHO" "NOMUTT")
          vals (mapcar 'getvar vars)
    )

    (defun *error* ( msg )
        (TagTweak:RelockLayers lck)
        (TagTweak:EndUndo)
        (mapcar 'setvar vars vals)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** TAGSPIN error: " msg " **"))
        )
        (princ)
    )

    (setvar 'cmdecho 0)

    (if
        (and
            (setq asel (TagTweak:GetSelection t))
            ;; Rubber-band the angle prompt from the picked attribute's own
            ;; position, so the user can see what they are aiming at.
            ;;
            ;; Which position property holds that point depends on the
            ;; attribute's justification: a left-justified single-line
            ;; attribute uses InsertionPoint, anything else uses
            ;; TextAlignmentPoint. Reading the wrong one returns the origin.
            (setq obj (car asel))
            (setq base
                (trans
                    (vlax-get obj
                        (if (and (or (not (vlax-property-available-p obj 'mtextattribute))
                                     (= :vlax-false (vla-get-mtextattribute obj))
                                 )
                                 (/= acalignmentleft (vla-get-alignment obj))
                            )
                            'textalignmentpoint
                            'insertionpoint
                        )
                    )
                    0 1
                )
            )
            (setq rot (getangle "\nSpecify rotation angle: " base))
        )
        (progn
            ;; getangle returns an angle relative to the current UCS. Object
            ;; rotation is stored relative to the world, so the UCS rotation
            ;; is added back in -- without this, working in a rotated UCS
            ;; would leave every attribute skewed by the UCS angle.
            (setq rot (+ rot (angle '(0. 0. 0.)
                                    (trans (getvar 'ucsxdir) 0
                                           (trans '(0. 0. 1.) 1 0 t) t)))
                  sel (cadr  asel)
                  tag (caddr asel)
            )
            (TagTweak:StartUndo)
            (setq lck (TagTweak:UnlockLayers))

            (setq cnt
                (TagTweak:ForEachTag sel tag t
                   '(lambda ( att ) (vla-put-rotation att rot))
                )
            )

            (TagTweak:RelockLayers lck)
            (setq lck nil)
            (TagTweak:EndUndo)
            (princ (strcat "\n" (itoa cnt) " attribute" (if (= 1 cnt) "" "s") " rotated."))
        )
        (princ "\nCancelled.")
    )

    (mapcar 'setvar vars vals)
    (princ)
)

;;; ===========================================================================
;;; TAGEDIT  -  change attribute formatting through a dialog
;;; ===========================================================================
;;;
;;; The dialog pairs each property with a check box. Only ticked properties
;;; are written, so you can change height across three hundred blocks without
;;; disturbing their text, style or position.
;;;
;;; Every field is pre-filled from the attribute you picked, so leaving a box
;;; ticked with its value untouched simply propagates that attribute's
;;; current setting to all the others -- which is often exactly the job.
;;;
;;; The check box states are held as bit flags in *TagTweak:Mode* and
;;; persist between runs.
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; The field table. Each row is:
;;;   toggle key, edit key, bit flag, ActiveX property name
;;;
;;; The order is the order the properties are applied in. Alignment
;;; deliberately comes before the size properties: changing justification
;;; repositions the text, and the size changes should follow that.
;;; ---------------------------------------------------------------------------

(setq *TagTweak:Fields*
   '(
        ("t2"  "e2"   2 textstring  )
        ("t4"  "e4"   4 stylename   )
        ("t8"  "e8"   8 alignment   )
        ("t16" "e16" 16 height      )
        ("t32" "e32" 32 obliqueangle)
        ("t64" "e64" 64 rotation    )
    )
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:Put
;;;
;;; Returns the association list with the value for the given key replaced.
;;; Used by the dialog callbacks to record what has been typed without
;;; needing a separate variable per edit box.
;;; ---------------------------------------------------------------------------

(defun TagTweak:Put ( lst key val )
    (if (assoc key lst)
        (subst (cons key val) (assoc key lst) lst)
        (cons  (cons key val) lst)
    )
)

;;; ---------------------------------------------------------------------------
;;; TagTweak:Get
;;;
;;; Returns the current string value for an edit box key.
;;; ---------------------------------------------------------------------------

(defun TagTweak:Get ( lst key )
    (cdr (assoc key lst))
)

(defun c:TagEdit

    ( / *error* TagTweak:Fill aligns asel cnt dch dcl des edits fld lck obj
        props sel styles tag tmp vals vars result
    )

    (setq vars '("CMDECHO" "NOMUTT")
          vals (mapcar 'getvar vars)
    )

    (defun *error* ( msg )
        (if (= 'file (type des)) (close des))
        (if (and (= 'int (type dch)) (< 0 dch)) (unload_dialog dch))
        (if (and (= 'str (type dcl)) (findfile dcl)) (vl-file-delete dcl))
        (TagTweak:RelockLayers lck)
        (TagTweak:EndUndo)
        (mapcar 'setvar vars vals)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** TAGEDIT error: " msg " **"))
        )
        (princ)
    )

    ;;; -----------------------------------------------------------------------
    ;;; TagTweak:Fill
    ;;;
    ;;; Loads a popup list tile with the supplied strings.
    ;;; -----------------------------------------------------------------------

    (defun TagTweak:Fill ( key lst )
        (start_list key)
        (foreach x lst (add_list x))
        (end_list)
        (princ)
    )

    (setvar 'cmdecho 0)

    ;; ---- gather the choices the dialog will offer --------------------------

    (vlax-for sty (vla-get-textstyles (TagTweak:Doc))
        (setq styles (cons (vla-get-name sty) styles))
    )
    (setq styles (acad_strlsort styles))

    ;; Name shown in the list, paired with the enum value the property takes.
    (setq aligns
        (list
            (cons "Left"          acalignmentleft        )
            (cons "Center"        acalignmentcenter      )
            (cons "Right"         acalignmentright       )
            (cons "Aligned"       acalignmentaligned     )
            (cons "Middle"        acalignmentmiddle      )
            (cons "Fit"           acalignmentfit         )
            (cons "Top-Left"      acalignmenttopleft     )
            (cons "Top-Center"    acalignmenttopcenter   )
            (cons "Top-Right"     acalignmenttopright    )
            (cons "Middle-Left"   acalignmentmiddleleft  )
            (cons "Middle-Center" acalignmentmiddlecenter)
            (cons "Middle-Right"  acalignmentmiddleright )
            (cons "Bottom-Left"   acalignmentbottomleft  )
            (cons "Bottom-Center" acalignmentbottomcenter)
            (cons "Bottom-Right"  acalignmentbottomright )
        )
    )

    (cond
        ;;  ---- selection first, so a cancelled pick costs nothing ----------
        (   (not (setq asel (TagTweak:GetSelection nil)))
            (princ "\nCancelled.")
        )

        ;;  ---- build and load the dialog -----------------------------------
        ;;  The DCL is written to a uniquely named temporary file, loaded,
        ;;  and deleted when the command ends. A unique name means two
        ;;  AutoCAD sessions running at once cannot collide over it, and
        ;;  deleting it means nothing is left behind in the support folder.
        (   (not
                (and
                    (setq dcl (vl-filename-mktemp nil nil ".dcl"))
                    (setq des (open dcl "w"))
                    (progn
                        (foreach line
                           '(
                                "edit5 : edit_box   { edit_limit = 5; fixed_width = true; }"
                                "pop   : popup_list { width = 20;     fixed_width = true; }"
                                ""
                                "TagEdit : dialog { label = \"Attribute Modification\"; initial_focus = \"e2\";"
                                "  spacer;"
                                "  : toggle    { key = \"t2\" ; label = \"Content\"; }"
                                "  : edit_box  { key = \"e2\" ; width = 45; fixed_width = true; }"
                                "  spacer;"
                                "  : row {"
                                "    : toggle  { key = \"t4\" ; label = \"Style\"        ; }"
                                "    : pop     { key = \"e4\" ; }"
                                "  }"
                                "  : row {"
                                "    : toggle  { key = \"t8\" ; label = \"Alignment\"    ; }"
                                "    : pop     { key = \"e8\" ; }"
                                "  }"
                                "  : row {"
                                "    : toggle  { key = \"t16\"; label = \"Height\"       ; }"
                                "    : edit5   { key = \"e16\"; }"
                                "  }"
                                "  : row {"
                                "    : toggle  { key = \"t32\"; label = \"Oblique Angle\"; }"
                                "    : edit5   { key = \"e32\"; }"
                                "  }"
                                "  : row {"
                                "    : toggle  { key = \"t64\"; label = \"Rotation\"     ; }"
                                "    : edit5   { key = \"e64\"; }"
                                "  }"
                                "  spacer;"
                                "  ok_cancel;"
                                "}"
                            )
                            (write-line line des)
                        )
                        (setq des (close des))
                        (< 0 (setq dch (load_dialog dcl)))
                    )
                    (new_dialog "TagEdit" dch)
                )
            )
            (princ "\nUnable to create the dialog.")
        )

        (   t
            (setq obj (car   asel)
                  sel (cadr  asel)
                  tag (caddr asel)
            )

            (TagTweak:Fill "e4" styles)
            (TagTweak:Fill "e8" (mapcar 'car aligns))

            ;; ---- pre-fill every field from the picked attribute -----------
            ;; Popup lists take an index into their own contents, so the
            ;; attribute's current style and alignment are converted to
            ;; positions in the lists just loaded.
            (setq edits
                (list
                    (cons "e2"  (vla-get-textstring obj))
                    (cons "e4"  (itoa (vl-position (vla-get-stylename obj) styles)))
                    (cons "e8"  (itoa (vl-position (vla-get-alignment obj)
                                                   (mapcar 'cdr aligns))))
                    (cons "e16" (rtos   (vla-get-height       obj)))
                    (cons "e32" (angtos (vla-get-obliqueangle obj)))
                    (cons "e64" (angtos (vla-get-rotation     obj)))
                )
            )

            ;; ---- wire up the tiles ----------------------------------------
            ;;
            ;; Callback expressions are built with vl-prin1-to-string rather
            ;; than hand-assembled strings: the printer emits correct quoting
            ;; every time, where hand-escaping is a reliable source of bugs.
            ;;
            ;; Each edit box is greyed out when its check box is clear, so the
            ;; dialog makes plain at a glance which properties will change.

            (foreach fld *TagTweak:Fields*
                (   (lambda ( tkey ekey bit )
                        (set_tile ekey (TagTweak:Get edits ekey))
                        (set_tile tkey
                            (if (= bit (logand bit *TagTweak:Mode*)) "1" "0")
                        )
                        (mode_tile ekey
                            (if (= bit (logand bit *TagTweak:Mode*)) 0 1)
                        )
                        ;; Toggle: flip the bit and enable or grey the field.
                        (action_tile tkey
                            (vl-prin1-to-string
                                (list 'progn
                                    (list 'setq '*TagTweak:Mode*
                                        (list 'if '(= "1" $value)
                                            (list 'logior '*TagTweak:Mode* bit)
                                            (list 'logand '*TagTweak:Mode* (~ bit))
                                        )
                                    )
                                    (list 'mode_tile ekey '(- 1 (atoi $value)))
                                )
                            )
                        )
                        ;; Field: record what was typed or chosen.
                        (action_tile ekey
                            (vl-prin1-to-string
                                (list 'setq 'edits
                                    (list 'TagTweak:Put 'edits ekey '$value)
                                )
                            )
                        )
                    )
                    (car fld) (cadr fld) (caddr fld)
                )
            )

            ;; ---- OK: validate before closing -------------------------------
            ;; Validation happens here rather than after the dialog closes so
            ;; that a bad value can be corrected without losing everything
            ;; else the user has set up.
            (action_tile "accept"
                (vl-prin1-to-string
                   '(cond
                        (   (not (and (setq tmp (distof (TagTweak:Get edits "e16")))
                                      (< 0 tmp)
                                 )
                            )
                            (alert "Height must be a positive number.")
                        )
                        ;;  AutoCAD rejects oblique angles outside +/-85
                        ;;  degrees, which is 17/36 of pi.
                        (   (not (and (setq tmp (angtof (TagTweak:Get edits "e32")))
                                      (<= (/ (* pi -17.) 36.) tmp (/ (* pi 17.) 36.))
                                 )
                            )
                            (alert "Oblique angle must be between -85 and 85 degrees.")
                        )
                        (   (not (angtof (TagTweak:Get edits "e64")))
                            (alert "Rotation must be a valid angle.")
                        )
                        (   t (done_dialog 1))
                    )
                )
            )

            (setq result (start_dialog)
                  dch    (unload_dialog dch)
            )
            (vl-file-delete dcl)
            (setq dcl nil)

            (if (/= 1 result)
                (princ "\nCancelled.")
                (progn
                    ;; ---- turn the ticked fields into property/value pairs ----
                    ;; Built in table order and then reversed, so the
                    ;; properties are applied in the order documented above.
                    (foreach fld *TagTweak:Fields*
                        (if (= (caddr fld) (logand (caddr fld) *TagTweak:Mode*))
                            (setq props
                                (cons
                                    (list (cadddr fld)
                                        (   (lambda ( prop val )
                                                (cond
                                                    ((= 'stylename    prop) (nth (atoi val) styles))
                                                    ((= 'alignment    prop) (cdr (nth (atoi val) aligns)))
                                                    ((= 'height       prop) (distof val))
                                                    ((= 'obliqueangle prop) (angtof val))
                                                    ((= 'rotation     prop) (angtof val))
                                                    (t val)
                                                )
                                            )
                                            (cadddr fld) (TagTweak:Get edits (cadr fld))
                                        )
                                    )
                                    props
                                )
                            )
                        )
                    )
                    (setq props (reverse props))

                    (if (null props)
                        (princ "\nNo properties were selected for change.")
                        (progn
                            (TagTweak:StartUndo)
                            (setq lck (TagTweak:UnlockLayers))

                            (setq cnt
                                (TagTweak:ForEachTag sel tag nil
                                   '(lambda ( att / hold )
                                        (foreach prop props
                                            ;; Changing justification moves the
                                            ;; text: AutoCAD switches which
                                            ;; point governs its position, and
                                            ;; the new one is not yet set. The
                                            ;; old position is captured first
                                            ;; and written back afterwards, so
                                            ;; the attribute stays put instead
                                            ;; of jumping to the origin.
                                            (if (= 'alignment (car prop))
                                                (setq hold (vla-get-insertionpoint att))
                                            )
                                            ;; Caught individually: one
                                            ;; attribute on a locked or frozen
                                            ;; layer, or of an incompatible
                                            ;; type, must not abort the rest.
                                            (vl-catch-all-apply 'vlax-put-property
                                                (cons att prop)
                                            )
                                            (if (and hold
                                                     (= 'alignment (car prop))
                                                     (/= acalignmentleft (cadr prop))
                                                )
                                                (vla-put-textalignmentpoint att hold)
                                            )
                                        )
                                    )
                                )
                            )

                            (TagTweak:RelockLayers lck)
                            (setq lck nil)
                            (TagTweak:EndUndo)
                            (princ (strcat "\n" (itoa cnt) " attribute"
                                           (if (= 1 cnt) "" "s") " updated."))
                        )
                    )
                )
            )
        )
    )

    (if (and (= 'str (type dcl)) (findfile dcl)) (vl-file-delete dcl))
    (mapcar 'setvar vars vals)
    (princ)
)

(princ "\nTagTweak loaded. TAGMOVE / TAGSPIN / TAGEDIT to modify attributes across blocks.")
(princ)

;;; ---------------------------------------------------------------------------
;;; End of file
;;; ---------------------------------------------------------------------------
