# Copyright (c) 2000 by the Regents of the University of California.
# All rights reserved.  See http://www.cgl.ucsf.edu/chimera/ for
# license details.
#
# $Id: base.py 40917 2016-04-04 18:19:43Z pett $

import chimera
from chimera.baseDialog import ModelessDialog
from chimera import help, openModels, Molecule
from Group import Group, GroupAttr
import Tix, Pmw
import Tkinter
import CGLtk
import os

_buttonInfo = {}
_mp = None

def addButton(name, callback, minModels=1, maxModels=None,
			moleculesOnly=True, balloon=None, defaultFrequent=True,
			defaultFavorite=None, groupsOkay=False):
	"""Add a button to the 'Model Actions' button list.

	   'name' is the button name (duh).  'callback' is the
	   function to call when the button is pressed.  The arg to
	   'callback' will be a list of models.  'min/maxModels'
	   indicate how many models have to be selected in the
	   browser for the button to be active ('None' indicates no
	   limit).  if 'moleculesOnly' is True, then those models have
	   to be Molecules.

	   This is a module function so that it can be called even if
	   the model panel has not yet been created.
	"""

	if defaultFavorite is None:
		defaultFavorite = defaultFrequent
	if _buttonInfo.has_key(name):
		raise KeyError, \
			"Button named '%s' already exists" % name
	_buttonInfo[name] = (callback, minModels, maxModels,
					moleculesOnly, balloon, defaultFavorite, groupsOkay)

	if _mp:
		_mp._confDialog.newButton(name, balloon=balloon,
					defaultFavorite=defaultFavorite)
		_mp._showButton(name)

_columnNames = []
_valueTypes = []
_valueFuncs = []
_defaultShows = []
def addColumns(columnInfo, defaultShown=1):
	"""Add columns to the model table.

	   'columnInfo' is a list of 3-tuples, one for each column
	   to add.  The tuple consists of (column name, value type,
	   value-fetch function).  The value type should be 'text',
	   'image', 'imagetext', or 'toggle'.  The value-fetch function
	   takes one argument (a model) and (for 'image' and 'text')
	   should return the value to display in the table cell.  For
	   'imagetext' the return value should be an (image, text)
	   tuple.  'toggle' shows a toggle button and the return value
	   should be a (boolean, callback) tuple.  If the boolean is
	   true, a check will be shown on the toggle button; otherwise
	   the button is blank.  The callback is invoked when the
	   toggle is pressed, with the model and the new boolean as
	   arguments.  The value of an image is the name of the image
	   (the Tix name, e.g. 'tick' for tickmark).  A value of None
	   for image or text will leave a blank cell.

	   'defaultShown' controls whether the column is shown in
	   the model table or not as long as the user has not yet
	   expressed a preference in the Configuration panel about it.
	"""

	noneShown = 1
	for name,type,func in columnInfo:
		if name in _columnNames:
			raise ValueError, "Duplicate model panel"\
						"column name: %s" % name
		_columnNames.append(name)
		_valueTypes.append(type)
		_valueFuncs.append(func)
		_defaultShows.append(defaultShown)
		if _mp:
			try:
				shown = _mp._confDialog.prefs[
							'shownColumns'][name]
			except KeyError:
				shown = defaultShown
			_mp.shownColumns.append(shown)
			if shown:
				noneShown = 0
			_mp._confDialog.newColumn(name, shown)

	if not noneShown:
		_mp._buildTable()

def readableName(model):
	if model.name:
		for char in model.name:
			if ord(char) < 32:
				break
		else:
			return model.name
	if isinstance(model, chimera.Molecule):
		return "unknown Molecule"
	if isinstance(model, chimera.MSMSModel):
		return "unknown MSMS surface"
	if isinstance(model, chimera.VRMLModel):
		return "unknown VRML object"
	return "unknown"

def inputPath(model):
	if not hasattr(model, 'openedAs') or '\n' in model.openedAs[0]:
		return readableName(model)
	path = model.openedAs[0]
	curdir = os.getcwd() + os.sep
	if path.startswith(curdir):
		return path[len(curdir):]
	return path

def getPhysicalChains(model):
	# return chains of physically connected residues as list of lists;
	# single-residue "chains" collated into first list
	from operator import add
	physical = [[]]
	seen = {}

	for root in model.roots(1):
		resAtoms = root.atom.residue.atoms
		numRootAtoms = root.size.numAtoms

		if numRootAtoms < len(resAtoms):
			# disconnected residue; continue only if this is
			# the largest fragment of the residue
			largestFrag = 1
			for atom in resAtoms:
				if atom.rootAtom == root.atom:
					continue
				if atom.molecule.rootForAtom(atom, 1).size\
						.numAtoms > numRootAtoms:
					largestFrag = 0
					break
			if not largestFrag:
				continue

		if numRootAtoms <= len(resAtoms):
			curPhysical = physical[0]
		else:
			curPhysical = []
			physical.append(curPhysical)
		for atom in model.traverseAtoms(root):
			res = atom.residue
			if seen.has_key(res):
				continue
			seen[res] = 1

			curPhysical.append(res)
	
	return physical

def nameColumn(m):
	if _mp and _mp._confDialog.showColorVar.get():
		bcolor = isinstance(m, chimera.Molecule) and m.color or None
		return readableName(m), bcolor
	return readableName(m)
	
def _oslIdent(item):
	if isinstance(item, Group):
		models = item.models
		osls = [m.oslIdent() for m in models]
		from chimera.misc import oslModelCmp
		osls.sort(oslModelCmp)
		return u"%s\N{HORIZONTAL ELLIPSIS}%s" % (osls[0][1:], osls[-1][1:])
	return item.oslIdent()[1:]

from _surface import SurfaceModel
addColumns([
	('ID', 'text', _oslIdent),
	('', 'well', lambda m: (hasattr(m, 'color') and (not isinstance(m, SurfaceModel)) and m.color, True, True, lambda m, c: setattr(m, 'color', c))),
	('Active', 'toggle', lambda m: (m.openState.active, lambda m, b: setattr(m.openState, 'active', b))),
	('Shown', 'toggle', lambda m: (m.display, lambda m, b: setattr(m, 'display', b))),
	('Name', 'text', nameColumn)
])
addColumns([
	('Note', 'text', lambda m: (hasattr(m, 'note') and m.note or '')),
	('Input file', 'text', inputPath)
], defaultShown=False)

