Layers panel

Layer Manager

A docked list of the drawing's layers with a filter box. Pick layers to isolate, freeze, turn off, lock, make current, merge or delete them without opening the Layer Properties Manager.

On the ribbonDock panel, Layers panel
NeedsAn open drawing for the list to show anything
ShortcutNone (keytip L M)

What it does#

Opens an AutoCAD palette titled Layer Manager that lists every layer of the active drawing, sorted by name, with whether it is current, on, frozen and locked, and its colour number. Filter the list, pick one or more layers, and act on them with the buttons underneath. It is lighter than the Layer Properties Manager and stays open beside the drawing while you work.

How to use it#

  1. Click Layer Manager on the ribbon. The button stays pressed while the palette is showing; click it again to hide it. Drag the palette to dock it on the left or right, or leave it floating.
  2. Type in the Filter box to narrow the list. Plain text matches anywhere in the name, ignoring case. A term with * or ? is a wildcard pattern over the whole name (A-* means names starting with A-). Separate several terms with commas: wall, E-*.
  3. Pick layers with click, Ctrl+click and Shift+click. The status line underneath counts them and names the current layer.
  4. Click an action. Each one changes the drawing straight away, shows a toast, and re-reads the list.
  5. Double-click a row to make that layer current.

The buttons#

  • Isolate: thaws and turns on the selected layers and freezes every other layer. If the current layer is not among them, the first selected layer becomes current.
  • Freeze/Thaw: freezes the selected layers, or thaws them when every one is already frozen. The current layer cannot be frozen, so it is skipped and the toast says so.
  • On/Off: turns the selected layers off, or on when every one is already off.
  • Lock/Unlock: locks the selected layers, or unlocks them when every one is already locked.
  • Set current (one layer selected): thaws it, turns it on and makes it current.
  • Merge into...: asks for a target layer name, moves everything on the selected layers onto it, then deletes them. If no layer has that name, it offers to create one (colour 7, Continuous). The toast says how many objects moved.
  • Delete: asks first, then deletes the selected layers. Layer 0, Defpoints, the current layer, xref layers and layers still in use are kept, and the toast gives the first reason; the rest are in the pyACAD log.

Good to know / limits#

  • The list follows the active drawing. It is read again when the palette is shown, when the mouse comes back over it, and when you click Refresh, so a change made elsewhere shows up without you asking. Your selection and scroll position are kept when nothing changed.
  • Merge reaches everywhere in the drawing: objects inside block definitions and attributes move too, so every reference of such a block changes, and objects on locked layers are moved as well. If a selected layer cannot be deleted afterwards (layer 0, Defpoints, an xref layer), the merge fails as a whole and nothing changes.
  • Freeze, Isolate and the rest work on the layers themselves, for every viewport. They do not use viewport freeze.
  • The palette acts outside any AutoCAD command. Check how U behaves on a palette action before you rely on it for a large Merge or Delete, or take a Layer Snapshot first.
  • The Color column shows the AutoCAD colour number, not a swatch, and is not editable. Neither is any other column: rename layers and change their properties in the Layer Properties Manager or with Layer Mapper.
  • The palette is not in the Shortcuts editor. Reach it from the keyboard with its keytip: press Alt, follow the letters the ribbon shows to the pyACAD tab, then LM.
  • AutoCAD remembers the palette's size, dock side and whether it was showing. A Reload rebuilds it and clears the filter box.

Other uses#

  • Focus on one discipline: filter E-*, select all the rows, Isolate.
  • Tidy an imported drawing: filter for its odd prefixes, Merge them into your own layers, then Delete what is left empty.
  • Keep it open as a live readout of which layer is current and what is frozen.

Source#

The tool's own files, word for word as shipped in Layers.panel/Layer_Manager.dockpane. Every pyACAD tool is an ordinary bundle: copy these into your own extension and change what you like.

script.py Runs on a click. View on GitHub
# -*- coding: utf-8 -*-
"""The Layer Manager palette: the active drawing's layers, filtered, with actions.

The list reads the layer table in one transaction (layers.snapshot) and reads it
again whenever the mouse comes back into the palette, so it follows changes made
elsewhere without a timer, and re-draws only when something changed so the
selection and scroll stay put. Every button acts on the selected rows through
pyacad.layers under a document lock, because a palette click runs outside any
command, then refreshes the list. A failure toasts its message.

This runs once, when the palette is built; the handlers it wires fire later, so
they act through `pane`, never through whichever command ran last.
"""

__title__ = 'Layer Manager'

