# --- UCSF Chimera Copyright ---
# Copyright (c) 2000 Regents of the University of California.
# All rights reserved.  This software provided pursuant to a
# license agreement containing restrictions on its disclosure,
# duplication and use.  This notice must be embedded in or
# attached to all copies, including partial copies, of the
# software or any revisions or derivations thereof.
# --- UCSF Chimera Copyright ---

import os
import math
import Tkinter

import chimera
from chimera import selection
from chimera import chimage
from chimera import tkgui
from chimera import match
from chimera import preferences
from chimera.baseDialog import ModelessDialog
import Midas

prefs = preferences.addCategory("EnsembleCluster", preferences.HiddenCategory)

class EnsembleMatch(ModelessDialog):
	"""Display a table for manipulating one ensemble relative to another.

	The two ensembles should consist of conformers of the same model.
	Table rows correspond to conformers in the reference ensemble.
	Table columns correspond to conformers in the alternative ensemble.
	A conformer in the alternative ensemble will be matched onto
	a conformer in the reference ensemble when a table entry is
	clicked.  The table entry label is the RMSD between the two
	conformers."""

	title = "Ensemble Match"
	help = "ContributedSoftware/ensemblematch/ensemblematch.html"
	buttons = ("Save...", "Hide", "Quit")

	def __init__(self, refModels, altModels, subSelection, *args, **kw):
		#
		# Save the subselection (part of model to match)
		# as well as the model lists (ordered by subid)
		#
		self.subSelection = subSelection
		l = [ (m.subid, m) for m in refModels ]
		l.sort()
		self.refModels = [ t[1] for t in l ]
		l = [ (m.subid, m) for m in  altModels ]
		l.sort()
		self.altModels = [ t[1] for t in l ]

		#
		# Grab the atomic coordinates that will be used
		# to compute RMSDs and check for errors
		#
		self.atomDict = {}
		counts = []
		for ml in self.refModels, self.altModels:
			for m in ml:
				osl = '%s%s' % (m.oslIdent(), self.subSelection)
				try:
					s = selection.OSLSelection(osl)
				except SyntaxError:
					from chimera import UserError
					raise UserError("Atom specifier for ensemble match is mangled")
				atoms = s.atoms()
				self.atomDict[m] = match._coordArray(atoms)
				counts.append(len(atoms))
		if min(counts) != max(counts):
			raise chimera.UserError("Unequal number of atoms "
						"found in models")
		if counts[0] == 0:
			raise chimera.UserError("No atoms match atom specifier")

		#
		# Add handler to monitor change in display or active
		# status changes
		#
		self.trigger = chimera.triggers.addHandler('check for changes',
							self._statusCheck, None)

		#
		# No save dialog yet
		#
		self.saveDialog = None

		#
		# Initialize dialog
		#
		ModelessDialog.__init__(self, *args, **kw)

		# 
		# Register with extension manager
		#
		chimera.extension.manager.registerInstance(self)

	def fillInUI(self, parent):
		#
		# Construct user interface
		#
		self.modelVars = {}

		# Find images
		top = parent.winfo_toplevel()
		dir = os.path.dirname(__file__)
		icon = os.path.join(dir, 'move.tiff')
		self.moveIcon = chimage.get(icon, top)
		icon = os.path.join(dir, 'zoom.tiff')
		self.zoomIcon = chimage.get(icon, top)
		icon = os.path.join(dir, 'reset.tiff')
		self.resetIcon = chimage.get(icon, top)

		# Label rows
		for n in range(len(self.refModels)):
			parent.rowconfigure(n + 1, weight=1)
			m = self.refModels[n]
			lf = Tkinter.Frame(parent, relief=Tkinter.GROOVE, bd=2)
			lf.grid(row=n + 1, column=0, sticky='nsew')
			self._addRefConformer(lf, m)

		# Label columns
		for n in range(len(self.altModels)):
			parent.columnconfigure(n + 1, weight=1)
			m = self.altModels[n]
			lf = Tkinter.Frame(parent, relief=Tkinter.GROOVE, bd=2)
			lf.grid(row=0, column=n + 1, sticky='nsew')
			self._addAltConformer(lf, m)

		# Add table entries
		# Each column comprises a set of radio buttons.
		# This design is selected because each conformer
		# in the alternative ensemble can only be matched
		# to a single conformer in the reference ensemble.
		self.matchVars = {}
		self.matchRMSD = {}
		for a in range(len(self.altModels)):
			alt = self.altModels[a]
			var = Tkinter.IntVar(parent)
			var.set(-1)
			self.matchVars[alt] = var
			for r in range(len(self.refModels)):
				ref = self.refModels[r]
				try:
					rmsd = self.matchRMSD[(ref, alt)]
				except KeyError:
					m = chimera.match.Match(
							self.atomDict[ref],
							self.atomDict[alt])
					rmsd = m.rms
				self.matchRMSD[(alt, ref)] = rmsd
				b = Tkinter.Radiobutton(parent,
						relief=Tkinter.SUNKEN,
						text='%.3f' % rmsd,
						value=r,
						variable=var,
						command=lambda s=self, m=alt:
							s.matchModel(m))
				b.grid(column=a + 1, row=r + 1, sticky='nsew')

	def _addRefConformer(self, frame, model):
		#
		# Add reference conformer label and buttons
		# A is for active
		# D is for display
		# 
		frame.columnconfigure(0, weight=1)
		frame.columnconfigure(1, weight=1)
		frame.columnconfigure(2, weight=1)
		l = Tkinter.Label(frame, text=model.oslIdent())
		l.grid(row=0, column=0, sticky='nsew')
		va = Tkinter.IntVar(frame)
		va.set(model.openState.active and 1 or 0)
		ba = Tkinter.Checkbutton(frame,
					text='A',
					indicatoron=Tkinter.FALSE,
					variable=va,
					command=lambda v=va, s=self, m=model:
						s.activateModel(m, v.get()))
		ba.grid(row=0, column=1, sticky='nsew')
		vd = Tkinter.IntVar(frame)
		vd.set(model.display and 1 or 0)
		bd = Tkinter.Checkbutton(frame,
					text='D',
					indicatoron=Tkinter.FALSE,
					variable=vd,
					command=lambda v=vd, s=self, m=model:
						s.displayModel(m, v.get()))
		bd.grid(row=0, column=2, sticky='nsew')
		self.modelVars[model] = (va, vd)
		br = Tkinter.Button(frame,
					image=self.moveIcon,
					command=lambda s=self, m=model:
						s.manipulateModel(m))
		br.grid(row=0, column=3, sticky='nsew')
		br = Tkinter.Button(frame,
					image=self.zoomIcon,
					command=lambda s=self, m=model:
						s.zoomModel(m))
		br.grid(row=0, column=4, sticky='nsew')

	def _addAltConformer(self, frame, model):
		#
		# Add alternative conformer label and buttons
		# A is for active
		# D is for display
		# R is for reset
		#
		frame.columnconfigure(0, weight=1)
		frame.columnconfigure(1, weight=1)
		frame.columnconfigure(2, weight=1)
		l = Tkinter.Label(frame, text=model.oslIdent())
		l.grid(row=0, column=0, columnspan=3, sticky='nsew')
		va = Tkinter.IntVar(frame)
		va.set(model.openState.active and 1 or 0)
		ba = Tkinter.Checkbutton(frame,
					text='A',
					indicatoron=Tkinter.FALSE,
					variable=va,
					command=lambda v=va, s=self, m=model:
						s.activateModel(m, v.get()))
		ba.grid(row=1, column=0, sticky='nsew')
		vd = Tkinter.IntVar(frame)
		vd.set(model.display and 1 or 0)
		bd = Tkinter.Checkbutton(frame,
					text='D',
					indicatoron=Tkinter.FALSE,
					variable=vd,
					command=lambda v=vd, s=self, m=model:
						s.displayModel(m, v.get()))
		bd.grid(row=1, column=1, sticky='nsew')
		self.modelVars[model] = (va, vd)
		br = Tkinter.Button(frame,
					relief=Tkinter.FLAT,
					image=self.resetIcon,
					command=lambda s=self, m=model:
						s.resetModel(m))
		br.grid(row=1, column=2, sticky='nsew')

	def activateModel(self, model, on):
		model.openState.active = on

	def displayModel(self, model, on):
		model.display = on

	def resetModel(self, model):
		self.matchVars[model].set(-1)
		va, vd = self.modelVars[model]
		va.set(0)
		vd.set(0)
		self.activateModel(model, 0)
		self.displayModel(model, 0)

	def matchModel(self, model):
		n = self.matchVars[model].get()
		va, vd = self.modelVars[model]
		if n < 0:
			va.set(0)
			vd.set(0)
			self.activateModel(model, 0)
			self.displayModel(model, 0)
			return
		altOSL = '%s%s' % (model.oslIdent(), self.subSelection)
		ref = self.refModels[n]
		refOSL = '%s%s' % (ref.oslIdent(), self.subSelection)
		try:
			Midas.match(altOSL, refOSL)
		except Midas.MidasError, e:
			raise chimera.UserError(str(e))
		va.set(1)
		vd.set(1)
		self.activateModel(model, 1)
		self.displayModel(model, 1)
		va, vd = self.modelVars[ref]
		va.set(1)
		vd.set(1)
		self.activateModel(ref, 1)
		self.displayModel(ref, 1)

	def manipulateModel(self, model):
		for m in self.refModels:
			state = m == model and 1 or 0
			va, vd = self.modelVars[m]
			va.set(state)
			self.activateModel(m, state)
		for m in self.altModels:
			if m == model:
				# In case we're matching the ensemble to itself
				continue
			n = self.matchVars[m].get()
			if n < 0:
				state = 0
			else:
				state = self.refModels[n] == model and 1 or 0
			va, vd = self.modelVars[m]
			va.set(state)
			self.activateModel(m, state)

	def zoomModel(self, model):
		self.manipulateModel(model)
		sel = selection.ItemizedSelection()
		for m in self.refModels:
			if m.openState.active:
				sel.add(m.atoms)
		for m in self.altModels:
			if m.openState.active:
				sel.add(m.atoms)
		Midas.window(sel)

	def _statusCheck(self, name, closure, triggerData):
		try:
			for ml in self.refModels, self.altModels:
				for m in ml:
					va, vd = self.modelVars[m]
					if va.get() != m.openState.active:
						va.set(m.openState.active)
					if vd.get() != m.display:
						vd.set(m.display)
		except:
			self.emQuit()

	def Save(self):
		if self.saveDialog is None:
			self.saveDialog = EnsembleMatchSaveDialog()
		files = self.saveDialog.run(self.uiMaster())
		if not files:
			return
		filename = files[0][0]
		inclHeaders = self.saveDialog.inclHeaders.get()
		f = open(filename, "w")
		if inclHeaders:
			l = [ m.oslIdent() for m in self.refModels ]
			l.insert(0, "")
			print >> f, '\t'.join(l)
		for alt in self.altModels:
			l = [ "%.3f" % self.matchRMSD[(alt, ref)]
					for ref in self.refModels ]
			if inclHeaders:
				l.insert(0, alt.oslIdent())
			print >> f, '\t'.join(l)
		f.close()
		print "RMSD matrix saved in", filename

	def Hide(self):
		self.emHide()

	def Quit(self):
		self.emQuit()

	#
	# Extension manager callback routines
	#
	def emName(self):
		return self.title

	def emRaise(self):
		self.enter()

	def emHide(self):
		self.Close()

	def emQuit(self):
		if self.trigger:
			chimera.triggers.deleteHandler('check for changes',
							self.trigger)
			self.trigger = None
		self.destroy()
		chimera.extension.manager.deregisterInstance(self)