class ModelPanel(ModelessDialog):
	title="Model Panel"
	buttons=('Close','Configure...')
	name="model panel"
	help="UsersGuide/modelpanel.html"

	itemTableHelp = "click to select models;"\
			"\nright-hand action buttons work on selected models;"\
			"\ndouble-click to perform default action on model"\
			"\n(see 'Configure...' for default action info)"
	def fillInUI(self, parent):
		global _mp
		_mp = self

		self.parent = parent

		# model table
		self._getConfig()

		# action buttons
		atf = self.allTitleFrame = Tkinter.Frame(self.parent)
		atf.grid(row=5, column=20, sticky='w')
		atf.grid_remove()
		self.commandLabel = Tkinter.Label(atf, text="Command")
		self.commandLabel.grid(row=0, column=1)
		Tkinter.Label(atf, text="Fav").grid(row=0, column=3, sticky='w')

		self.buttonScroll = Pmw.ScrolledFrame(self.parent,
							hscrollmode='none')
		self.buttonScroll.grid(row=10, column=20, sticky='nsew')
		self.favActionButtons = FavButtonBox(
			self.buttonScroll.interior(), orient='vertical', pady=0)
		self._shownActions = None
		self.allActionButtons = AllButtonBox(
			self.buttonScroll.interior(), self)
		self._favToggle = Pmw.RadioSelect(self.parent,
				command=self._favToggleCB,
				orient="horizontal", buttontype="radiobutton")
		self._favToggle.add("favorites")
		self._favToggle.add("all")
		self._favToggle.grid(row=20, column=20)
		self.favButtonsCreated = []
		self.allButtonsCreated = []

		self._addColumns()
		# add buttons from other extensions...
		self._addButtons()

		# add standard buttons
		addButton("add/edit note...", noteCmd, balloon="add notation"
			" that will be displayed in model table", moleculesOnly=False)
		addButton("activate", lambda m, f='active', v=1,
			smf=setModelField: smf(m, f, v, openState=1),
			moleculesOnly=False,
			balloon="make selected models active"
			"\n(responsive to mouse motions)")
		addButton("activate all", lambda m, f=activateAllCmd: f(),
			minModels = 0, moleculesOnly=False,
			balloon="activate all models;\n"
			"restore previous active states with this same button")
		addButton("activate only", lambda m, f='active',
			smfo=setModelFieldOnly: smfo(m, f, openState=1),
			moleculesOnly=False,
			balloon="make selected models active"
			"\n(responsive to mouse motions);\ndeactivate others")
		addButton("attributes...", attributesCmd,
			moleculesOnly=False,
			balloon="inspect/modify model attributes")
		def runClipping(models):
			import ModelClip.gui
			cd = chimera.dialogs.display(ModelClip.gui.ClipDialog.name)
			cd.setModel(models[0])
		addButton("clipping...", runClipping, moleculesOnly=False,
			balloon="adjust per-model clipping plane")
		addButton("close", openModels.close, moleculesOnly=False,
			balloon="close models")
		addButton("deactivate", lambda m, f='active', v=0,
			smf=setModelField: smf(m, f, v, openState=1),
			moleculesOnly=False,
			balloon="make selected models inactive"
			"\n(insensitive to mouse motions)")
		addButton("focus", focusCmd, moleculesOnly=False,
			defaultFavorite=False,
			balloon="bring selected models fully into view"
			"\nin main graphics window")
		addButton("group/ungroup", groupCmd, moleculesOnly=False,
			balloon= "group selected items into a single line"
			" or, if just one group selected, ungroup them",
			groupsOkay=True)
		addButton("hide", lambda m, f='display', v=0,
			smf=setModelField: smf(m, f, v),
			moleculesOnly=False,
			balloon="hide selected models; undo with 'show'")
		def showRainbowDialog(models):
			from chimera import dialogs
			from rainbow import RainbowDialog
			if len(models) > 1:
				target = "models"
			else:
				target = "residues"
			dialogs.display(RainbowDialog.name).configure(
						models=models, target=target)
		addButton("rainbow...", showRainbowDialog, defaultFavorite=False,
			balloon="rainbow-color residues or chains")
		addButton("rename...", renameCmd, moleculesOnly=False, groupsOkay=True)
		addButton("select", selectCmd, moleculesOnly=False,
			balloon="incorporate models into graphics window"
			"\nselection using current selection mode"
			"\n(see graphics window Selection menu)")
		from chainPicker import ChainPicker
		addButton("select chain(s)...", lambda m,
			cp=ChainPicker: cp(m).enter(),
			balloon="select some/all chains\n"
			"(using current selection\n"
			"mode from Selection menu)")
		addButton("sequence...", seqCmd, defaultFavorite=False,
			balloon="inspect molecule sequence")
		addButton("show", lambda m, f='display', v=1,
			smf=setModelField: smf(m, f, v),
			moleculesOnly=False, balloon="unhide selected models")
		addButton("show all atoms", showAllAtomsCmd,
			balloon="show all atoms"
			" (but use 'show' to undo 'hide')")
		addButton("show only", lambda m, f='display',
			smfo=setModelFieldOnly: smfo(m, f), moleculesOnly=False,
			balloon="show selected models and hide all others")
		addButton("surface main", lambda m, c="main", sc=surfCmd:
			sc(m, c), defaultFavorite=False,
			balloon="surface non-ligand portion of models")
		def showTileDialog(models):
			from chimera import dialogs
			from EnsembleMatch.choose import TileStructuresCB
			dialogs.display(TileStructuresCB.name).configure(
								models=models)
		addButton("tile...", showTileDialog, minModels=2,
			moleculesOnly=False, defaultFavorite=False,
			balloon="arrange selected models into a"
			"\nrectangular grid and focus on them")
		addButton("toggle active", lambda m, f='active',
			tmf=toggleModelField: tmf(m, f, openState=1),
			moleculesOnly=False,
			balloon="invert active states of selected models")
		addButton("trace backbones", lambda m, bc=backboneCmd:
			bc(m, resTrace=0), defaultFavorite=False,
			balloon="show backbone atom trace for protein"
			"\nor nucleic acid; undo with 'show all atoms'")
		addButton("trace chains", backboneCmd, defaultFavorite=False,
			balloon="show residue connectivity trace for protein"
			"\nor nucleic acid; undo with 'show all atoms'")
		from transformDialog import TransformDialog
		addButton("transform as...", TransformDialog,
			moleculesOnly=False,
			balloon="rotate/translate models same as another model")
		from writePDBdialog import WritePDBdialog
		addButton("write PDB", lambda mols: chimera.dialogs.display(
			WritePDBdialog.name).configure(mols, selOnly=False),
			balloon="write molecule as PDB file")
		from ksdsspDialog import KsdsspDialog
		addButton("compute SS", KsdsspDialog,
			balloon="compute secondary structure elements"
			"\nusing Kabsch and Sander algorithm")
		
		self._favToggle.invoke("favorites")

		# add these last, since if they somehow fire before the
		# constructor is complete then an exception will occur
		self._updateHandler = None
		chimera.triggers.addHandler('Model', self._requestUpdate, None)
		chimera.triggers.addHandler('OpenState', self._fillTable, None)
	
	def Configure(self):
		"""configure action buttons"""
		self._confDialog.enter()

	def see(self, buttonName):
		pass # I don't think anything calls this

	def selected(self, moleculesOnly=False, groupsOkay=False):
		"""Return a list of the selected models"""

		selected = []
		for ii in self.itemTable.hlist.info_selection():
			item = self.items[int(ii)]
			if groupsOkay:
				selected.append(item)
				continue
			models = _getModels(item)
			if moleculesOnly:
				models = [m for m in models if isinstance(m, Molecule)]
			selected.extend(models)
		return selected

	def selectionChange(self, models, extend=False, priorSelection=None):
		"""set (or extend) the selection to contain the given models
		
		   'models' can be Models or oslIdents"""

		# may have to ungroup groups if they are partially selected
		newSelected = []
		breakGroups = []
		if models:
			testSet = set()
			for m in models:
				testSet.update(_getModels(m))
			if isinstance(models[0], basestring):
				# OSL ident
				for i, item in enumerate(self.items):
					val = item.oslIdent()
					if isinstance(val, set):
						if testSet & val:
							breakGroups.append(item)
					elif val in testSet:
						newSelected.append(i)
			else:
				for i, item in enumerate(self.items):
					val = set(_getModels(item))
					if val & testSet:
						if val & testSet < val:
							breakGroups.append(item)
						else:
							newSelected.append(i)
		if breakGroups:
			if priorSelection is None:
				priorSelection = self.selected(groupsOkay=True)
			for group in breakGroups:
				self.items.remove(group)
				self.items.extend(group.components)
				if group in priorSelection:
					priorSelection.remove(group)
					priorSelection.extend(group.components)
			self.selectionChange(models, extend=extend,
					priorSelection=priorSelection)
			return
		if priorSelection is not None:
			self._fillTable(fromScratch=True, selected=priorSelection)
		if not extend:
			self.itemTable.hlist.selection_clear()

		for i in newSelected:
			self.itemTable.hlist.selection_set(i)
		self._selChangeCB()

	def _addButtons(self):
		"""Add buttons to interface that were requested before
		   panel was created.
		"""

		for name, info in _buttonInfo.items():
			balloon, defaultFavorite = info[-3:-1]
			self._confDialog.newButton(name, balloon=balloon,
						defaultFavorite=defaultFavorite)
			self._showButton(name)

	def _addColumns(self):
		"""Process column information"""
		self.shownColumns = []

		for i in range(len(_columnNames)):
			name = _columnNames[i]
			if name == "Note":
				shown = False
				for m in openModels.list():
					if hasattr(m, 'note') and m.note:
						shown = True
						break
			else:
				try:
					shown = self._confDialog.prefs[
							'shownColumns'][name]
				except KeyError:
					shown = _defaultShows[i]
			self.shownColumns.append(shown)
			self._confDialog.newColumn(name, shown)
		self._buildTable()

	def _buttonParams(self, name):
		callback, minModels, maxModels, moleculesOnly, balloon, \
					defaultFavorite, groupsOkay = _buttonInfo[name]
		kw = {}
		state = 'normal'
		if self._shouldDisable(minModels, maxModels, moleculesOnly):
			state = 'disabled'
		kw['state'] = state
		kw['pady'] = 0
		# if you click a button fast enough, you can get around it's
		# upcoming disabling...
		def cmd(cb=callback, s=self, mo=moleculesOnly, minm=minModels,
				maxm=maxModels, go=groupsOkay):
			if not s._shouldDisable(minm, maxm, mo):
				cb(s.selected(moleculesOnly=mo, groupsOkay=go))
		kw['command'] = cmd
		return kw, balloon, defaultFavorite

	def _buildTable(self):
		if hasattr(self, 'itemTable'):
			# can't dynamically add columns to Tix widget;
			# destroy and recreate
			selected = self.selected()
			self.itemTable.grid_forget()
			self.itemTable.destroy()
		else:
			selected = None

		w, h = self._confDialog.prefs['table w/h']
		inch = self.parent.winfo_fpixels("1i")
		self.itemTable = Tix.ScrolledHList(self.parent,
			width="%d" % int(w * inch + 0.5),
			height="%d" % int(h * inch + 0.5),
			options="""hlist.columns %d
			hlist.header 1
			hlist.selectMode extended
			hlist.indicator 0"""
			% len(filter(lambda s: s == 1, self.shownColumns)))
		help.register(self.itemTable, balloon=self.itemTableHelp)
		self.itemTable.hlist.config(browsecmd=self._selChange,
							command=self._dblClick)
		self.textStyle = Tix.DisplayStyle("text",
				refwindow=self.itemTable)
		# get a style for checkbutton columns...
		self.checkButtonStyle = Tix.DisplayStyle("window",
				refwindow=self.itemTable, anchor="center")
		self.colorWellStyle = Tix.DisplayStyle("window",
				refwindow=self.itemTable, anchor="center")
		colNum = 0
		self.columnMap = []
		showFullTitles = False
		last = self._confDialog.prefs["lastUse"]
		from time import time
		now = self._confDialog.prefs["lastUse"] = time()
		if last is None or now - last > 777700: # about 3 months
			showFullTitles = True
		for index in range(len(_columnNames)):
			if not self.shownColumns[index]:
				continue
			self.columnMap.append(index)
			text = _columnNames[index]
			if _valueTypes[index] == 'toggle' \
			and not showFullTitles:
				text = text[:1]
			self.itemTable.hlist.header_create(colNum,
						itemtype='text', text=text)
			colNum = colNum + 1
			
		self.parent.columnconfigure(10, weight=1)
		self.parent.rowconfigure(10, weight=1)
		self.itemTable.grid(row=5, column=10, sticky='nsew',
								rowspan=16)
		self._fillTable(selected=selected, fromScratch=1)
		self.itemTable.bind("<Configure>", self._rememberSize, add=True)

	def _dblClick(self, item):
		"""user has double-clicked on model table entry"""

		# if the state of the action buttons is due to change,
		# execute that change before calling the double-click routine
		if hasattr(self, '_selChangeIdle') and self._selChangeIdle:
			self.parent.after_cancel(self._selChangeIdle)
			self._selChangeCB()

		self._confDialog.dblClick()

	def _doUpdate(self):
		self._updateHandler = None
		self._fillTable(*self._triggerArgs)
		self._triggerArgs = None

	def _favButton(self, name, fav):
		names = self.favButtonsCreated
		actionButtons = self.favActionButtons
		if fav:
			names.append(name)
			names.sort(lambda a, b: cmp(a.lower(), b.lower()))
			kw, balloon, defaultFavorite = self._buttonParams(name)
			index = names.index(name)
			if index == len(names)-1:
				addFunc = actionButtons.add
			else:
				addFunc = actionButtons.insert
				kw['beforeComponent'] = names[index+1]

			but = addFunc(name, **kw)
			but.config(default='disabled')
			if balloon:
				help.register(but, balloon=balloon)
		else:
			names.remove(name)
			actionButtons.delete(name)

	def _favToggleCB(self, label):
		if label == "favorites":
			self._showActions(self.favActionButtons)
		else:
			self._showActions(self.allActionButtons)

	def _fillTable(self, *triggerArgs, **kw):
		if len(triggerArgs) > 0:
			if triggerArgs[0] == 'OpenState':
				if 'active change' not in triggerArgs[-1].reasons:
					return
			elif triggerArgs[0] == 'Model':
				global _groupNameCache
				_groupNameCache.clear()
		hlist = self.itemTable.hlist
		defaultable = False
		if kw.get('selected', None) != None:
			selected = kw['selected']
		else:
			selected = self.selected(groupsOkay=True)
			defaultable = True
		rebuild = False
		curModels = set(openModels.list())
		if not hasattr(self, 'items'):
			global _groups
			self.items = _groups[:]
			_groups[:] = []
			rebuild = True
		elif kw.get('fromScratch', False):
			rebuild = True
		else:
			prevModels = set()
			for item in self.items:
				prevModels.update(_getModels(item))
			if curModels != prevModels:
				rebuild = True

		if rebuild:
			newItems = []
			for item in self.items:
				if not isinstance(item, Group):
					continue
				item.update()
				if len(item.models) > 1:
					newItems.append(item)
					curModels.difference_update(item.models)
			self.items = newItems + list(curModels)
			self.items.sort(self._itemSort)
			self._prevValues = {}
			hlist.delete_all()
			vf = _valueFuncs[self.columnMap[0]]
			for i, item in enumerate(self.items):
				hlist.add(i, **self._hlistKw(item, 0))
				self._prevValues[(i, 0)] = vf(item)
			for ci in range(1, len(self.columnMap)):
				vf = _valueFuncs[self.columnMap[ci]]
				for i, item in enumerate(self.items):
					hlist.item_create(i, ci, **self._hlistKw(item, ci))
					self._prevValues[(i, ci)] = vf(item)
		else:
			for ci in range(len(self.columnMap)):
				vf = _valueFuncs[self.columnMap[ci]]
				for i, item in enumerate(self.items):
					curVal = vf(item)
					prevVal = self._prevValues[(i, ci)]
					if isinstance(curVal, tuple):
						for vi in range(len(curVal)):
							valItem = curVal[vi]
							if callable(valItem) and not isinstance(valItem,
									GroupAttr):
								continue
							pv = prevVal[vi]
							if type(valItem) != type(pv) or valItem != pv:
								break
						else:
							# equal
							continue
					elif curVal == prevVal:
						continue
					self._prevValues[(i, ci)] = curVal
					hlist.item_configure(i, ci, **self._hlistKw(item, ci))
		# if only one item, select it
		if defaultable and len(self.items) == 1:
			selected = self.items
		for item in selected:
			if item not in self.items:
				continue
			hlist.selection_set(self.items.index(item))
		self._selChange(None)

	def _getConfig(self):
		"""retrieve configuration preferences"""

		# set up configuration dialog
		from confDialog import ConfDialog
		self._confDialog = ConfDialog(self)
		self._confDialog.Close()

	def _hlistKw(self, item, colNum):
		vt = _valueTypes[self.columnMap[colNum]]
		vf = _valueFuncs[self.columnMap[colNum]]
		kw = {'itemtype': vt}
		txt = None
		img = None
		val = vf(item)
		if isinstance(val, set) and vt not in ['toggle', 'well']:
			return {}
		from Group import GroupAttr
		if vt == 'text':
			txt = val
			if isinstance(txt, GroupAttr):
				testable = list(txt.vals)[0]
			else:
				testable = txt
			if not isinstance(testable, basestring):
				txt, bcolor = txt
				if bcolor is not None:
					if not isinstance(bcolor, basestring):
						if hasattr(bcolor, 'rgba'):
							rgba = bcolor.rgba()
						else:
							rgba = bcolor
						from CGLtk.color import rgba2tk
						bcolor = rgba2tk(rgba)
						fcolor = CGLtk.textForeground(
							bcolor, self.itemTable)
					kw['style'] = Tix.DisplayStyle("text",
						refwindow=self.itemTable,
						background=bcolor,
						foreground=fcolor,
						selectforeground=bcolor)
			else:
				kw['style'] = self.textStyle
		elif vt == 'image':
			img = val
		elif vt == 'imagetext':
			img, txt = val
		elif vt == 'toggle':
			kw['itemtype'] = 'window'
			truth, cb = val
			togKw = {'command':
				# avoid holding references to model
				lambda cb=cb, i=self.items.index(item),
					nt=isinstance(truth, GroupAttr) or not truth:
					cb(self.items[i], nt),
				'indicatoron': 0,
				'borderwidth': 0}
			if isinstance(truth, GroupAttr):
				togKw['image'] = self.itemTable.tk.call(
					'tix', 'getimage', 'ck_onoff_37')
			elif truth:
				togKw['image'] = self.itemTable.tk.call(
					'tix', 'getimage', 'ck_on')
			else:
				togKw['image'] = self.itemTable.tk.call(
					'tix', 'getimage', 'ck_off')
			toggle = Tkinter.Checkbutton(
						self.itemTable.hlist, **togKw)
			kw['window'] = toggle
			kw['style'] = self.checkButtonStyle
		elif vt == 'well':
			color, noneOkay, alphaOkay, cb = val
			if color is False:
				kw['itemtype'] = 'text'
				txt = ""
			else:
				kw['itemtype'] = 'window'
				if isinstance(color, chimera.MaterialColor):
					color = color.rgba()
				from weakref import proxy
				def wellCB(clr, cb=cb, mdl=proxy(item)):
					if clr is not None:
						clr = chimera.MaterialColor(
									*clr)
					cb(mdl, clr)
				from CGLtk.color.ColorWell import ColorWell
				kw['window'] = ColorWell(self.itemTable.hlist,
					color, callback=wellCB,
					multiple=isinstance(color, GroupAttr),
					width=18, height=18,
					noneOkay=noneOkay, wantAlpha=alphaOkay)
				kw['style'] = self.colorWellStyle
		else:
			raise ValueError("Unknown column type: '%s'" % vt)
		
		if txt != None:
			kw['text'] = unicode(txt)
		if img != None:
			kw['image'] = self.itemTable.tk.call(
							'tix', 'getimage', img)
		return kw
	
	def _itemSort(self, i1, i2):
		def getVal(vals):
			if isinstance(vals, set):
				return min(vals)
			return vals
		id1 = getVal(i1.id)
		id2 = getVal(i2.id)
		if id1 < id2:
			return -1
		if id1 > id2:
			return 1
		subid1 = getVal(i1.subid)
		subid2 = getVal(i2.subid)
		if subid1 < subid2:
			return -1
		if subid1 > subid2:
			return 1
		return 0

	def _rememberSize(self, event):
		w, h = event.width, event.height
		if min(w,h) < 20:
			return
		inch = self.parent.winfo_fpixels("1i")
		self._confDialog.prefs["table w/h"] = (w/inch, h/inch)

	def _requestUpdate(self, *triggerArgs):
		# rebuilding the table is a little slow, so if many models are being
		# opened/closed one at a time in script, the rebuild can slow the
		# script prohibitively, so only initiate the reuild after a delay
		# to try to allow all the triggers to fire
		if self._updateHandler:
			self.parent.after_cancel(self._updateHandler)
		self._triggerArgs = triggerArgs
		# after_idle() doesn't seem to suppress any updates
		self._updateHandler = self.parent.after(500, self._doUpdate)

	def _selChange(self, item):
		# slow browse callback interferes with double-click detection,
		# so delay callback enough to allow most double-clicks to work
		if hasattr(self, '_selChangeIdle') and self._selChangeIdle:
			self.parent.after_cancel(self._selChangeIdle)
		self._selChangeIdle = self.parent.after(300, self._selChangeCB)

	def _selChangeCB(self):
		numSel = len(self.itemTable.hlist.info_selection())
		allButtons = _buttonInfo.keys()
		favs = self._confDialog.prefs["favorites"]
		for buttons, actionButtons in [
				([b for b in allButtons if favs[b]], self.favActionButtons),
				(allButtons, self.allActionButtons)]:
			for b in buttons:
				state = 'normal'
				callback, minModels, maxModels, moleculesOnly, \
					balloon, defaultFavorite, groupsOkay \
							= _buttonInfo[b]
				if self._shouldDisable(minModels, maxModels,
								moleculesOnly):
					state = 'disabled'
				actionButtons.button(b).config(state=state)
		self._selChangeIdle = None

	def _shouldDisable(self, minModels, maxModels, moleculesOnly):
		if moleculesOnly:
			numSel = len(self.selected(moleculesOnly=True))
		else:
			numSel = len(self.itemTable.hlist.info_selection())
		if minModels != None and numSel < minModels \
		or maxModels != None and numSel > maxModels:
			return 1
		return 0

	def _showActions(self, actionButtons):
		if actionButtons == self._shownActions:
			return
		if self._shownActions:
			if actionButtons == self.favActionButtons:
				self.allActionButtons.grid_remove()
				self.allTitleFrame.grid_remove()
			else:
				self.favActionButtons.grid_remove()
				self.allTitleFrame.grid()
				bw = actionButtons.buttonWidth()
				lw = self.commandLabel.winfo_reqwidth()
				pad = (bw-lw) /2.0
				self.allTitleFrame.columnconfigure(0, minsize=pad)
				self.allTitleFrame.columnconfigure(2, minsize=pad)
		actionButtons.grid()
		self.buttonScroll.component('clipper').configure(
						width=actionButtons.clipWidth()+2, height='2i')
		if chimera.tkgui.windowSystem == 'aqua':
			# work around bug where Aqua would behave as if the
			# scroller was to the right of its actual position
			# once the clipper was narrowed
			def later(tl = self.allTitleFrame.winfo_toplevel()):
				tl.wm_geometry(tl.wm_geometry())
			self.allTitleFrame.after(100, later)
		self.buttonScroll.yview(mode='moveto', value=0.0)
		self._shownActions = actionButtons

	def _showButton(self, name):
		kw, balloon, defaultFavorite = self._buttonParams(name)
		favPrefs = self._confDialog.prefs['favorites']
		names = self.allButtonsCreated
		actionButtons = self.allActionButtons
		names.append(name)
		names.sort(lambda a, b: cmp(a.lower(), b.lower()))
		index = names.index(name)
		if index == len(names)-1:
			addFunc = actionButtons.add
		else:
			addFunc = actionButtons.insert
			kw['beforeComponent'] = names[index+1]

		but = addFunc(name, **kw)
		but.config(default='disabled')
		if balloon:
			help.register(but, balloon=balloon)
		if favPrefs.get(name, defaultFavorite):
			self._favButton(name, True)

