inputCore.py 25.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
#inputCore.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2010 James Teh <jamie@jantrid.net>

"""Core framework for handling input from the user.
Every piece of input from the user (e.g. a key press) is represented by an L{InputGesture}.
The singleton L{InputManager} (L{manager}) manages functionality related to input from the user.
For example, it is used to execute gestures and handle input help.
"""

import sys
import os
import itertools
import weakref
import configobj
import sayAllHandler
import baseObject
import scriptHandler
import queueHandler
import api
import speech
import characterProcessing
import config
import watchdog
from logHandler import log
import globalVars
import languageHandler
import controlTypes

#: Script category for emulated keyboard keys.
# Translators: The name of a category of NVDA commands.
SCRCAT_KBEMU = _("Emulated system keyboard keys")
#: Script category for miscellaneous commands.
# Translators: The name of a category of NVDA commands.
SCRCAT_MISC = _("Miscellaneous")
#: Script category for Browse Mode  commands.
# Translators: The name of a category of NVDA commands.
SCRCAT_BROWSEMODE = _("Browse mode")

class NoInputGestureAction(LookupError):
	"""Informs that there is no action to execute for a gesture.
	"""

class InputGesture(baseObject.AutoPropertyObject):
	"""A single gesture of input from the user.
	For example, this could be a key press on a keyboard or Braille display or a click of the mouse.
	At the very least, subclasses must implement L{_get_identifiers} and L{_get_displayName}.
	"""
	cachePropertiesByDefault = True

	#: indicates that sayAll was running before this gesture
	#: @type: bool
	wasInSayAll=False

	#: Indicates that while in Input Help Mode, this gesture should be handled as if Input Help mode was currently off.
	#: @type: bool
	bypassInputHelp=False

	#: Indicates that this gesture should be reported in Input help mode. This would only be false for floodding Gestures like touch screen hovers.
	#: @type: bool
	reportInInputHelp=True

	def _get_identifiers(self):
		"""The identifier(s) which will be used in input gesture maps to represent this gesture.
		These identifiers will be looked up in order until a match is found.
		A single identifier should take the form: C{source:id}
		where C{source} is a few characters representing the source of this gesture
		and C{id} is the specific gesture.
		If C{id} consists of multiple items with indeterminate order,
		they should be separated by a + sign and they should be in Python set order.
		Also, the entire identifier should be in lower case.
		An example identifier is: C{kb(desktop):nvda+1}
		Subclasses must implement this method.
		@return: One or more identifiers which uniquely identify this gesture.
		@rtype: list or tuple of str
		"""
		raise NotImplementedError

	def _get_logIdentifier(self):
		"""A single identifier which will be logged for this gesture.
		This identifier should be usable in input gesture maps, but should be as readable as possible to the user.
		For example, it might sort key names in a particular order
		and it might contain mixed case.
		This is in contrast to L{identifiers}, which must be normalized.
		The base implementation returns the first identifier from L{identifiers}.
		"""
		return self.identifiers[0]

	def _get_displayName(self):
		"""The name of this gesture as presented to the user.
		Subclasses must implement this method.
		@return: The display name.
		@rtype: str
		"""
		return self.getDisplayTextForIdentifier(self.identifiers[0])[1]

	#: Whether this gesture should be reported when reporting of command gestures is enabled.
	#: @type: bool
	shouldReportAsCommand = True

	#: whether this gesture represents a character being typed (i.e. not a potential command)
	#: @type bool
	isCharacter=False

	SPEECHEFFECT_CANCEL = "cancel"
	SPEECHEFFECT_PAUSE = "pause"
	SPEECHEFFECT_RESUME = "resume"
	#: The effect on speech when this gesture is executed; one of the SPEECHEFFECT_* constants or C{None}.
	speechEffectWhenExecuted = SPEECHEFFECT_CANCEL

	#: Whether this gesture is only a modifier, in which case it will not search for a script to execute.
	#: @type: bool
	isModifier = False

	def reportExtra(self):
		"""Report any extra information about this gesture to the user.
		This is called just after command gestures are reported.
		For example, it could be used to report toggle states.
		"""

	def _get_script(self):
		"""The script bound to this input gesture.
		@return: The script to be executed.
		@rtype: script function
		"""
		self.script=scriptHandler.findScript(self)
		return self.script

	def send(self):
		"""Send this gesture to the operating system.
		This is not possible for all sources.
		@raise NotImplementedError: If the source does not support sending of gestures.
		"""
		raise NotImplementedError

	def _get_scriptableObject(self):
		"""An object which contains scripts specific to this  gesture or type of gesture.
		This object will be searched for scripts before any other object when handling this gesture.
		@return: The gesture specific scriptable object or C{None} if there is none.
		@rtype: L{baseObject.ScriptableObject}
		"""
		return None

	@classmethod
	def getDisplayTextForIdentifier(cls, identifier):
		"""Get the text to be presented to the user describing a given gesture identifier.
		This should only be called for gesture identifiers associated with this class.
		Most callers will want L{inputCore.getDisplayTextForIdentifier} instead.
		The display text consists of two strings:
		the gesture's source (e.g. "laptop keyboard")
		and the specific gesture (e.g. "alt+tab").
		@param identifier: The normalized gesture identifier in question.
		@type identifier: basestring
		@return: A tuple of (source, specificGesture).
		@rtype: tuple of (basestring, basestring)
		@raise Exception: If no display text can be determined.
		"""
		raise NotImplementedError