from OpenSave import SaveModal
class EnsembleMatchSaveDialog(SaveModal):

	title = "Save RMSD Matrix"

	def __init__(self, **kw):
		SaveModal.__init__(self, clientPos="s", clientSticky="w", **kw)

	def fillInUI(self, master):
		SaveModal.fillInUI(self, master)
		self.inclHeaders = Tkinter.IntVar(master)
		self.inclHeaders.set(0)
		b = Tkinter.Checkbutton(
				self.clientArea, variable=self.inclHeaders,
				text="Include row/column headers")
		b.pack()

def tile(modelList, scale=1.1, columns = None, viewAll = True):
	"Arrange the models in the given list in regular rows and columns"

	if not modelList:
		return

	# Make table of openState objects (share same transform)
	osModels = {}
	for m in modelList:
		o = m.openState
		if o in osModels:
			osModels[o].append(m)
		else:
			osModels[o] = [m]
	sortList = [ (min([(m.id, m.subid) for m in mlist]), o)
		     for o,mlist in osModels.items() ]
	sortList.sort()
	oStates = [ t[1] for t in sortList ]

	#
	# Find the bounding sphere of each model.  We first display the
	# model so that the bounding boxes get computed
	# correctly, and reset the display status afterwards.
	#
	displayed = {}
	for m in modelList:
		displayed[m] = m.display
		m.display = True
	bSpheres = {}
	for o,mlist in osModels.items():
		for m in mlist:
			has_bounds, s = m.bsphere()
			if has_bounds:
				s.xform(o.xform)
				if o in bSpheres:
					bSpheres[o].merge(s)
				else:
					bSpheres[o] = s
	if not bSpheres:
		return
	oStates = [o for o in oStates if o in bSpheres]
	radius = max([s.radius for s in bSpheres.values()]) * scale
	for m, disp in displayed.iteritems():
		m.display = disp

	#
	# Find the number of rows and columns that best match the
	# viewer aspect ratio
	#
	nPos = len(bSpheres)
	if columns is None:
		rows, cols = rowAndColumnSize(nPos)
	else:
		cols = columns
		rows = (nPos + cols - 1) / cols

	#
	# Update transformation of models to place them in proper
	# row and column
	#
	diameter = radius * 2
	r = 0
	c = 0
	ref = bSpheres[oStates[0]].center
	xforms = {}
	for o in oStates:
		s = bSpheres[o]
		cx,cy,cz = s.center.data()
		dx = ref.x - cx + c * diameter
		dy = ref.y - cy - r * diameter
		dz = ref.z - cz
		xf = chimera.Xform.translation(dx, dy, dz)
		xforms[m] = xf
		o.globalXform(xf)
		c = c + 1
		if c == cols:
			r = r + 1
			c = 0

	#
	# Update viewing parameters so all models appear in viewer
	#
	if viewAll:
		chimera.viewer.viewAll(resetCofrMethod=False)