if not chimera.nogui:
	class FavButtonBox(Pmw.ButtonBox):
		def __init__(self, *args, **kw):
			Pmw.ButtonBox.__init__(self, *args, **kw)

		def clipWidth(self):
			maxWidth = 0
			for i in range(self.numbuttons()):
				w = self.button(i).winfo_reqwidth()
				if w > maxWidth:
					maxWidth = w
			return maxWidth

		buttonWidth = clipWidth

	class AllButtonBox(Tkinter.Frame):
		def __init__(self, master, modelPanel):
			Tkinter.Frame.__init__(self, master)
			self.__rowInfo = {}
			self.__modelPanel = modelPanel
			self.__maxButtonWidth = 0

		def add(self, name, **buttonKw):
			return self.__addButton(name, len(self.__rowInfo), **buttonKw)

		def button(self, name):
			return self.__rowInfo[name][2]

		def clipWidth(self):
			if self.__rowInfo:
				return self.__maxButtonWidth + self.__checkWidth
			return 0

		def buttonWidth(self):
			return self.__maxButtonWidth

		def insert(self, name, beforeComponent=None, **buttonKw):
			at = self.__rowInfo[beforeComponent][0]
			for bname, info in self.__rowInfo.items():
				row, frame, button, check = info
				if row >= at:
					frame.grid_forget()
					frame.grid(row=row+1, column=0)
					self.__rowInfo[bname][0] += 1
			return self.__addButton(name, at, **buttonKw)

		def __addButton(self, name, row, **buttonKw):
			f = Tkinter.Frame(self)
			f.grid(row=row, column=0)
			b = Tkinter.Button(f, text=name, **buttonKw)
			b.grid(row=0, column=0, sticky="ew")
			bw = b.winfo_reqwidth()
			if bw > self.__maxButtonWidth:
				for info in self.__rowInfo.values():
					info[1].columnconfigure(0, minsize=bw)
				self.__maxButtonWidth = bw
			else:
				f.columnconfigure(0, minsize=self.__maxButtonWidth)
			chkKw = {'command': lambda nm=name, s=self: s.__changeFav(nm),
				'indicatoron': 0, 'pady': 0,
				'borderwidth': 0}
			isFav = self.__modelPanel._confDialog.prefs['favorites'][name]
			if isFav:
				chkKw['image'] = f.tk.call('tix', 'getimage', 'ck_on')
			else:
				chkKw['image'] = f.tk.call('tix', 'getimage', 'ck_off')
			if chimera.tkgui.windowSystem == 'aqua':
				chkKw['relief'] = 'flat'
				chkKw['bd'] = 0
			check = Tkinter.Checkbutton(f, **chkKw)
			self.__checkWidth = check.winfo_reqwidth()
			check.grid(row=0, column=1)
			self.__rowInfo[name] = [row, f, b, check]
			return b

		def __changeFav(self, name):
			favs = self.__modelPanel._confDialog.prefs['favorites']
			fav = favs[name]
			self.__modelPanel._favButton(name, not fav)
			favsCopy = favs.copy()
			favsCopy[name] = not fav
			self.__modelPanel._confDialog.prefs['favorites'] = favsCopy
			chk = self.__rowInfo[name][-1]
			if fav:
				chk.configure(image=chk.tk.call('tix', 'getimage', 'ck_off'))
			else:
				chk.configure(image=chk.tk.call('tix', 'getimage', 'ck_on'))