class GlobalGestureMap(object):
	"""Maps gestures to scripts anywhere in NVDA.
	This is used to allow users and locales to bind gestures in addition to those bound by individual scriptable objects.
	Map entries will most often be loaded from a file using the L{load} method.
	See that method for details of the file format.
	"""

	def __init__(self, entries=None):
		"""Constructor.
		@param entries: Initial entries to add; see L{update} for the format.
		@type entries: mapping of str to mapping
		"""
		self._map = {}
		#: Indicates that the last load or update contained an error.
		#: @type: bool
		self.lastUpdateContainedError = False
		#: The file name for this gesture map, if any.
		#: @type: basestring
		self.fileName = None
		if entries:
			self.update(entries)

	def clear(self):
		"""Clear this map.
		"""
		self._map.clear()
		self.lastUpdateContainedError = False

	def add(self, gesture, module, className, script,replace=False):
		"""Add a gesture mapping.
		@param gesture: The gesture identifier.
		@type gesture: str
		@param module: The name of the Python module containing the target script.
		@type module: str
		@param className: The name of the class in L{module} containing the target script.
		@type className: str
		@param script: The name of the target script
			or C{None} to unbind the gesture for this class.
		@type script: str
		@param replace: if true replaces all existing bindings for this gesture with the given script, otherwise only appends this binding.
		@type replace: boolean
		"""
		gesture = normalizeGestureIdentifier(gesture)
		try:
			scripts = self._map[gesture]
		except KeyError:
			scripts = self._map[gesture] = []
		if replace:
			del scripts[:]
		scripts.append((module, className, script))

	def load(self, filename):
		"""Load map entries from a file.
		The file is an ini file.
		Each section contains entries for a particular scriptable object class.
		The section name must be the full Python module and class name.
		The key of each entry is the script name and the value is a comma separated list of one or more gestures.
		If the script name is "None", the gesture will be unbound for this class.
		For example, the following binds the "a" key to move to the next heading in virtual buffers
		and removes the default "h" binding::
			[virtualBuffers.VirtualBuffer]
			nextHeading = kb:a
			None = kb:h
		@param filename: The name of the file to load.
		@type: str
		"""
		self.fileName = filename
		try:
			conf = configobj.ConfigObj(filename, file_error=True, encoding="UTF-8")
		except (configobj.ConfigObjError,UnicodeDecodeError), e:
			log.warning("Error in gesture map '%s': %s"%(filename, e))
			self.lastUpdateContainedError = True
			return
		self.update(conf)

	def update(self, entries):
		"""Add multiple map entries.
		C{entries} must be a mapping of mappings.
		Each inner mapping contains entries for a particular scriptable object class.
		The key in the outer mapping must be the full Python module and class name.
		The key of each entry in the inner mappings is the script name and the value is a list of one or more gestures.
		If the script name is C{None}, the gesture will be unbound for this class.
		For example, the following binds the "a" key to move to the next heading in virtual buffers
		and removes the default "h" binding::
			{
				"virtualBuffers.VirtualBuffer": {
					"nextHeading": "kb:a",
					None: "kb:h",
				}
			}
		@param entries: The items to add.
		@type entries: mapping of str to mapping
		"""
		self.lastUpdateContainedError = False
		for locationName, location in entries.iteritems():
			try:
				module, className = locationName.rsplit(".", 1)
			except:
				log.error("Invalid module/class specification: %s" % locationName)
				self.lastUpdateContainedError = True
				continue
			for script, gestures in location.iteritems():
				if script == "None":
					script = None
				if gestures == "":
					gestures = ()
				elif isinstance(gestures, basestring):
					gestures = [gestures]
				for gesture in gestures:
					try:
						self.add(gesture, module, className, script)
					except:
						log.error("Invalid gesture: %s" % gesture)
						self.lastUpdateContainedError = True
						continue

	def getScriptsForGesture(self, gesture):
		"""Get the scripts associated with a particular gesture.
		@param gesture: The gesture identifier.
		@type gesture: str
		@return: The Python class and script name for each script;
			the script name may be C{None} indicating that the gesture should be unbound for this class.
		@rtype: generator of (class, str)
		"""
		try:
			scripts = self._map[gesture]
		except KeyError:
			return
		for moduleName, className, scriptName in scripts:
			try:
				module = sys.modules[moduleName]
			except KeyError:
				continue
			try:
				cls = getattr(module, className)
			except AttributeError:
				continue
			yield cls, scriptName

	def getScriptsForAllGestures(self):
		"""Get all of the scripts and their gestures.
		@return: The Python class, gesture and script name for each mapping;
			the script name may be C{None} indicating that the gesture should be unbound for this class.
		@rtype: generator of (class, str, str)
		"""
		for gesture in self._map:
			for cls, scriptName in self.getScriptsForGesture(gesture):
				yield cls, gesture, scriptName

	def remove(self, gesture, module, className, script):
		"""Remove a gesture mapping.
		@param gesture: The gesture identifier.
		@type gesture: str
		@param module: The name of the Python module containing the target script.
		@type module: str
		@param className: The name of the class in L{module} containing the target script.
		@type className: str
		@param script: The name of the target script.
		@type script: str
		@raise ValueError: If the requested mapping does not exist.
		"""
		gesture = normalizeGestureIdentifier(gesture)
		try:
			scripts = self._map[gesture]
		except KeyError:
			raise ValueError("Mapping not found")
		scripts.remove((module, className, script))

	def save(self):
		"""Save this gesture map to disk.
		@precondition: L{load} must have been called.
		"""
		if globalVars.appArgs.secure:
			return
		if not self.fileName:
			raise ValueError("No file name")
		out = configobj.ConfigObj(encoding="UTF-8")
		out.filename = self.fileName

		for gesture, scripts in self._map.iteritems():
			for module, className, script in scripts:
				key = "%s.%s" % (module, className)
				try:
					outSect = out[key]
				except KeyError:
					out[key] = {}
					outSect = out[key]
				if script is None:
					script = "None"
				try:
					outVal = outSect[script]
				except KeyError:
					# Write the first value as a string so configobj doesn't output a comma if there's only one value.
					outVal = outSect[script] = gesture
				else:
					if isinstance(outVal, list):
						outVal.append(gesture)
					else:
						outSect[script] = [outVal, gesture]

		out.write()