import clr

clr.AddReference('PresentationCore')
clr.AddReference('PresentationFramework')
clr.AddReference('WindowsBase')
clr.AddReference('PyAcad.Runtime')

from System import String
from System.Collections.Generic import List
from System.Windows import Setter, Style
from System.Windows.Controls import Control, GridViewColumnHeader, ItemsControl
from System.Windows.Documents import TextElement
from PyAcad.Runtime.Forms import DesignSystem

from pyacad import doc, forms, layers, script, toast

pane = __pane__
log = script.get_logger()

root = pane.Find('Root')
filter_box = pane.Find('Filter')
layer_list = pane.Find('LayerList')
status = pane.Find('Status')
ACTIONS = ('Isolate', 'FreezeThaw', 'OnOff', 'LockUnlock', 'SetCurrent', 'Merge', 'Delete')
buttons = dict((name, pane.Find(name)) for name in ACTIONS)

# What the list shows: the last layer read, the current layer, or why there is none.
view = {'snapshot': {}, 'current': '', 'error': None}


# ---- reading and showing ------------------------------------------------------

def refresh(quiet=False):
    """Reads the active drawing's layers and re-draws the list if they changed.

    quiet (mouse-enter and visibility refreshes) keeps the current list when the
    read fails for a passing reason, such as a command holding the drawing.
    """
    try:
        doc.active()
    except RuntimeError as error:
        update({}, '', str(error), force=not quiet)
        return
    try:
        with doc.lock():
            snapshot = layers.snapshot()
            current = layers.current()
    except Exception as error:
        if not quiet:
            log.warning('could not read the layers: %s' % error)
            update({}, '', 'Could not read the layers: %s' % error, force=True)
        return
    update(snapshot, current, None, force=not quiet)


def update(snapshot, current, error, force=False):
    same = (snapshot == view['snapshot'] and current == view['current']
            and error == view['error'])
    if same and not force:
        return
    view.update(snapshot=snapshot, current=current, error=error)
    show()


def show():
    """Fills the list from the last read, through the filter box, keeping the selection."""
    keep = set(selected())
    snapshot = view['snapshot']
    names = layers.filter_names(sorted(snapshot, key=lambda n: n.lower()), filter_box.Text)
    rows = List[List[String]]()
    for name in names:
        rows.Add(row_for(name, snapshot[name]))
    layer_list.ItemsSource = rows
    for row in rows:
        if row[0] in keep:
            layer_list.SelectedItems.Add(row)
    on_selection()


def row_for(name, state):
    """The list row: [0] name, [1] current mark, [2] on, [3] frozen, [4] locked, [5] color."""
    row = List[String]()
    cells = (name,
             u'\u2713' if name.lower() == view['current'].lower() else '',
             'No' if state.get('is_off') else 'Yes',
             'Yes' if state.get('is_frozen') else 'No',
             'Yes' if state.get('is_locked') else 'No',
             str(state.get('color', '')))
    for cell in cells:
        row.Add(cell)
    return row


def selected():
    return [str(row[0]) for row in layer_list.SelectedItems]


def on_selection(sender=None, args=None):
    """Status line and which buttons apply to the selection."""
    count = layer_list.SelectedItems.Count
    for name, button in buttons.items():
        button.IsEnabled = count == 1 if name == 'SetCurrent' else count > 0
    if view['error']:
        status.Text = view['error']
        return
    total = len(view['snapshot'])
    shown = layer_list.Items.Count
    parts = ['%d layers' % total if shown == total else '%d of %d layers' % (shown, total),
             'current: %s' % view['current']]
    if count:
        parts.append('%d selected' % count)
    status.Text = u'  \u00b7  '.join(parts)


def counted(n):
    n = n if isinstance(n, int) else len(n)
    return '%d layer%s' % (n, '' if n == 1 else 's')


# ---- actions ------------------------------------------------------------------

def action(label, work):
    """A click handler running work(selected names), toasting any failure, then
    re-reading the layers."""
    def handle(sender, args):
        names = selected()
        if not names:
            toast.info('Select one or more layers first')
            return
        try:
            work(names)
        except Exception as error:
            log.error('%s failed: %s' % (label, error))
            toast.error('%s failed' % label, str(error))
        refresh()
    return handle


def all_set(names, flag):
    """True when every named layer has flag set, read fresh from the drawing."""
    states = layers.snapshot()
    return all(states.get(name, {}).get(flag) for name in names)