from chimera import dialogs
dialogs.register(ModelPanel.name, ModelPanel)

def _setAttr(m, field, value, openState=0):
	if openState:
		setattr(m.openState, field, value)
	else:
		setattr(m, field, value)

# functions used in model panel button; could be called directly also
def setModelField(models, field, value, openState=0):
	for m in models:
		_setAttr(m, field, value, openState)

def setModelFieldOnly(models, field, onVal=1, offVal=0, openState=0):
	# turn off first, then on, so that models not in the models list
	# that nonetheless have shared openStates get the 'on' value
	for m in openModels.list():
		_setAttr(m, field, offVal, openState)
	for m in models:
		_setAttr(m, field, onVal, openState)

def toggleModelField(models, field, onVal=1, offVal=0, openState=0):
	openStates = {}
	for m in models:
		if openState:
			openStates[m.openState] = 1
			continue
		if curval == onVal:
			_setAttr(m, field, offVal, openState)
		else:
			_setAttr(m, field, onVal, openState)
	for os in openStates.keys():
		if getattr(os, field) == onVal:
			setattr(os, field, offVal)
		else:
			setattr(os, field, onVal)

_prevActivities = None
def activateAllCmd():
	"""Activate all models.  Restore previous activities if called again."""

	global _prevActivities
	if _prevActivities:
		for m in openModels.list():

			if _prevActivities.has_key(m.openState):
				m.openState.active = _prevActivities[
								m.openState]
		_prevActivities = None
		if _mp:
			butText = 'activate all'
	else:
		_prevActivities = {}
		for m in openModels.list():
			if _prevActivities.has_key(m.openState):
				continue
			_prevActivities[m.openState] = m.openState.active
			m.openState.active = 1
		if _mp:
			butText = 'restore activities'
	if _mp:
		favPrefs = _mp._confDialog.prefs['favorites']
		if favPrefs['activate all']:
			_mp.favActionButtons.component('activate all').config(text=butText)
		_mp.allActionButtons.button('activate all').config(text=butText)