class InputManager(baseObject.AutoPropertyObject):
	"""Manages functionality related to input from the user.
	Input includes key presses on the keyboard, as well as key presses on Braille displays, etc.
	"""

	#: a modifier gesture was just executed while sayAll was running
	#: @type: bool
	lastModifierWasInSayAll=False

	def __init__(self):
		#: The function to call when capturing gestures.
		#: If it returns C{False}, normal execution will be prevented.
		#: @type: callable
		self._captureFunc = None
		#: The gestures mapped for the NVDA locale.
		#: @type: L{GlobalGestureMap}
		self.localeGestureMap = GlobalGestureMap()
		#: The gestures mapped by the user.
		#: @type: L{GlobalGestureMap}
		self.userGestureMap = GlobalGestureMap()
		self.loadLocaleGestureMap()
		self.loadUserGestureMap()

	def executeGesture(self, gesture):
		"""Perform the action associated with a gesture.
		@param gesture: The gesture to execute.
		@type gesture: L{InputGesture}
		@raise NoInputGestureAction: If there is no action to perform.
		"""
		if watchdog.isAttemptingRecovery:
			# The core is dead, so don't try to perform an action.
			# This lets gestures pass through unhindered where possible,
			# as well as stopping a flood of actions when the core revives.
			raise NoInputGestureAction

		script = gesture.script
		focus = api.getFocusObject()
		if focus.sleepMode is focus.SLEEP_FULL or (focus.sleepMode and not getattr(script, 'allowInSleepMode', False)):
			raise NoInputGestureAction

		wasInSayAll=False
		if gesture.isModifier:
			if not self.lastModifierWasInSayAll:
				wasInSayAll=self.lastModifierWasInSayAll=sayAllHandler.isRunning()
		elif self.lastModifierWasInSayAll:
			wasInSayAll=True
			self.lastModifierWasInSayAll=False
		else:
			wasInSayAll=sayAllHandler.isRunning()
		if wasInSayAll:
			gesture.wasInSayAll=True

		speechEffect = gesture.speechEffectWhenExecuted
		if speechEffect == gesture.SPEECHEFFECT_CANCEL:
			queueHandler.queueFunction(queueHandler.eventQueue, speech.cancelSpeech)
		elif speechEffect in (gesture.SPEECHEFFECT_PAUSE, gesture.SPEECHEFFECT_RESUME):
			queueHandler.queueFunction(queueHandler.eventQueue, speech.pauseSpeech, speechEffect == gesture.SPEECHEFFECT_PAUSE)

		if log.isEnabledFor(log.IO) and not gesture.isModifier:
			log.io("Input: %s" % gesture.logIdentifier)

		if self._captureFunc:
			try:
				if self._captureFunc(gesture) is False:
					return
			except:
				log.error("Error in capture function, disabling", exc_info=True)
				self._captureFunc = None

		if gesture.isModifier:
			raise NoInputGestureAction

		if config.conf["keyboard"]["speakCommandKeys"] and gesture.shouldReportAsCommand:
			queueHandler.queueFunction(queueHandler.eventQueue, speech.speakMessage, gesture.displayName)

		gesture.reportExtra()

		# #2953: if an intercepted command Script (script that sends a gesture) is queued
		# then queue all following gestures (that don't have a script) with a fake script so that they remain in order.
		if not script and scriptHandler._numIncompleteInterceptedCommandScripts:
			script=lambda gesture: gesture.send()


		if script:
			scriptHandler.queueScript(script, gesture)
			return

		raise NoInputGestureAction

	def _get_isInputHelpActive(self):
		"""Whether input help is enabled, wherein the function of each key pressed by the user is reported but not executed.
		@rtype: bool
		"""
		return self._captureFunc == self._inputHelpCaptor

	def _set_isInputHelpActive(self, enable):
		if enable:
			self._captureFunc = self._inputHelpCaptor
		elif self.isInputHelpActive:
			self._captureFunc = None

	def _inputHelpCaptor(self, gesture):
		bypass = gesture.bypassInputHelp or getattr(gesture.script, "bypassInputHelp", False)
		queueHandler.queueFunction(queueHandler.eventQueue, self._handleInputHelp, gesture, onlyLog=bypass or not gesture.reportInInputHelp)
		return bypass

	def _handleInputHelp(self, gesture, onlyLog=False):
		textList = [gesture.displayName]
		script = gesture.script
		runScript = False
		logMsg = "Input help: gesture %s"%gesture.logIdentifier
		if script:
			scriptName = scriptHandler.getScriptName(script)
			logMsg+=", bound to script %s" % scriptName
			scriptLocation = scriptHandler.getScriptLocation(script)
			if scriptLocation:
				logMsg += " on %s" % scriptLocation
			if scriptName == "toggleInputHelp":
				runScript = True
			else:
				desc = script.__doc__
				if desc:
					textList.append(desc)

		log.info(logMsg)
		if onlyLog:
			return

		import braille
		braille.handler.message("\t\t".join(textList))
		# Punctuation must be spoken for the gesture name (the first chunk) so that punctuation keys are spoken.
		speech.speakText(textList[0], reason=controlTypes.REASON_MESSAGE, symbolLevel=characterProcessing.SYMLVL_ALL)
		for text in textList[1:]:
			speech.speakMessage(text)

		if runScript:
			script(gesture)

	def loadUserGestureMap(self):
		self.userGestureMap.clear()
		try:
			self.userGestureMap.load(os.path.join(globalVars.appArgs.configPath, "gestures.ini"))
		except IOError:
			log.debugWarning("No user gesture map")

	def loadLocaleGestureMap(self):
		self.localeGestureMap.clear()
		lang = languageHandler.getLanguage()
		try:
			self.localeGestureMap.load(os.path.join("locale", lang, "gestures.ini"))
		except IOError:
			try:
				self.localeGestureMap.load(os.path.join("locale", lang.split('_')[0], "gestures.ini"))
			except IOError:
				log.debugWarning("No locale gesture map for language %s" % lang)

	def emulateGesture(self, gesture):
		"""Convenience method to emulate a gesture.
		First, an attempt will be made to execute the gesture using L{executeGesture}.
		If that fails, the gesture will be sent to the operating system if possible using L{InputGesture.send}.
		@param gesture: The gesture to execute.
		@type gesture: L{InputGesture}
		"""
		try:
			return self.executeGesture(gesture)
		except NoInputGestureAction:
			pass
		try:
			gesture.send()
		except NotImplementedError:
			pass

	def getAllGestureMappings(self, obj=None, ancestors=None):
		if not obj:
			obj = api.getFocusObject()
			ancestors = api.getFocusAncestors()
		return _AllGestureMappingsRetriever(obj, ancestors).results

