I have done a little digging onto how keynote tags display the number when the Revit setting are set to By Sheet for the numbering method. What I have found so far in testing is that the numbering is all based on the first instance of the Key Value that was placed. I have found that the only way (my current hack) to renumbering the keynote displayed value is to Cut keynote tags and Paste them back.
Example:
Keynote 1
Keynote 2
Keynote 3
If i want Keynote 2 and 3 to swap, I first collect all the Keynote 2’s and Cut them. This moves the current Keynote 3 to be the new 2. Then I paste back the previously cut Keynote 2 and they are now the new Keynote 3.
It works.
My testing so far has been a sheet with a single view.
I came up with this so far:
I am hoping this has potential and others in the community could do a better job than me at taking this further to a more “Move Up/Down Reorder” type tool.
# -*- coding: utf-8 -*-
"""
NXGN | Select Keynote Tags From List
Purpose:
- Build visible By Sheet keynote numbering from Keynote Tag Element IDs.
- Show the user a checkbox list of keynote display numbers.
- User selects one or more keynote numbers.
- Tool selects all matching Keynote Tags.
Logic:
- Each Key Value group has one or more Keynote Tag Element IDs.
- The lowest Element ID in each Key Value group is treated as the first placed tag.
- Groups are sorted by their lowest Element ID.
- That sorted order becomes the visible By Sheet numbering order.
Revit / pyRevit
IronPython 2.7
No f-strings
"""
import clr
clr.AddReference("RevitAPI")
clr.AddReference("RevitAPIUI")
from Autodesk.Revit.DB import (
BuiltInCategory,
BuiltInParameter,
FilteredElementCollector,
ElementId,
ViewSheet,
Viewport
)
from pyrevit import revit, forms, script
from System.Collections.Generic import List
# ------------------------------------------------------------
# Setup
# ------------------------------------------------------------
doc = revit.doc
uidoc = revit.uidoc
view = doc.ActiveView
# ------------------------------------------------------------
# Helpers
# ------------------------------------------------------------
def safe_str(value):
try:
if value is None:
return ""
return str(value)
except:
return ""
def clean_value(value):
return safe_str(value).strip()
def clean_key(value):
return clean_value(value).lower()
def values_match(a, b):
return clean_key(a) == clean_key(b)
def add_unique(values, value):
value = clean_value(value)
if not value:
return
existing = []
for v in values:
existing.append(clean_key(v))
if clean_key(value) not in existing:
values.append(value)
def add_unique_element_id(values, element_id):
if element_id is None:
return
try:
new_int = element_id.IntegerValue
except:
return
for existing_id in values:
try:
if existing_id.IntegerValue == new_int:
return
except:
pass
values.append(element_id)
def get_element_name(elem):
if elem is None:
return ""
try:
name = elem.Name
if name:
return safe_str(name)
except:
pass
try:
p = elem.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM)
if p:
value = p.AsString()
if value:
return safe_str(value)
except:
pass
try:
p = elem.get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_NAME)
if p:
value = p.AsString()
if value:
return safe_str(value)
except:
pass
return ""
def get_param_value(param):
if param is None:
return ""
try:
storage_type = safe_str(param.StorageType)
if storage_type == "String":
value = param.AsString()
if value:
return clean_value(value)
return ""
if storage_type == "Integer":
return clean_value(param.AsInteger())
if storage_type == "Double":
value = param.AsValueString()
if value:
return clean_value(value)
return clean_value(param.AsDouble())
if storage_type == "ElementId":
eid = param.AsElementId()
if eid and eid != ElementId.InvalidElementId:
elem = doc.GetElement(eid)
if elem:
elem_name = get_element_name(elem)
if elem_name:
return "{0} ({1})".format(elem_name, eid.IntegerValue)
return clean_value(eid.IntegerValue)
return ""
except:
pass
try:
value = param.AsValueString()
if value:
return clean_value(value)
except:
pass
try:
value = param.AsString()
if value:
return clean_value(value)
except:
pass
return ""
def get_all_named_parameter_values(elem, param_name):
"""
Important:
Revit can have duplicate parameter names.
LookupParameter can return the blank one.
This loops all parameters and gets every non-empty value with that name.
"""
values = []
if elem is None:
return values
try:
params = elem.Parameters
except:
params = []
for p in params:
try:
pname = p.Definition.Name
except:
pname = ""
if pname == param_name:
add_unique(values, get_param_value(p))
return values
def get_key_values_from_tag(tag):
"""
Reads stored Key Value from a Keynote Tag.
Handles duplicate 'Key Value' parameters.
"""
values = []
if tag is None:
return values
for v in get_all_named_parameter_values(tag, "Key Value"):
add_unique(values, v)
try:
p = tag.get_Parameter(BuiltInParameter.KEYNOTE_PARAM)
if p:
add_unique(values, get_param_value(p))
except:
pass
return values
def get_primary_key_value_from_tag(tag):
values = get_key_values_from_tag(tag)
if values:
return values[0]
return ""
def get_active_sheet_for_view(active_view):
"""
If active view is a sheet:
return that sheet.
If active view is a model/detail/drafting view:
find the sheet where that view is placed.
"""
if isinstance(active_view, ViewSheet):
return active_view
try:
viewports = (
FilteredElementCollector(doc)
.OfClass(Viewport)
.ToElements()
)
for vp in viewports:
try:
if vp.ViewId == active_view.Id:
sheet = doc.GetElement(vp.SheetId)
if sheet:
return sheet
except:
pass
except:
pass
return None
def get_views_on_sheet(sheet):
views = []
if sheet is None:
return views
try:
viewports = (
FilteredElementCollector(doc)
.OfClass(Viewport)
.ToElements()
)
for vp in viewports:
try:
if vp.SheetId == sheet.Id:
v = doc.GetElement(vp.ViewId)
if v:
views.append(v)
except:
pass
except:
pass
return views
def collect_keynote_tags_from_view(target_view):
if target_view is None:
return []
try:
return list(
FilteredElementCollector(doc, target_view.Id)
.OfCategory(BuiltInCategory.OST_KeynoteTags)
.WhereElementIsNotElementType()
.ToElements()
)
except:
return []
def add_unique_tag(tags, tag):
if tag is None:
return
try:
new_int = tag.Id.IntegerValue
except:
return
for existing in tags:
try:
if existing.Id.IntegerValue == new_int:
return
except:
pass
tags.append(tag)
def collect_sheet_keynote_tags(sheet):
"""
Collects keynote tags from all views placed on the sheet,
plus keynote tags directly on the sheet.
"""
tags = []
if sheet is None:
return tags
placed_views = get_views_on_sheet(sheet)
for v in placed_views:
view_tags = collect_keynote_tags_from_view(v)
for tag in view_tags:
add_unique_tag(tags, tag)
sheet_tags = collect_keynote_tags_from_view(sheet)
for tag in sheet_tags:
add_unique_tag(tags, tag)
return tags
def collect_selection_target_tags(active_view, sheet):
"""
If user runs from a plan/detail view:
select tags in the active view only.
If user runs from a sheet:
select tags from all views placed on the sheet,
plus any tags directly on the sheet.
"""
if isinstance(active_view, ViewSheet):
return collect_sheet_keynote_tags(sheet)
return collect_keynote_tags_from_view(active_view)
def collect_numbering_source_tags(active_view, sheet):
"""
Numbering Method = By Sheet is sheet-based.
If the active view is placed on a sheet:
use all keynote tags on that sheet to build the display numbering order.
If no sheet is found:
fall back to active view tags.
"""
if sheet is not None:
sheet_tags = collect_sheet_keynote_tags(sheet)
if sheet_tags:
return sheet_tags
return collect_selection_target_tags(active_view, sheet)
def build_key_value_groups_from_tags(tags):
groups = {}
for tag in tags:
key_value = get_primary_key_value_from_tag(tag)
if not key_value:
continue
group_key = clean_key(key_value)
if group_key not in groups:
groups[group_key] = {
"key_value": key_value,
"tag_ids": [],
"min_id": None,
"count": 0
}
try:
tag_int_id = tag.Id.IntegerValue
except:
tag_int_id = None
add_unique_element_id(groups[group_key]["tag_ids"], tag.Id)
groups[group_key]["count"] = len(groups[group_key]["tag_ids"])
if tag_int_id is not None:
if groups[group_key]["min_id"] is None:
groups[group_key]["min_id"] = tag_int_id
else:
if tag_int_id < groups[group_key]["min_id"]:
groups[group_key]["min_id"] = tag_int_id
return groups
def build_display_number_items(groups):
"""
Sort groups by their lowest tag Element Id.
Assign display numbers starting at 1.
"""
group_list = []
for group_key in groups:
item = groups[group_key]
if item["min_id"] is None:
continue
group_list.append(item)
group_list = sorted(group_list, key=lambda x: x["min_id"])
display_items = []
display_number = 1
for item in group_list:
display_items.append({
"display_number": display_number,
"display_text": safe_str(display_number),
"key_value": item["key_value"],
"tag_ids": item["tag_ids"],
"min_id": item["min_id"],
"count": item["count"]
})
display_number += 1
return display_items
def get_tag_ids_for_key_value(tags, key_value):
ids = []
for tag in tags:
tag_key_value = get_primary_key_value_from_tag(tag)
if values_match(tag_key_value, key_value):
add_unique_element_id(ids, tag.Id)
return ids
def select_element_ids(element_ids):
id_list = List[ElementId]()
for eid in element_ids:
id_list.Add(eid)
uidoc.Selection.SetElementIds(id_list)
def format_option_label(item, target_count):
return "{0} | {1} | {2} tag(s)".format(
item["display_text"],
item["key_value"],
target_count
)
# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
sheet = get_active_sheet_for_view(view)
numbering_tags = collect_numbering_source_tags(view, sheet)
selection_tags = collect_selection_target_tags(view, sheet)
if not numbering_tags:
script.exit()
if not selection_tags:
script.exit()
groups = build_key_value_groups_from_tags(numbering_tags)
display_items = build_display_number_items(groups)
if not display_items:
script.exit()
# ------------------------------------------------------------
# Build SelectFromList options
# ------------------------------------------------------------
label_to_item = {}
option_labels = []
for item in display_items:
target_ids = get_tag_ids_for_key_value(selection_tags, item["key_value"])
target_count = len(target_ids)
if target_count < 1:
continue
label = format_option_label(item, target_count)
option_labels.append(label)
label_to_item[label] = item
if not option_labels:
script.exit()
selected_labels = forms.SelectFromList.show(
option_labels,
title="NXGN | Select Keynote Tags",
button_name="Select Keynote Tags",
multiselect=True
)
if not selected_labels:
script.exit()
# ------------------------------------------------------------
# Select matching keynote tags
# ------------------------------------------------------------
selected_ids = []
for label in selected_labels:
item = label_to_item.get(label)
if not item:
continue
target_ids = get_tag_ids_for_key_value(selection_tags, item["key_value"])
for eid in target_ids:
add_unique_element_id(selected_ids, eid)
if not selected_ids:
script.exit()
select_element_ids(selected_ids)