_attrInspectors = {}
_headers = {}
_seqInspectors = {}
_inspectors = [_attrInspectors, _headers] # _seqInspectors is per chain

def _checkTrigger():
	global _modelTrigger
	for inspDict in _inspectors:
		if len(inspDict) > 0:
			break
	else:
		# should be no trigger active; start one
		_modelTrigger = chimera.triggers.addHandler(
						'Model', _modelTriggerCB, None)

def attributesCmd(models):
	global _attrInspectors
	_checkTrigger()
	for model in models:
		if not _attrInspectors.has_key(model):
			from modelInspector import ModelInspector
			_attrInspectors[model] = ModelInspector(model)
		_attrInspectors[model].enter()

def seqCmd(items):
	global _seqInspectors
	todo = []
	for item in items[:]:
		if not _seqInspectors.has_key(item):
			from chimera.Sequence import StructureSequence
			if isinstance(item, StructureSequence):
				copySeq = StructureSequence.__copy__(item)
				copySeq.name = item.fullName()
				_addSeqInspector(item, mavSeq=copySeq)
			else:
				seqs = item.sequences()
				if seqs:
					todo.extend(seqs)
				else:
					items.remove(item)
				continue
		_seqInspectors[item].enter()
	if todo:
		if len(todo) > 1:
			from seqPanel import SeqPickerDialog
			from chimera import dialogs
			d = dialogs.display(SeqPickerDialog.name)
			d.molListBox.setvalue(todo)
		else:
			seqCmd(todo)
	else:
		# if handed only sequences...
		return [_seqInspectors[item] for item in items]