def isolate(names):
    with doc.lock():
        frozen = layers.isolate(names)
    toast.success('Isolated %s' % counted(names), '%s frozen' % counted(frozen))


def freeze_thaw(names):
    with doc.lock():
        if all_set(names, 'is_frozen'):
            verb, changed = 'Thawed', layers.thaw(names)
        else:
            verb, changed = 'Froze', layers.freeze(names)
    detail = None
    if verb == 'Froze' and view['current'].lower() in [n.lower() for n in names]:
        detail = 'The current layer cannot be frozen, so it stays thawed'
    toast.success('%s %s' % (verb, counted(changed)), detail)


def on_off(names):
    with doc.lock():
        if all_set(names, 'is_off'):
            verb, changed = 'Turned on', layers.turn_on(names)
        else:
            verb, changed = 'Turned off', layers.turn_off(names)
    toast.success('%s %s' % (verb, counted(changed)))


def lock_unlock(names):
    with doc.lock():
        if all_set(names, 'is_locked'):
            verb, changed = 'Unlocked', layers.unlock(names)
        else:
            verb, changed = 'Locked', layers.lock(names)
    toast.success('%s %s' % (verb, counted(changed)))


def set_current(names):
    if len(names) != 1:
        toast.info('Select just one layer to make current')
        return
    with doc.lock():
        layers.set_current(names[0])
    toast.success('%s is now the current layer' % names[0])


def merge(names):
    target = forms.ask_string(
        'Merge %s into which layer? Everything on them moves there, then they are deleted.'
        % counted(names), '', 'Merge layers')
    target = (target or '').strip()
    if not target:
        return
    sources = [n for n in names if n.lower() != target.lower()]
    if not sources:
        toast.info('Nothing to merge', 'The only layer selected is the target')
        return
    with doc.lock():
        exists = layers.exists(target)
    if not exists and not forms.confirm(
            'There is no layer named %s. Create it and merge into it?' % target, 'Merge layers'):
        return
    with doc.lock():
        if not exists:
            layers.create(target)
        moved = layers.merge(sources, target)
    toast.success('Merged %s into %s' % (counted(sources), target),
                  '%d object%s moved' % (moved, '' if moved == 1 else 's'))


def delete(names):
    if not forms.confirm('Delete %s?\n\nThe current layer, layer 0, Defpoints, xref layers '
                         'and layers still in use are kept.' % counted(names), 'Delete layers'):
        return
    deleted, kept = 0, []
    with doc.lock():
        for name in names:
            try:
                layers.delete(name)
                deleted += 1
            except ValueError as error:
                kept.append(str(error))
    for reason in kept:
        log.info('kept: %s' % reason)
    if not kept:
        toast.success('Deleted %s' % counted(deleted))
    elif deleted:
        toast.warning('Deleted %s, kept %d' % (counted(deleted), len(kept)), kept[0])
    else:
        toast.warning('No layers deleted', kept[0])


def on_double_click(sender, args):
    """Double-clicking a row (not the header) makes that layer current."""
    if ItemsControl.ContainerFromElement(layer_list, args.OriginalSource) is None:
        return
    action('Set current', set_current)(sender, args)


# ---- build --------------------------------------------------------------------

def apply_theme():
    """Paper and ink from the host's light or dark theme. The column headers keep
    the default light chrome, so their text stays dark in both."""
    tokens = DesignSystem.Tokens.Current
    surface = DesignSystem.Brush(tokens.Surface)
    ink = DesignSystem.Brush(tokens.Ink)
    line = DesignSystem.Brush(tokens.Line)
    root.Background = DesignSystem.Brush(tokens.Paper)
    root.SetValue(TextElement.ForegroundProperty, ink)
    for box in (filter_box, layer_list):
        box.Background = surface
        box.Foreground = ink
        box.BorderBrush = line
    filter_box.CaretBrush = ink
    header = Style(clr.GetClrType(GridViewColumnHeader))
    header.Setters.Add(Setter(Control.ForegroundProperty,
                              DesignSystem.Brush(DesignSystem.Tokens.For(False).Ink)))
    layer_list.View.ColumnHeaderContainerStyle = header


def quietly(handler):
    """A handler that logs what it raises: an exception escaping a WPF event
    inside AutoCAD is far worse than a list that did not update."""
    def handle(sender, args):
        try:
            handler(sender, args)
        except Exception as error:
            log.error('Layer Manager: %s' % error)
    return handle


try:
    apply_theme()
except Exception as error:                      # cosmetic: the palette still works
    log.warning('could not theme the Layer Manager: %s' % error)