def rowAndColumnSize(nPos):

	vWidth, vHeight = chimera.viewer.windowSize
	vRatio = vWidth / float(vHeight)
	for rows in range(1, nPos + 1):
		cols = int((nPos + rows - 1) / rows)
		ratio = cols / float(rows)
		if ratio < vRatio:
			score = ratio / vRatio
		else:
			score = vRatio / ratio
		try:
			if score > bestScore:
				best = (rows, cols)
				bestScore = score
		except NameError:
			best = (rows, cols)
			bestScore = score
	return best

def bboxSize(m):
	for a in m.atoms:
		c = a.coord()
		try:
			if c.x < minX:
				minX = c.x
			elif c.x > maxX:
				maxX = c.x
			if c.y < minY:
				minY = c.y
			elif c.y > maxY:
				maxY = c.y
			if c.z < minZ:
				minZ = c.z
			elif c.z > maxZ:
				maxZ = c.z
		except NameError:
			minX = maxX = c.x
			minY = maxY = c.y
			minZ = maxZ = c.z
	return minX, maxX, minY, maxY, minZ, maxZ

def boundingSphere(m):
	"Compute the bounding sphere with center at average atomic coordinates"
	sumX = 0
	sumY = 0
	sumZ = 0
	atoms = m.atoms
	for a in atoms:
		c = a.coord()
		sumX = sumX + c.x
		sumY = sumY + c.y
		sumZ = sumZ + c.z
	center = chimera.Coord()
	center.x = sumX / len(atoms)
	center.y = sumY / len(atoms)
	center.z = sumZ / len(atoms)
	rsq = 0
	for a in atoms:
		nrsq = center.sqdistance(a.coord())
		if nrsq > rsq:
			rsq = nrsq
	return center.x, center.y, center.z, math.sqrt(rsq)