def _addSeqInspector(item, mavSeq=None, mav=None):
	global _seqInspectors, _saveSessionTrigger
	if not _seqInspectors:
		from SimpleSession import SAVE_SESSION
		_saveSessionTrigger = chimera.triggers.addHandler(
				SAVE_SESSION, _saveSessionCB, None)
	trigMav = []
	hid = item.triggers.addHandler(item.TRIG_DELETE,
			lambda tn, md, td: md[0].Quit(), trigMav)
	def quitCB(mav, i=item, h=hid):
		del _seqInspectors[i]
		i.triggers.deleteHandler(i.TRIG_DELETE, h)
		if not _seqInspectors:
			from SimpleSession import SAVE_SESSION
			chimera.triggers.deleteHandler(SAVE_SESSION, _saveSessionTrigger)
	if mav:
		mav.quitCB = quitCB
	else:
		from MultAlignViewer.MAViewer import MAViewer
		if mavSeq.descriptiveName:
			title = "chain %s: %s" % (mavSeq.chainID, mavSeq.descriptiveName)
		else:
			title = mavSeq.name
		mav = MAViewer([mavSeq], title=title, quitCB=quitCB,
						autoAssociate=None, sessionSave=False)
	_seqInspectors[item] = mav
	trigMav.append(mav)