class _AllGestureMappingsRetriever(object):

	def __init__(self, obj, ancestors):
		self.results = {}
		self.scriptInfo = {}
		self.handledGestures = set()

		self.addGlobalMap(manager.userGestureMap)
		self.addGlobalMap(manager.localeGestureMap)
		import braille
		gmap = braille.handler.display.gestureMap
		if gmap:
			self.addGlobalMap(gmap)

		# Global plugins.
		import globalPluginHandler
		for plugin in globalPluginHandler.runningPlugins:
			self.addObj(plugin)

		# App module.
		app = obj.appModule
		if app:
			self.addObj(app)

		# Tree interceptor.
		ti = obj.treeInterceptor
		if ti:
			self.addObj(ti)

		# NVDAObject.
		self.addObj(obj)
		for anc in reversed(ancestors):
			self.addObj(anc, isAncestor=True)

		# Global commands.
		import globalCommands
		self.addObj(globalCommands.commands)

	def addResult(self, scriptInfo):
		self.scriptInfo[scriptInfo.cls, scriptInfo.scriptName] = scriptInfo
		try:
			cat = self.results[scriptInfo.category]
		except KeyError:
			cat = self.results[scriptInfo.category] = {}
		cat[scriptInfo.displayName] = scriptInfo

	def addGlobalMap(self, gmap):
		for cls, gesture, scriptName in gmap.getScriptsForAllGestures():
			key = (cls, gesture)
			if key in self.handledGestures:
				continue
			self.handledGestures.add(key)
			if scriptName is None:
				# The global map specified that no script should execute for this gesture and object.
				continue
			try:
				scriptInfo = self.scriptInfo[cls, scriptName]
			except KeyError:
				if scriptName.startswith("kb:"):
					scriptInfo = self.makeKbEmuScriptInfo(cls, scriptName)
				else:
					try:
						script = getattr(cls, "script_%s" % scriptName)
					except AttributeError:
						continue
					scriptInfo = self.makeNormalScriptInfo(cls, scriptName, script)
					if not scriptInfo:
						continue
				self.addResult(scriptInfo)
			scriptInfo.gestures.append(gesture)

	def makeKbEmuScriptInfo(self, cls, scriptName):
		info = AllGesturesScriptInfo(cls, scriptName)
		info.category = SCRCAT_KBEMU
		info.displayName = scriptName[3:]
		return info

	def makeNormalScriptInfo(self, cls, scriptName, script):
		info = AllGesturesScriptInfo(cls, scriptName)
		info.category = self.getScriptCategory(cls, script)
		info.displayName = script.__doc__
		if not info.displayName:
			return None
		return info

	def getScriptCategory(self, cls, script):
		try:
			return script.category
		except AttributeError:
			pass
		try:
			return cls.scriptCategory
		except AttributeError:
			pass
		return SCRCAT_MISC

	def addObj(self, obj, isAncestor=False):
		scripts = {}
		for cls in obj.__class__.__mro__:
			for scriptName, script in cls.__dict__.iteritems():
				if not scriptName.startswith("script_"):
					continue
				if isAncestor and not getattr(script, "canPropagate", False):
					continue
				scriptName = scriptName[7:]
				try:
					scriptInfo = self.scriptInfo[cls, scriptName]
				except KeyError:
					scriptInfo = self.makeNormalScriptInfo(cls, scriptName, script)
					if not scriptInfo:
						continue
					self.addResult(scriptInfo)
				scripts[script] = scriptInfo
		for gesture, script in obj._gestureMap.iteritems():
			try:
				scriptInfo = scripts[script.__func__]
			except KeyError:
				continue
			key = (scriptInfo.cls, gesture)
			if key in self.handledGestures:
				continue
			self.handledGestures.add(key)
			scriptInfo.gestures.append(gesture)

