Designer quality of life "mods"

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[:]