pane.Find('Refresh').Click += quietly(lambda sender, args: refresh())
filter_box.TextChanged += quietly(lambda sender, args: show())
layer_list.SelectionChanged += quietly(on_selection)
layer_list.MouseDoubleClick += quietly(on_double_click)
root.MouseEnter += quietly(lambda sender, args: refresh(quiet=True))
root.IsVisibleChanged += quietly(
    lambda sender, args: refresh(quiet=True) if root.IsVisible else None)

for name, label, work in (('Isolate', 'Isolate', isolate),
                          ('FreezeThaw', 'Freeze/Thaw', freeze_thaw),
                          ('OnOff', 'On/Off', on_off),
                          ('LockUnlock', 'Lock/Unlock', lock_unlock),
                          ('SetCurrent', 'Set current', set_current),
                          ('Merge', 'Merge', merge),
                          ('Delete', 'Delete', delete)):
    buttons[name].Click += quietly(action(label, work))

refresh()
pane.xaml The dock panel's content. View on GitHub
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      x:Name="Root">
  <!-- Plain WPF: script.py fills the list, wires every handler and applies the
       host's light or dark colours. Rows are string lists, so the columns bind
       by index: [0] name, [1] current mark, [2] on, [3] frozen, [4] locked, [5] color. -->
  <Grid Margin="8">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True">
      <TextBlock DockPanel.Dock="Left" Text="Filter" VerticalAlignment="Center" Margin="0,0,6,0" />
      <Button x:Name="Refresh" DockPanel.Dock="Right" Content="Refresh" Padding="8,2"
              Margin="6,0,0,0" ToolTip="Read the layers of the active drawing again" />
      <TextBox x:Name="Filter" VerticalContentAlignment="Center" Padding="2"
               ToolTip="Part of a name, or a pattern such as A-* ; separate several with commas" />
    </DockPanel>

    <ListView x:Name="LayerList" Grid.Row="1" SelectionMode="Extended" MinHeight="120"
              ToolTip="Ctrl or Shift + click picks several layers; double-click makes one current">
      <ListView.View>
        <GridView>
          <GridViewColumn Header="" Width="22" DisplayMemberBinding="{Binding [1]}" />
          <GridViewColumn Header="Name" Width="160" DisplayMemberBinding="{Binding [0]}" />
          <GridViewColumn Header="On" Width="40" DisplayMemberBinding="{Binding [2]}" />
          <GridViewColumn Header="Frozen" Width="52" DisplayMemberBinding="{Binding [3]}" />
          <GridViewColumn Header="Locked" Width="52" DisplayMemberBinding="{Binding [4]}" />
          <GridViewColumn Header="Color" Width="46" DisplayMemberBinding="{Binding [5]}" />
        </GridView>
      </ListView.View>
    </ListView>

    <WrapPanel Grid.Row="2" Margin="0,8,0,0">
      <WrapPanel.Resources>
        <Style TargetType="Button">
          <Setter Property="Margin" Value="0,0,6,6" />
          <Setter Property="Padding" Value="8,3" />
        </Style>
      </WrapPanel.Resources>
      <Button x:Name="Isolate" Content="Isolate"
              ToolTip="Freeze every layer except the selected ones" />
      <Button x:Name="FreezeThaw" Content="Freeze/Thaw"
              ToolTip="Freeze the selected layers, or thaw them when all are frozen" />
      <Button x:Name="OnOff" Content="On/Off"
              ToolTip="Turn the selected layers off, or on when all are off" />
      <Button x:Name="LockUnlock" Content="Lock/Unlock"
              ToolTip="Lock the selected layers, or unlock them when all are locked" />
      <Button x:Name="SetCurrent" Content="Set current"
              ToolTip="Make the selected layer the current one" />
      <Button x:Name="Merge" Content="Merge into..."
              ToolTip="Move everything on the selected layers onto another layer, then delete them" />
      <Button x:Name="Delete" Content="Delete"
              ToolTip="Delete the selected layers; ones still in use are kept" />
    </WrapPanel>

    <TextBlock x:Name="Status" Grid.Row="3" Margin="0,2,0,0" TextWrapping="Wrap" Opacity="0.75" />
  </Grid>
</Grid>
bundle.yaml The bundle contract: the keys pyACAD reads for this tool. View on GitHub
title: Layer Manager
tooltip: A docked list of the drawing's layers with a filter box. Pick layers to isolate, freeze, turn off, lock, make current, merge or delete them without opening the Layer Properties Manager.
keytip: LM