class AllGesturesScriptInfo(object):
	__slots__ = ("cls", "scriptName", "category", "displayName", "gestures")
	
	def __init__(self, cls, scriptName):
		self.cls = cls
		self.scriptName = scriptName
		self.gestures = []

	@property
	def moduleName(self):
		return self.cls.__module__

	@property
	def className(self):
		return self.cls.__name__

def normalizeGestureIdentifier(identifier):
	"""Normalize a gesture identifier so that it matches other identifiers for the same gesture.
	Any items separated by a + sign after the source are considered to be of indeterminate order
	and are reordered into Python set ordering.
	Then the entire identifier is converted to lower case.
	"""
	prefix, main = identifier.split(":", 1)
	main = main.split("+")
	# The order of the parts doesn't matter as far as the user is concerned,
	# but we need them to be in a determinate order so they will match other gesture identifiers.
	# We use Python's set ordering.
	main = "+".join(frozenset(main))
	return u"{0}:{1}".format(prefix, main).lower()

#: Maps registered source prefix strings to L{InputGesture} classes.
gestureSources = weakref.WeakValueDictionary()

def registerGestureSource(source, gestureCls):
	"""Register an input gesture class for a source prefix string.
	The specified gesture class will be used for queries regarding all gesture identifiers with the given source prefix.
	For example, if "kb" is registered with the C{KeyboardInputGesture} class,
	any queries for "kb:tab" or "kb(desktop):tab" will be directed to the C{KeyboardInputGesture} class.
	If there is no exact match for the source, any parenthesised portion is stripped.
	For example, for "br(baum):d1", if "br(baum)" isn't registered,
	"br" will be used if it is registered.
	This registration is used, for example, to get the display text for a gesture identifier.
	@param source: The source prefix for associated gesture identifiers.
	@type source: basestring
	@param gestureCls: The input gesture class.
	@type gestureCls: L{InputGesture}
	"""
	gestureSources[source] = gestureCls