def _modelTriggerCB(trigName, myArg, modelsChanges):
	for model in modelsChanges.deleted:
		for inspDict in _inspectors:
			if inspDict.has_key(model):
				_deleteInspector(model, inspDict)

def _saveSessionCB(trigName, myArg, session):
	from SimpleSession import sessionID, sesRepr
	info = []
	for seq, mav in _seqInspectors.items():
		info.append((seq.name, sessionID(seq.molecule),
			[seq.saveInfo() for seq in mav.seqs], mav.saveInfo()))
	print>>session, """
try:
	from ModelPanel import restoreSeqInspectors
	restoreSeqInspectors(%s)
except:
	reportRestoreError("Error restoring sequence viewers")
""" % sesRepr(info)

def restoreSeqInspectors(info):
	global _seqInspectors
	from SimpleSession import idLookup
	for seqName, molID, seqsInfo, mavInfo in info:
		mol = idLookup(molID)
		for seq in mol.sequences():
			if seq.name == seqName:
				break
		else:
			continue
		if seq in _seqInspectors:
			continue

		from MultAlignViewer.MAViewer import restoreMAV
		from chimera.Sequence import restoreSequence
		mav = restoreMAV([restoreSequence(seqInfo)
						for seqInfo in seqsInfo], mavInfo)
		_addSeqInspector(seq, mav=mav)

def _deleteInspector(model, dict):
	inspector = dict[model]
	del dict[model]
	for inspDict in _inspectors:
		if len(inspDict) > 0:
			break
	else:
		# no inspectors; drop triggers
		chimera.triggers.deleteHandler('Model', _modelTrigger)
	if hasattr(inspector, 'destroy'):
		inspector.destroy()
	else:
		inspector._toplevel.destroy()

def backboneCmd(models, resTrace=1):
	from chimera.misc import displayResPart
	for m in models:
		if not hasattr(m, 'residues'):
			continue
		if resTrace:
			displayResPart(m.residues, trace=1)
		else:
			displayResPart(m.residues, backbone=1)

def focusCmd(models):
	from chimera import openModels, viewer, update
	shown = {}
	for m in openModels.list():
		shown[m] = m.display
		if m in models:
			m.display = 1
		else:
			m.display = 0
	update.checkForChanges()
	viewer.viewAll()
	if chimera.openModels.cofrMethod != chimera.OpenModels.Independent:
		openModels.cofrMethod = openModels.CenterOfView
		viewer.clipping = True

	for m,disp in shown.items():
		m.display = disp
	update.checkForChanges()