def cluster(modelList, subSelection):
	"Assign a cluster id to each model in the list"

	if not modelList:
		import chimera
		raise chimera.UserError("No ensemble selected")
	d = EnsembleCluster(modelList, subSelection)

class EnsembleCluster(ModelessDialog):
	"""Display a table for manipulating clusters."""

	title = "Ensemble Cluster"
	help = "ContributedSoftware/ensemblematch/ensemblecluster.html"
	buttons = ("Save", "Hide", "Quit")

	def __init__(self, modelList, subSelection, *args, **kw):
		from chimera import preferences
		self._removeHandler = None
		for m in modelList:
			m.preclusterColor = m.color
		self.subSelection = subSelection
		self._cluster(modelList)
		ModelessDialog.__init__(self, *args, **kw)
		chimera.extension.manager.registerInstance(self)
		self._removeHandler = chimera.openModels.addRemoveHandler(
						self._modelsChanged, None)

	def __del__(self):
		if self._removeHandler:
			chimera.openModels.deleteRemoveHandler(self._removeHandler)

	def _cluster(self, modelList):
		from chimera import replyobj
		from distmat import DistanceMatrix
		replyobj.status("Computing distance matrix")
		modelList = [ m for m in modelList if len(m.atoms) > 0 ]
		fulldm = DistanceMatrix(len(modelList))
		sameAs = {}
		atoms = [ match._coordArray(self._getAtoms(m))
				for m in modelList ]
		for i in range(len(modelList)):
			mi = modelList[i]
			if mi in sameAs:
				continue
			ai = atoms[i]
			for j in range(i + 1, len(modelList)):
				aj = atoms[j]
				if len(ai) != len(aj):
					mj = modelList[j]
					print ("%s (%d atoms) and "
						"%s (%d atoms) not matched" %
							(mi.oslIdent(),
								len(ai),
								mj.oslIdent(),
								len(aj)))
					fulldm.set(i, j, float("inf"))
					continue
				m = chimera.match.Match(ai, aj)
				if m.rms <= 0:
					mj = modelList[j]
					sameAs[mj] = mi
					print ("Zero RMSD between %s and %s" %
							(mi.oslIdent(), mj.oslIdent()))
				fulldm.set(i, j, m.rms)
		print "RMSD/Distance matrix"
		print fulldm
		if not sameAs:
			dm = fulldm
			models = modelList
		else:
			dm = DistanceMatrix(len(modelList) - len(sameAs))
			models = []
			indexMap = []
			for i, mi in enumerate(modelList):
				if mi in sameAs:
					continue
				models.append(mi)
				indexMap.append(i)
			for i in range(len(models)):
				im = indexMap[i]
				for j in range(i + 1, len(models)):
					jm = indexMap[j]
					dm.set(i, j, fulldm.get(im, jm))
		replyobj.status("Using %d of %d models for clustering" %
					(dm.size, fulldm.size))

		from nmrclust import NMRClust
		nmrc = NMRClust(dm)
		id = 0
		cList = []
		for c in nmrc.clusters:
			members = c.members()
			cList.append((len(members), c, members))
		cList.sort()
		cList.reverse()
		clusterInfo = []
		for i, (size, c, members) in enumerate(cList):
			mList = []
			for member in members:
				m = models[member]
				m.clusterId = id
				m.clusterRep = 0
				mList.append(m)
			rep = nmrc.representative(c)
			m = models[rep]
			m.clusterRep = size
			clusterInfo.append((m, mList))
			id += 1
		for mj, mi in sameAs.iteritems():
			mj.clusterId = mi.clusterId
			mj.clusterRep = 0
			clusterInfo[mj.clusterId][1].append(mj)
		replyobj.status("Generated %d clusters" % id)

		self.nmrc = nmrc
		self.modelList = modelList
		self.clusterInfo = clusterInfo

	def _getAtoms(self, m):
		if not self.subSelection:
			atoms = m.atoms
		else:
			osl = '%s%s' % (m.oslIdent(), self.subSelection)
			s = selection.OSLSelection(osl)
			wanted = s.vertices(asDict=True)
			atoms = [ a for a in m.atoms if a in wanted ]
		return atoms

	def _modelsChanged(self, triggerName, closure, mols):
		self.modelList = [ m for m in self.modelList if m not in mols ]
		for i, (rep, mList) in enumerate(self.clusterInfo):
			newList = [ m for m in mList if m not in mols ]
			if not newList:
				# All members of cluster deleted
				self.clusterInfo[i] = (None, [])
				continue
			if rep in mols:
				newRep = mList[0]
				newRep.clusterRep = rep.clusterRep
			else:
				newRep = rep
			if newRep is not rep or len(newList) != len(mList):
				self.clusterInfo[i] = (newRep, newList)

	def fillInUI(self, parent):
		import Pmw, Tkinter
		from chimera import chimage
		self.parent = parent

		d = prefs.get("treatment", {})
		self.treatmentShow = d.get("show", 0)
		self.treatmentSelAtom = d.get("selectAtoms", 0)
		self.treatmentSelAtomOf = d.get("selectAtomsOf", "all members")
		self.treatmentSelModel = d.get("selectModels", 1)
		self.treatmentSelModelOf = d.get("selectModelsOf", "all members")
		self.treatmentColorModel = d.get("colorModels", 1)
		self.treatmentColorModelOf = d.get("colorModelsOf", "all members")

		self.rightArrow = chimage.get("rightarrow.png", parent)
		self.downArrow = chimage.get("downarrow.png", parent)

		if self.treatmentShow:
			relief = "sunken"
			image = self.downArrow
		else:
			relief = "flat"
			image = self.rightArrow
		self.treatmentGroup = Pmw.Group(parent,
					collapsedsize=0,
					tagindent=0,
					ring_relief=relief,
					tag_pyclass=Tkinter.Button,
					tag_text=" Treatment of Chosen Clusters",
					tag_relief="flat",
					tag_compound="left",
					tag_image=image,
					tag_command=self._treatmentCB)
		if not self.treatmentShow:
			self.treatmentGroup.collapse()
		self.treatmentGroup.pack(side="top", fill="x", padx=3)

		self.treatmentSelAtomVar = Tkinter.IntVar(parent)
		if self.treatmentSelAtom:
			self.treatmentSelAtomVar.set(1)
		else:
			self.treatmentSelAtomVar.set(0)
		f = Tkinter.Frame(self.treatmentGroup.interior())
		f.pack(side="top", fill="x")
		self.treatmentSelAtomButton = Tkinter.Checkbutton(f,
					text=" Select atoms of",
					onvalue=1, offvalue=0,
					variable=self.treatmentSelAtomVar,
					command=self._treatmentSelAtomCB)
		self.treatmentSelAtomButton.pack(side="left")
		self.treatmentSelAtomMenu = Pmw.OptionMenu(f,
					items = ( "representatives",
							"all members" ),
					initialitem=self.treatmentSelAtomOf,
					menubutton_pady=0,
					command=self._treatmentSelAtomCB)
		self.treatmentSelAtomMenu.pack(side="left")

		self.treatmentColorModelVar = Tkinter.IntVar(parent)
		if self.treatmentColorModel:
			self.treatmentColorModelVar.set(1)
		else:
			self.treatmentColorModelVar.set(0)
		f = Tkinter.Frame(self.treatmentGroup.interior())
		f.pack(side="top", fill="x")
		self.treatmentColorModelButton = Tkinter.Checkbutton(f,
					text=" Color",
					onvalue=1, offvalue=0,
					variable=self.treatmentColorModelVar,
					command=self._treatmentColorModelCB)
		self.treatmentColorModelButton.pack(side="left")
		self.treatmentColorModelMenu = Pmw.OptionMenu(f,
					items = ( "representatives",
							"all members" ),
					initialitem=self.treatmentColorModelOf,
					menubutton_pady=0,
					command=self._treatmentColorModelCB)
		self.treatmentColorModelMenu.pack(side="left")

		self.treatmentSelModelVar = Tkinter.IntVar(parent)
		if self.treatmentSelModel:
			self.treatmentSelModelVar.set(1)
		else:
			self.treatmentSelModelVar.set(0)
		f = Tkinter.Frame(self.treatmentGroup.interior())
		f.pack(side="top", fill="x")
		self.treatmentSelModelButton = Tkinter.Checkbutton(f,
					text=" Choose",
					onvalue=1, offvalue=0,
					variable=self.treatmentSelModelVar,
					command=self._treatmentSelModelCB)
		self.treatmentSelModelButton.pack(side="left")
		self.treatmentSelModelMenu = Pmw.OptionMenu(f,
					items = ( "representatives",
							"all members" ),
					initialitem=self.treatmentSelModelOf,
					menubutton_pady=0,
					labelpos="e",
					label_text=" in Model Panel",
					command=self._treatmentSelModelCB)
		self.treatmentSelModelMenu.pack(side="left")

		height = min(len(self.nmrc.clusters), 10)
		values = []
		maxLength = 0
		for id in range(len(self.nmrc.clusters)):
			m = self.clusterInfo[id][0]
			n = len(self.clusterInfo[id][1])
			v = "%d (%d models, rep: %s)" % (id, n, m.oslIdent())
			values.append(v)
			if len(v) > maxLength:
				maxLength = len(v)
		self.listbox = Pmw.ScrolledListBox(self.parent,
					listbox_selectmode="extended",
					listbox_width=maxLength,
					listbox_height=height,
					selectioncommand=self._listboxSelect)
		self.listbox.pack(fill="both", expand=True)
		self.listbox.setlist(values)

	def _treatmentCB(self):
		self.treatmentShow = not self.treatmentShow
		if self.treatmentShow:
			self.treatmentGroup.configure(ring_relief="groove",
						tag_image=self.downArrow)
			self.treatmentGroup.expand()
		else:
			self.treatmentGroup.configure(ring_relief="flat",
						tag_image=self.rightArrow)
			self.treatmentGroup.collapse()
		self._savePrefs()

	def _savePrefs(self):
		prefs["treatment"] = {
			"show": self.treatmentShow,
			"selectAtoms": self.treatmentSelAtom,
			"selectAtomsOf": self.treatmentSelAtomOf,
			"selectModels": self.treatmentSelModel,
			"selectModelsOf": self.treatmentSelModelOf,
			"colorModels": self.treatmentColorModel,
			"colorModelsOf": self.treatmentColorModelOf,
		}
		prefs.save()

	def _treatmentSelAtomCB(self, ignore=None):
		selAtom = self.treatmentSelAtomVar.get()
		selAtomOf = self.treatmentSelAtomMenu.getvalue()
		if (selAtom == self.treatmentSelAtom
		and selAtomOf == self.treatmentSelAtomOf):
			return
		self.treatmentSelAtom = selAtom
		self.treatmentSelAtomOf = selAtomOf
		self._selectionCB()
		self._savePrefs()

	def _treatmentColorModelCB(self, ignore=None):
		colorModel = self.treatmentColorModelVar.get()
		colorModelOf = self.treatmentColorModelMenu.getvalue()
		if (colorModel == self.treatmentColorModel
		and colorModelOf == self.treatmentColorModelOf):
			return
		self.treatmentColorModel = colorModel
		self.treatmentColorModelOf = colorModelOf
		self._colorCB()
		self._savePrefs()

	def _treatmentSelModelCB(self, ignore=None):
		selModel = self.treatmentSelModelVar.get()
		selModelOf = self.treatmentSelModelMenu.getvalue()
		if (selModel == self.treatmentSelModel
		and selModelOf == self.treatmentSelModelOf):
			return
		self.treatmentSelModel = selModel
		self.treatmentSelModelOf = selModelOf
		self._modelPanelCB()
		self._savePrefs()

	def _listboxSelect(self):
		self._selectionCB()
		self._colorCB()
		self._modelPanelCB()

	def _getSelected(self):
		cursel = self.listbox.getcurselection()
		if not cursel:
			idList = range(len(self.clusterInfo))
		else:
			idList = [ int(v.split(None, 1)[0]) for v in cursel ]
		return idList

	def _modelPanelCB(self):
		if not self.treatmentSelModel:
			return
		if self.treatmentSelModelOf == "representatives":
			# Choose representative
			mList = [ self.clusterInfo[id][0]
					for id in self._getSelected() ]
			self._getModelPanel().selectionChange(mList)
		elif self.treatmentSelModelOf == "all members":
			# Choose cluster
			mList = []
			for id in self._getSelected():
				mList.extend(self.clusterInfo[id][1])
			self._getModelPanel().selectionChange(mList)
		else:
			# No action
			pass

	def _selectionCB(self):
		from chimera import selection
		if not self.treatmentSelAtom:
			return
		if self.treatmentSelAtomOf == "representatives":
			# Select representative
			selection.clearCurrent()
			mList = [ self.clusterInfo[id][0]
					for id in self._getSelected() ]
			selection.addCurrent(mList)
			selection.addImpliedCurrent()
		elif self.treatmentSelAtomOf == "all members":
			# Select cluster
			selection.clearCurrent()
			mList = []
			for id in self._getSelected():
				mList.extend(self.clusterInfo[id][1])
			selection.addCurrent(mList)
			selection.addImpliedCurrent()
		else:
			# No action
			pass

	def _getModelPanel(self):
		from ModelPanel import ModelPanel
		from chimera import dialogs
		return dialogs.display(ModelPanel.name)

	def _colorCB(self):
		if self.treatmentColorModel == 0:
			# No action
			return
		idList = self._getSelected()
		from CGLtk.color import colorRange
		from chimera import MaterialColor
		colorList = [MaterialColor(r, g, b, 1.0)
					for r, g, b in colorRange(len(idList))]

		lb = self.listbox.component("listbox")
		origColor = lb["fg"]
		origSelColor = lb["selectforeground"]
		for i, color in enumerate(colorList):
			id = idList[i]
			if self.treatmentColorModelOf == "representatives":
				# Color representative
				for m in self.clusterInfo[id][1]:
					m.color = m.preclusterColor
					self._resetColor(m)
				self.clusterInfo[id][0].color = color
			else:
				# Color cluster
				for m in self.clusterInfo[id][1]:
					m.color = color
					self._resetColor(m)
			r, g, b, a = color.rgba()
			tkColor = "#%02x%02x%02x" % (int(r * 255),
							int(g * 255),
							int(b * 255))
			lb.itemconfigure(id, fg=tkColor,
						selectforeground=tkColor)
		for id in range(len(self.clusterInfo)):
			if id in idList:
				continue
			for m in self.clusterInfo[id][1]:
				m.color = m.preclusterColor
				self._resetColor(m)
			lb.itemconfigure(id, fg=origColor,
						selectforeground=origSelColor)

	def _resetColor(self, m):
		for a in m.atoms:
			a.color = None
			a.surfaceColor = None
			a.labelColor = None
			a.vdwColor = None
		for b in m.bonds:
			b.color = None
			b.labelColor = None

	def Hide(self):
		self.emHide()

	def Quit(self):
		self.emQuit()

	def Save(self):
		if not hasattr(self, '_saveDialog'):
			self._saveDialog = _SaveClusteringInfo(self,
				title="Save Clusting Information")
		self._saveDialog.enter()

	def destroy(self):
		if hasattr(self, '_saveDialog'):
			self._saveDialog.destroy()
			delattr(self, '_saveDialog')
		# calling ModelessDialog.Close causes recursion
		ModelessDialog.destroy(self)

	#
	# Extension manager callback routines
	#
	def emName(self):
		return self.title

	def emRaise(self):
		self.enter()

	def emHide(self):
		self.Close()

	def emQuit(self):
		self.destroy()
		chimera.extension.manager.deregisterInstance(self)

from OpenSave import SaveModeless
class _SaveClusteringInfo(SaveModeless):
	def __init__(self, clusterDialog, **kw):
		self.clusterDialog = clusterDialog
		SaveModeless.__init__(self, **kw)

	def Apply(self):
		clusterIDs = self.clusterDialog._getSelected()
		clusterInfos = [self.clusterDialog.clusterInfo[id] for id in clusterIDs]

		clusterInfos.sort(lambda c1, c2: cmp(len(c2[1]), len(c1[1])) \
				or cmp(min(c1[1]), min(c2[1])))
		from OpenSave import osOpen
		saveFile = osOpen(self.getPaths()[0], 'w')
		print>>saveFile, "# one cluster per line; first model on each line is representative"
		for clusterInfo in clusterInfos:
			print>>saveFile, clusterInfo[0],
			for member in sorted(clusterInfo[1]):
				if member == clusterInfo[0]:
					continue
				print>>saveFile, member,
			print>>saveFile, ""
		saveFile.close()

	def destroy(self):
		self.clusterDialog = None
		SaveModeless.destroy(self)