def _getGestureClsForIdentifier(identifier):
	"""Get the registered gesture class for an identifier.
	"""
	source = identifier.split(":", 1)[0]
	try:
		return gestureSources[source]
	except KeyError:
		pass
	genSource = source.split("(", 1)[0]
	if genSource:
		try:
			return gestureSources[genSource]
		except KeyError:
			pass
	raise LookupError("Gesture source not registered: %s" % source)

def getDisplayTextForGestureIdentifier(identifier):
	"""Get the text to be presented to the user describing a given gesture identifier.
	The display text consists of two strings:
	the gesture's source (e.g. "laptop keyboard")
	and the specific gesture (e.g. "alt+tab").
	@param identifier: The normalized gesture identifier in question.
	@type identifier: basestring
	@return: A tuple of (source, specificGesture).
	@rtype: tuple of (basestring, basestring)
	@raise LookupError: If no display text can be determined.
	"""
	gcls = _getGestureClsForIdentifier(identifier)
	try:
		return gcls.getDisplayTextForIdentifier(identifier)
	except:
		raise
		raise LookupError("Couldn't get display text for identifier: %s" % identifier)

#: The singleton input manager instance.
#: @type: L{InputManager}
manager = None

def initialize():
	"""Initializes input core, creating a global L{InputManager} singleton.
	"""
	global manager
	manager=InputManager()

def terminate():
	"""Terminates input core.
	"""
	global manager
	manager=None