Also, a tag locator tool into the tag browser to locate a tag in the tag browser from a tag path (I've also wanted this one for a long time as well):
I'll post this one as it's a nice QOL feature. Run below in the Script Console:
dev.tools._standalone.tag_browser_locator.install()
# dev.tools._standalone.tag_browser_locator
"""
Injects a small search row into the Designer's Tag Browser panel that
lets you jump to any tag by path. Keeps the last 10 unique searches --
click the field to pick one from the dropdown, or type a new path.
Usage in the search field:
[Provider]Path/To/Tag
Path/To/Tag
Shortcut:
Ctrl+Shift+L focuses the search field from anywhere in the
Tag Browser window.
"""
VERSION = "0.3.0"
import traceback
from javax.swing import (JPanel, JComboBox, DefaultComboBoxModel, JButton,
BoxLayout, BorderFactory, SwingUtilities, Timer,
JComponent, KeyStroke, AbstractAction)
from javax.swing.tree import TreePath
from java.awt import (BorderLayout, Container, Window, Insets, Font,
Dimension)
from java.awt.event import ActionListener, MouseAdapter, KeyEvent, InputEvent
MARKER = "__tagPathSearchRow__"
MARKER_WRAP = "__tagPathSearchWrap__"
MAX_HISTORY = 10
_SEARCH_HISTORY = [] # module-level; survives install/uninstall cycles
# ---------- history ----------
def _add_to_history(text):
text = (text or "").strip()
if not text:
return
# de-dupe (case-insensitive) and move-to-front
lower = text.lower()
for existing in list(_SEARCH_HISTORY):
if existing.lower() == lower:
_SEARCH_HISTORY.remove(existing)
_SEARCH_HISTORY.insert(0, text)
while len(_SEARCH_HISTORY) > MAX_HISTORY:
_SEARCH_HISTORY.pop()
def _build_history_model():
m = DefaultComboBoxModel()
for item in _SEARCH_HISTORY:
m.addElement(item)
return m
# ---------- component discovery ----------
def _find_tag_browser():
for w in Window.getWindows():
hit = _find_by_class(w, "TagBrowserFrame")
if hit is not None:
return hit
return None
def _find_by_class(root, cls_name):
stack = [root]
while stack:
c = stack.pop()
if c.__class__.__name__ == cls_name:
return c
if isinstance(c, Container):
for ch in c.getComponents():
stack.append(ch)
return None
def _find_all_by_class(root, cls_name):
out, stack = [], [root]
while stack:
c = stack.pop()
if c.__class__.__name__ == cls_name:
out.append(c)
if isinstance(c, Container):
for ch in c.getComponents():
stack.append(ch)
return out
# ---------- path parsing / tree walking ----------
def _parse_path(text):
text = (text or "").strip()
provider = None
if text.startswith("["):
end = text.find("]")
if end > 0:
provider = text[1:end]
text = text[end+1:]
if text.startswith("/"):
text = text[1:]
return provider, [p for p in text.split("/") if p]
def _node_name(node):
for attr in ("getName", "getDisplayName"):
if hasattr(node, attr):
try:
v = getattr(node, attr)()
if v is not None:
return unicode(v)
except:
pass
return unicode(node)
def _try_expand(tree, parts):
model = tree.getModel()
root = model.getRoot()
if root is None:
return False
path, current = TreePath(root), root
for part in parts:
tree.expandPath(path) # expand parent so lazy children load
match = None
for i in range(model.getChildCount(current)):
ch = model.getChild(current, i)
nm = _node_name(ch)
if nm == part or nm.lower() == part.lower():
match = ch
break
if match is None:
return False
path = path.pathByAddingChild(match)
current = match
# NOTE: intentionally do NOT expandPath(path) here -- we want the
# target node selected/visible but left collapsed.
tree.setSelectionPath(path)
tree.scrollPathToVisible(path)
tree.requestFocusInWindow()
return True
def _expand_with_retry(tree, parts, attempts=25, delay_ms=150):
state = {"n": 0}
def tick(evt):
state["n"] += 1
if _try_expand(tree, parts):
evt.getSource().stop()
elif state["n"] >= attempts:
evt.getSource().stop()
print ("Tag search: could not locate '%s'"
% "/".join(parts))
t = Timer(delay_ms, tick)
t.setInitialDelay(50)
t.start()
# ---------- action ----------
def _do_search(text, provider_combo, get_tree):
provider, parts = _parse_path(text)
if provider and provider_combo is not None:
idx = -1
for i in range(provider_combo.getItemCount()):
if unicode(provider_combo.getItemAt(i)).lower() == provider.lower():
idx = i
break
if idx < 0:
print "Tag search: provider '%s' not found" % provider
return
if provider_combo.getSelectedIndex() != idx:
provider_combo.setSelectedIndex(idx)
if parts:
def go():
tree = get_tree()
if tree is not None:
_expand_with_retry(tree, parts)
SwingUtilities.invokeLater(go)
# ---------- install / uninstall ----------
def install():
try:
tb = _find_tag_browser()
if tb is None:
print "TagBrowserFrame not found"
return False
toolbar = _find_by_class(tb, "TagToolbar")
tabbed = _find_by_class(tb, "TagTabbedPanel")
if toolbar is None or tabbed is None:
print "Toolbar/tabbed panel not found"
return False
parent = tabbed.getParent()
for c in list(parent.getComponents()):
if c.getName() == MARKER_WRAP:
for ch in list(c.getComponents()):
c.remove(ch)
parent.remove(c)
if toolbar.getParent() is not parent:
parent.add(toolbar, BorderLayout.NORTH)
# ---- Build search row ----
row = JPanel(BorderLayout(4, 0))
row.setName(MARKER)
row.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4))
row.setMinimumSize(Dimension(40, 0))
# Editable combo box: text field + history dropdown
combo = JComboBox(_build_history_model())
combo.setEditable(True)
combo.setSelectedItem("")
combo.setToolTipText(
"[Provider]Path/To/Tag or Path/To/Tag "
"(click to pick recent) \u2014 Ctrl+Shift+L to focus")
combo.setPreferredSize(Dimension(240, 28))
# Don't let long history entries dictate the combo's min width --
# otherwise the Tag Browser panel can't be shrunk narrower than
# the widest recent search.
combo.setMinimumSize(Dimension(40, 28))
btn = JButton(u"\u2315")
f = btn.getFont()
btn.setFont(f.deriveFont(Font.PLAIN, f.getSize2D() + 10.0))
btn.setToolTipText("Find tag in Tag Browser")
btn.setFocusable(False)
btn.setMargin(Insets(0, 0, 0, 0))
btn.setBorder(BorderFactory.createEmptyBorder(1, 6, 1, 6))
btn.setPreferredSize(Dimension(28, 28))
provider_combo = _find_by_class(tb, "TagToolbar$ProviderDropdown")
def get_tree():
jtp = _find_by_class(tabbed, "JTabbedPane")
target = jtp.getSelectedComponent() if jtp else tabbed
trees = _find_all_by_class(target, "TagFrameTree")
return trees[0] if trees else None
# Guard so programmatic model/selection changes don't re-fire search
state = {"suppress": False}
def current_text():
return unicode(combo.getEditor().getItem() or "").strip()
def refresh_model(keep_text):
state["suppress"] = True
try:
combo.setModel(_build_history_model())
combo.setMinimumSize(Dimension(40, 22))
combo.setPreferredSize(Dimension(240, 22))
combo.getEditor().setItem(keep_text)
finally:
state["suppress"] = False
def run_search():
text = current_text()
if not text:
return
_add_to_history(text)
refresh_model(text)
_do_search(text, provider_combo, get_tree)
class H(ActionListener):
def actionPerformed(self, e):
if state["suppress"]:
return
run_search()
h = H()
btn.addActionListener(h)
combo.addActionListener(h)
# Clicking the editor pops the history list open
editor_comp = combo.getEditor().getEditorComponent()
class PopupOnClick(MouseAdapter):
def mousePressed(self, e):
if (combo.getItemCount() > 0
and not combo.isPopupVisible()):
combo.showPopup()
editor_comp.addMouseListener(PopupOnClick())
# ---- Ctrl+Shift+L: focus the search field from anywhere in
# the Tag Browser window ----
class FocusSearchAction(AbstractAction):
def actionPerformed(self, e):
def do_focus():
combo.requestFocusInWindow()
try:
editor_comp.selectAll()
except:
pass
SwingUtilities.invokeLater(do_focus)
focus_ks = KeyStroke.getKeyStroke(
KeyEvent.VK_L,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)
row.registerKeyboardAction(
FocusSearchAction(), focus_ks,
JComponent.WHEN_IN_FOCUSED_WINDOW)
row.add(combo, BorderLayout.CENTER)
row.add(btn, BorderLayout.EAST)
# ---- Wrap toolbar + row ----
layout = parent.getLayout()
constraint = (layout.getConstraints(toolbar)
if isinstance(layout, BorderLayout) else None)
wrap = JPanel()
wrap.setName(MARKER_WRAP)
wrap.setLayout(BoxLayout(wrap, BoxLayout.Y_AXIS))
parent.remove(toolbar)
wrap.add(toolbar)
wrap.add(row)
parent.add(wrap,
constraint if constraint is not None
else BorderLayout.NORTH)
parent.revalidate()
parent.repaint()
print "Tag path search row installed."
return True
except:
traceback.print_exc()
return False
def uninstall():
try:
tb = _find_tag_browser()
if tb is None:
return False
tabbed = _find_by_class(tb, "TagTabbedPanel")
if tabbed is None:
return False
parent = tabbed.getParent()
if parent is None:
return False
removed = False
for c in list(parent.getComponents()):
if c.getName() == MARKER_WRAP:
toolbar = None
for ch in list(c.getComponents()):
if ch.__class__.__name__ == "TagToolbar":
toolbar = ch
c.remove(ch)
parent.remove(c)
if toolbar is not None:
parent.add(toolbar, BorderLayout.NORTH)
removed = True
if removed:
parent.revalidate()
parent.repaint()
print "Tag path search row removed."
return removed
except:
traceback.print_exc()
return False
def clear_history():
"""Wipe the recent-searches list."""
del _SEARCH_HISTORY[:]