_groups = []
_groupNameCache = {}
def groupCmd(items, name=None):
	from Group import Group
	removeGroups = []
	addItems = []
	newGroup = None
	if len(items) == 1:
		item = items[0]
		if isinstance(item, Group):
			removeGroups.append(item)
			addItems.extend(item.components)
			sel = item.components
		else:
			from chimera import UserError
			raise UserError("Cannot group a single model.")
	else:
		models = []
		for item in items:
			if isinstance(item, Group):
				removeGroups.append(item)
				models.extend(item.models)
			else:
				models.append(item)
		global _groupNameCache
		models.sort()
		key = tuple([id(m) for m in models])
		if name is None:
			if key in _groupNameCache:
				name = _groupNameCache[key]
			else:
				name = getGroupName(items)
		_groupNameCache[key] = name
		newGroup = Group(items, name)
		addItems.append(newGroup)
		sel = addItems
	if _mp:
		selected = _mp.selected()
		for rg in removeGroups:
			_mp.items.remove(rg)
		_mp.items.extend(addItems)
		_mp._fillTable(fromScratch=True, selected=selected)
		_mp.selectionChange(sel)
	else:
		if addItems:
			addGroups = [ai for ai in addItems if isinstance(ai, Group)]
			_groups.extend(addGroups)
			if _groups and _groups == addGroups:
				# track Model deletions
				def checkGroups(tname, myData, tdata):
					if tdata.deleted:
						newGroups = []
						for group in _groups:
							group.update()
							if group.models:
								newGroups.append(group)
						_groups[:] = newGroups
						if not _groups:
							from chimera.triggerSet import ONESHOT
							return ONESHOT
				chimera.triggers.addHandler('Model', checkGroups, None)
		if removeGroups:
			for rg in removeGroups:
				_groups.remove(rg)
	return newGroup

def getGroupOf(model):
	if not _mp:
		raise RuntimeError("Model Panel not yet created")
	from Group import Group
	for item in _mp.items:
		if isinstance(item, Group):
			if model in item.models:
				return item
		elif item == model:
			return None
	raise ValueError("Model not in model panel at all")

def _saveGroups(trigName, myData, sessionFile):
	from Group import Group
	if _mp:
		# if model panel is waiting to update, cancel that and
		# immediately update
		if _mp._updateHandler:
			_mp.parent.after_cancel(_mp._updateHandler)
			_mp._doUpdate()
		groups = [g for g in _mp.items if isinstance(g, Group)]
	else:
		groups = _groups
	if not groups:
		return
	from SimpleSession import sessionID
	def _repr(grp):
		strings = []
		from ModelPanel.Group import Group
		for c in grp.components:
			if isinstance(c, Group):
				strings.append("groupCmd(%s)" % _repr(c))
			else:
				try:
					minfo = sessionID(c)
				except:
					minfo = (c.id, c.subid, c.__class__.__name__)
				strings.append("_mpGetModel(%s)" % repr(minfo))
		return "[%s], name=%s" % (", ".join(strings), repr(grp.name))
	print>>sessionFile, """
try:
	def _mpAfterModels():
		def _mpGetModel(info):
			from SimpleSession import modelMap, idLookup
			if isinstance(info, tuple) and len(info) == 3:
				id, subid, className = info
				return [m for m in modelMap[(id, subid)]
					if m.__class__.__name__ == className][0]
			return idLookup(info)
		from ModelPanel import groupCmd
"""
	for grp in groups:
		print>>sessionFile, "\t\tgroupCmd(%s)" % _repr(grp)
	print>> sessionFile, """
	registerAfterModelsCB(_mpAfterModels)
	del _mpAfterModels
except:
	reportRestoreError("Error restoring model panel groups")
"""
from SimpleSession import SAVE_SESSION
chimera.triggers.addHandler(SAVE_SESSION, _saveGroups, None)

from chimera.baseDialog import ModalDialog
class GroupNameDialog(ModalDialog):
	buttons = ("OK",)
	default = "OK"
	help = "UsersGuide/modelpanel.html#group"

	def __init__(self, defName):
		self.defName = defName
		ModalDialog.__init__(self)

	def fillInUI(self, parent):
		import Pmw
		self.nameEntry = Pmw.EntryField(parent, labelpos='w', label_text=
			"Group name:")
		self.nameEntry.setentry(self.defName)
		self.nameEntry.component('entry').select_range(0, 'end')
		self.nameEntry.component('entry').focus()
		self.nameEntry.grid(sticky="ew")

	def OK(self):
		self.Cancel(self.nameEntry.getvalue())

def getGroupName(items):
	defName = defaultGroupName(items)
	if chimera.nogui:
		return defName
	master = _mp and _mp.uiMaster().winfo_toplevel() or chimera.tkgui.app
	return GroupNameDialog(defName).run(master)

groupCounter = 0
def defaultGroupName(items):
	global groupCounter
	groupCounter += 1
	name = "Group %d" % groupCounter
	names = set([item.name for item in items])
	if len(names) == 1:
		name = names.pop()
		if not (len(name) == 4 and name[0].isdigit()
				and name[1:].isalnum()):
			# not PDB ID
			name = plural(name)
	else:
		import os.path
		prefix = os.path.commonprefix(names)
		if len(prefix) > 2:
			for n in names:
				if len(n) > len(prefix) and n[len(prefix)].isalpha():
					break
			else:
				name = plural(prefix)
	return name

def plural(text):
	if not text:
		return text
	if text[-1] == 's':
		if text[-2:] == "ss":
			return text + "es"
		else:
			return text
	if text[-1] == 'h':
		if text[-2:-1] in "cs":
			return text + "es"
		else:
			return text + "s"
	if text[-1] in "oxz":
		return text + "es"
	return text + "s"

def noteCmd(models):
	from noteDialog import NoteDialog
	NoteDialog(models)

def renameCmd(items):
	from renameDialog import RenameDialog
	RenameDialog(items)

def selectCmd(models):
	sel = chimera.selection.ItemizedSelection()
	sel.add(models)
	chimera.tkgui.selectionOperation(sel)
	
def showAllAtomsCmd(models):
	for m in models:
		if not hasattr(m, 'atoms') or not hasattr(m, 'bonds'):
			continue
		m.display = 1
		for a in m.atoms:
			a.display = 1

def surfCmd(models, category):
	import Midas
	mols = filter(lambda m: isinstance(m, chimera.Molecule), models)
	Midas.surfaceNew(category, models=mols)
	for m in mols:
		for a in m.atoms:
			if a.surfaceCategory == category:
				a.surfaceDisplay = 1

def _getModels(item):
	# VolumeModel has a 'models' attr(!), so...
	if isinstance(item, Group):
		return item.models
	return [item]