942 lines
34 KiB
Python
942 lines
34 KiB
Python
# Orca
|
|
#
|
|
# Copyright 2010-2011 The Orca Team
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the
|
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
# Boston MA 02110-1301 USA.
|
|
|
|
"""Implements generic chat support."""
|
|
|
|
__id__ = "$Id$"
|
|
__version__ = "$Revision$"
|
|
__date__ = "$Date$"
|
|
__copyright__ = "Copyright (c) 2010-2011 The Orca Team"
|
|
__license__ = "LGPL"
|
|
|
|
import pyatspi
|
|
|
|
from . import cmdnames
|
|
from . import debug
|
|
from . import guilabels
|
|
from . import input_event
|
|
from . import keybindings
|
|
from . import messages
|
|
from . import orca_state
|
|
from . import settings
|
|
from . import settings_manager
|
|
|
|
_settingsManager = settings_manager.getManager()
|
|
|
|
#############################################################################
|
|
# #
|
|
# Ring List. A fixed size circular list by Flavio Catalani #
|
|
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/435902 #
|
|
# #
|
|
# Included here to keep track of conversation histories. #
|
|
# #
|
|
#############################################################################
|
|
|
|
class RingList:
|
|
def __init__(self, length):
|
|
self.__data__ = []
|
|
self.__full__ = 0
|
|
self.__max__ = length
|
|
self.__cur__ = 0
|
|
|
|
def append(self, x):
|
|
if self.__full__ == 1:
|
|
for i in range (0, self.__cur__ - 1):
|
|
self.__data__[i] = self.__data__[i + 1]
|
|
self.__data__[self.__cur__ - 1] = x
|
|
else:
|
|
self.__data__.append(x)
|
|
self.__cur__ += 1
|
|
if self.__cur__ == self.__max__:
|
|
self.__full__ = 1
|
|
|
|
def get(self):
|
|
return self.__data__
|
|
|
|
def remove(self):
|
|
if (self.__cur__ > 0):
|
|
del self.__data__[self.__cur__ - 1]
|
|
self.__cur__ -= 1
|
|
|
|
def size(self):
|
|
return self.__cur__
|
|
|
|
def maxsize(self):
|
|
return self.__max__
|
|
|
|
def __str__(self):
|
|
return ''.join(self.__data__)
|
|
|
|
#############################################################################
|
|
# #
|
|
# Conversation #
|
|
# #
|
|
#############################################################################
|
|
|
|
class Conversation:
|
|
|
|
# The number of messages to keep in the history
|
|
#
|
|
MESSAGE_LIST_LENGTH = 9
|
|
|
|
def __init__(self, name, accHistory, inputArea=None):
|
|
|
|
"""Creates a new instance of the Conversation class.
|
|
|
|
Arguments:
|
|
- name: the chatroom/conversation name
|
|
- accHistory: the accessible which holds the conversation history
|
|
- inputArea: the editable text object for this conversation.
|
|
"""
|
|
|
|
self.name = name
|
|
self.accHistory = accHistory
|
|
self.inputArea = inputArea
|
|
|
|
# A cyclic list to hold the chat room history for this conversation
|
|
#
|
|
self._messageHistory = RingList(Conversation.MESSAGE_LIST_LENGTH)
|
|
|
|
# Initially populate the cyclic lists with empty strings.
|
|
#
|
|
i = 0
|
|
while i < self._messageHistory.maxsize():
|
|
self.addMessage("")
|
|
i += 1
|
|
|
|
# Keep track of the last typing status because some platforms (e.g.
|
|
# MSN) seem to issue the status constantly and even though it has
|
|
# not changed.
|
|
#
|
|
self._typingStatus = ""
|
|
|
|
def addMessage(self, message):
|
|
"""Adds the current message to the message history.
|
|
|
|
Arguments:
|
|
- message: A string containing the message to add
|
|
"""
|
|
|
|
self._messageHistory.append(message)
|
|
|
|
def getNthMessage(self, messageNumber):
|
|
"""Returns the specified message from the message history.
|
|
|
|
Arguments:
|
|
- messageNumber: the index of the message to get.
|
|
"""
|
|
|
|
messages = self._messageHistory.get()
|
|
|
|
return messages[messageNumber]
|
|
|
|
def getTypingStatus(self):
|
|
"""Returns the typing status of the buddy in this conversation."""
|
|
|
|
return self._typingStatus
|
|
|
|
def setTypingStatus(self, status):
|
|
"""Sets the typing status of the buddy in this conversation.
|
|
|
|
Arguments:
|
|
- status: a string describing the current status.
|
|
"""
|
|
|
|
self._typingStatus = status
|
|
|
|
#############################################################################
|
|
# #
|
|
# ConversationList #
|
|
# #
|
|
#############################################################################
|
|
|
|
class ConversationList:
|
|
|
|
def __init__(self, messageListLength):
|
|
|
|
"""Creates a new instance of the ConversationList class.
|
|
|
|
Arguments:
|
|
- messageListLength: the size of the message history to keep.
|
|
"""
|
|
|
|
self.conversations = []
|
|
|
|
# A cyclic list to hold the most recent (messageListLength) previous
|
|
# messages for all conversations in the ConversationList.
|
|
#
|
|
self._messageHistory = RingList(messageListLength)
|
|
|
|
# A corresponding cyclic list to hold the name of the conversation
|
|
# associated with each message in the messageHistory.
|
|
#
|
|
self._roomHistory = RingList(messageListLength)
|
|
|
|
# Initially populate the cyclic lists with empty strings.
|
|
#
|
|
i = 0
|
|
while i < self._messageHistory.maxsize():
|
|
self.addMessage("", None)
|
|
i += 1
|
|
|
|
def addMessage(self, message, conversation):
|
|
"""Adds the current message to the message history.
|
|
|
|
Arguments:
|
|
- message: A string containing the message to add
|
|
- conversation: The instance of the Conversation class with which
|
|
the message is associated
|
|
"""
|
|
|
|
if not conversation:
|
|
name = ""
|
|
else:
|
|
if not self.hasConversation(conversation):
|
|
self.addConversation(conversation)
|
|
name = conversation.name
|
|
|
|
self._messageHistory.append(message)
|
|
self._roomHistory.append(name)
|
|
|
|
def getNthMessageAndName(self, messageNumber):
|
|
"""Returns a list containing the specified message from the message
|
|
history and the name of the chatroom/conversation associated with
|
|
that message.
|
|
|
|
Arguments:
|
|
- messageNumber: the index of the message to get.
|
|
"""
|
|
|
|
messages = self._messageHistory.get()
|
|
rooms = self._roomHistory.get()
|
|
|
|
return messages[messageNumber], rooms[messageNumber]
|
|
|
|
def hasConversation(self, conversation):
|
|
"""Returns True if we know about this conversation.
|
|
|
|
Arguments:
|
|
- conversation: the conversation of interest
|
|
"""
|
|
|
|
return conversation in self.conversations
|
|
|
|
def getNConversations(self):
|
|
"""Returns the number of conversations we currently know about."""
|
|
|
|
return len(self.conversations)
|
|
|
|
def addConversation(self, conversation):
|
|
"""Adds conversation to the list of conversations.
|
|
|
|
Arguments:
|
|
- conversation: the conversation to add
|
|
"""
|
|
|
|
self.conversations.append(conversation)
|
|
|
|
def removeConversation(self, conversation):
|
|
"""Removes conversation from the list of conversations.
|
|
|
|
Arguments:
|
|
- conversation: the conversation to remove
|
|
|
|
Returns True if conversation was successfully removed.
|
|
"""
|
|
|
|
# TODO - JD: In the Pidgin script, I do not believe we handle the
|
|
# case where a conversation window is closed. I *think* it remains
|
|
# in the overall chat history. What do we want to do in that case?
|
|
# I would assume that we'd want to remove it.... So here's a method
|
|
# to do so. Nothing in the Chat class uses it yet.
|
|
#
|
|
try:
|
|
self.conversations.remove(conversation)
|
|
except:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
#############################################################################
|
|
# #
|
|
# Chat #
|
|
# #
|
|
#############################################################################
|
|
|
|
class Chat:
|
|
"""This class implements the chat functionality which is available to
|
|
scripts.
|
|
"""
|
|
|
|
def __init__(self, script, buddyListAncestries):
|
|
"""Creates an instance of the Chat class.
|
|
|
|
Arguments:
|
|
- script: the script with which this instance is associated.
|
|
- buddyListAncestries: a list of lists of pyatspi roles beginning
|
|
with the object serving as the actual buddy list (e.g.
|
|
ROLE_TREE_TABLE) and ending with the top level object (e.g.
|
|
ROLE_FRAME).
|
|
"""
|
|
|
|
self._script = script
|
|
self._buddyListAncestries = buddyListAncestries
|
|
|
|
# Keybindings to provide conversation message history. The message
|
|
# review order will be based on the index within the list. Thus F1
|
|
# is associated with the most recent message, F2 the message before
|
|
# that, and so on. A script could override this. Setting messageKeys
|
|
# to ["a", "b", "c" ... ] will cause "a" to be associated with the
|
|
# most recent message, "b" to be associated with the message before
|
|
# that, etc. Scripts can also override the messageKeyModifier.
|
|
#
|
|
self.messageKeys = \
|
|
["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9"]
|
|
self.messageKeyModifier = keybindings.ORCA_MODIFIER_MASK
|
|
self.inputEventHandlers = {}
|
|
self.setupInputEventHandlers()
|
|
self.keyBindings = self.getKeyBindings()
|
|
|
|
# The length of the message history will be based on how many keys
|
|
# are bound to the task of providing it.
|
|
#
|
|
self.messageListLength = len(self.messageKeys)
|
|
self._conversationList = ConversationList(self.messageListLength)
|
|
|
|
# To make pylint happy.
|
|
#
|
|
self.focusedChannelRadioButton = None
|
|
self.allChannelsRadioButton = None
|
|
self.allMessagesRadioButton = None
|
|
self.buddyTypingCheckButton = None
|
|
self.chatRoomHistoriesCheckButton = None
|
|
self.speakNameCheckButton = None
|
|
|
|
def setupInputEventHandlers(self):
|
|
"""Defines InputEventHandler fields for chat functions which
|
|
will be used by the script associated with this chat instance."""
|
|
|
|
self.inputEventHandlers["togglePrefixHandler"] = \
|
|
input_event.InputEventHandler(
|
|
self.togglePrefix,
|
|
cmdnames.CHAT_TOGGLE_ROOM_NAME_PREFIX)
|
|
|
|
self.inputEventHandlers["toggleBuddyTypingHandler"] = \
|
|
input_event.InputEventHandler(
|
|
self.toggleBuddyTyping,
|
|
cmdnames.CHAT_TOGGLE_BUDDY_TYPING)
|
|
|
|
self.inputEventHandlers["toggleMessageHistoriesHandler"] = \
|
|
input_event.InputEventHandler(
|
|
self.toggleMessageHistories,
|
|
cmdnames.CHAT_TOGGLE_MESSAGE_HISTORIES)
|
|
|
|
self.inputEventHandlers["reviewMessage"] = \
|
|
input_event.InputEventHandler(
|
|
self.readPreviousMessage,
|
|
cmdnames.CHAT_PREVIOUS_MESSAGE)
|
|
|
|
return
|
|
|
|
def getKeyBindings(self):
|
|
"""Defines the chat-related key bindings which will be used by
|
|
the script associated with this chat instance.
|
|
|
|
Returns: an instance of keybindings.KeyBindings.
|
|
"""
|
|
|
|
keyBindings = keybindings.KeyBindings()
|
|
|
|
keyBindings.add(
|
|
keybindings.KeyBinding(
|
|
"",
|
|
keybindings.defaultModifierMask,
|
|
keybindings.NO_MODIFIER_MASK,
|
|
self.inputEventHandlers["togglePrefixHandler"]))
|
|
|
|
keyBindings.add(
|
|
keybindings.KeyBinding(
|
|
"",
|
|
keybindings.defaultModifierMask,
|
|
keybindings.NO_MODIFIER_MASK,
|
|
self.inputEventHandlers["toggleBuddyTypingHandler"]))
|
|
|
|
keyBindings.add(
|
|
keybindings.KeyBinding(
|
|
"",
|
|
keybindings.defaultModifierMask,
|
|
keybindings.NO_MODIFIER_MASK,
|
|
self.inputEventHandlers["toggleMessageHistoriesHandler"]))
|
|
|
|
for messageKey in self.messageKeys:
|
|
keyBindings.add(
|
|
keybindings.KeyBinding(
|
|
messageKey,
|
|
self.messageKeyModifier,
|
|
keybindings.ORCA_MODIFIER_MASK,
|
|
self.inputEventHandlers["reviewMessage"]))
|
|
|
|
return keyBindings
|
|
|
|
def getAppPreferencesGUI(self):
|
|
"""Return a GtkGrid containing the application unique configuration
|
|
GUI items for the current application. """
|
|
|
|
from gi.repository import Gtk
|
|
|
|
grid = Gtk.Grid()
|
|
grid.set_border_width(12)
|
|
|
|
label = guilabels.CHAT_SPEAK_ROOM_NAME
|
|
value = _settingsManager.getSetting('chatSpeakRoomName')
|
|
self.speakNameCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
|
|
self.speakNameCheckButton.set_active(value)
|
|
grid.attach(self.speakNameCheckButton, 0, 0, 1, 1)
|
|
|
|
label = guilabels.CHAT_ANNOUNCE_BUDDY_TYPING
|
|
value = _settingsManager.getSetting('chatAnnounceBuddyTyping')
|
|
self.buddyTypingCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
|
|
self.buddyTypingCheckButton.set_active(value)
|
|
grid.attach(self.buddyTypingCheckButton, 0, 1, 1, 1)
|
|
|
|
label = guilabels.CHAT_SEPARATE_MESSAGE_HISTORIES
|
|
value = _settingsManager.getSetting('chatRoomHistories')
|
|
self.chatRoomHistoriesCheckButton = \
|
|
Gtk.CheckButton.new_with_mnemonic(label)
|
|
self.chatRoomHistoriesCheckButton.set_active(value)
|
|
grid.attach(self.chatRoomHistoriesCheckButton, 0, 2, 1, 1)
|
|
|
|
messagesFrame = Gtk.Frame()
|
|
grid.attach(messagesFrame, 0, 3, 1, 1)
|
|
label = Gtk.Label("<b>%s</b>" % guilabels.CHAT_SPEAK_MESSAGES_FROM)
|
|
label.set_use_markup(True)
|
|
messagesFrame.set_label_widget(label)
|
|
|
|
messagesAlignment = Gtk.Alignment.new(0.5, 0.5, 1, 1)
|
|
messagesAlignment.set_padding(0, 0, 12, 0)
|
|
messagesFrame.add(messagesAlignment)
|
|
messagesGrid = Gtk.Grid()
|
|
messagesAlignment.add(messagesGrid)
|
|
|
|
value = _settingsManager.getSetting('chatMessageVerbosity')
|
|
|
|
label = guilabels.CHAT_SPEAK_MESSAGES_ALL
|
|
rb1 = Gtk.RadioButton.new_with_mnemonic(None, label)
|
|
rb1.set_active(value == settings.CHAT_SPEAK_ALL)
|
|
self.allMessagesRadioButton = rb1
|
|
messagesGrid.attach(self.allMessagesRadioButton, 0, 0, 1, 1)
|
|
|
|
label = guilabels.CHAT_SPEAK_MESSAGES_ACTIVE
|
|
rb2 = Gtk.RadioButton.new_with_mnemonic(None, label)
|
|
rb2.join_group(rb1)
|
|
rb2.set_active(value == settings.CHAT_SPEAK_FOCUSED_CHANNEL)
|
|
self.focusedChannelRadioButton = rb2
|
|
messagesGrid.attach(self.focusedChannelRadioButton, 0, 1, 1, 1)
|
|
|
|
label = guilabels.CHAT_SPEAK_MESSAGES_ALL_IF_FOCUSED % \
|
|
self._script.app.name
|
|
rb3 = Gtk.RadioButton.new_with_mnemonic(None, label)
|
|
rb3.join_group(rb1)
|
|
rb3.set_active(value == settings.CHAT_SPEAK_ALL_IF_FOCUSED)
|
|
self.allChannelsRadioButton = rb3
|
|
messagesGrid.attach(self.allChannelsRadioButton, 0, 2, 1, 1)
|
|
|
|
grid.show_all()
|
|
|
|
return grid
|
|
|
|
def getPreferencesFromGUI(self):
|
|
"""Returns a dictionary with the app-specific preferences."""
|
|
|
|
if self.allChannelsRadioButton.get_active():
|
|
verbosity = settings.CHAT_SPEAK_ALL_IF_FOCUSED
|
|
elif self.focusedChannelRadioButton.get_active():
|
|
verbosity = settings.CHAT_SPEAK_FOCUSED_CHANNEL
|
|
else:
|
|
verbosity = settings.CHAT_SPEAK_ALL
|
|
|
|
return {
|
|
'chatMessageVerbosity': verbosity,
|
|
'chatSpeakRoomName': self.speakNameCheckButton.get_active(),
|
|
'chatAnnounceBuddyTyping': self.buddyTypingCheckButton.get_active(),
|
|
'chatRoomHistories': self.chatRoomHistoriesCheckButton.get_active(),
|
|
}
|
|
|
|
########################################################################
|
|
# #
|
|
# InputEvent handlers and supporting utilities #
|
|
# #
|
|
########################################################################
|
|
|
|
def togglePrefix(self, script, inputEvent):
|
|
""" Toggle whether we prefix chat room messages with the name of
|
|
the chat room.
|
|
|
|
Arguments:
|
|
- script: the script associated with this event
|
|
- inputEvent: if not None, the input event that caused this action.
|
|
"""
|
|
|
|
line = messages.CHAT_ROOM_NAME_PREFIX_ON
|
|
speakRoomName = _settingsManager.getSetting('chatSpeakRoomName')
|
|
_settingsManager.setSetting('chatSpeakRoomName', not speakRoomName)
|
|
if speakRoomName:
|
|
line = messages.CHAT_ROOM_NAME_PREFIX_OFF
|
|
self._script.presentMessage(line)
|
|
|
|
return True
|
|
|
|
def toggleBuddyTyping(self, script, inputEvent):
|
|
""" Toggle whether we announce when our buddies are typing a message.
|
|
|
|
Arguments:
|
|
- script: the script associated with this event
|
|
- inputEvent: if not None, the input event that caused this action.
|
|
"""
|
|
|
|
line = messages.CHAT_BUDDY_TYPING_ON
|
|
announceTyping = _settingsManager.getSetting('chatAnnounceBuddyTyping')
|
|
_settingsManager.setSetting(
|
|
'chatAnnounceBuddyTyping', not announceTyping)
|
|
if announceTyping:
|
|
line = messages.CHAT_BUDDY_TYPING_OFF
|
|
self._script.presentMessage(line)
|
|
|
|
return True
|
|
|
|
def toggleMessageHistories(self, script, inputEvent):
|
|
""" Toggle whether we provide chat room specific message histories.
|
|
|
|
Arguments:
|
|
- script: the script associated with this event
|
|
- inputEvent: if not None, the input event that caused this action.
|
|
"""
|
|
|
|
line = messages.CHAT_SEPARATE_HISTORIES_ON
|
|
roomHistories = _settingsManager.getSetting('chatRoomHistories')
|
|
_settingsManager.setSetting('chatRoomHistories', not roomHistories)
|
|
if roomHistories:
|
|
line = messages.CHAT_SEPARATE_HISTORIES_OFF
|
|
self._script.presentMessage(line)
|
|
|
|
return True
|
|
|
|
def readPreviousMessage(self, script, inputEvent=None, index=0):
|
|
""" Speak/braille a previous chat room message.
|
|
|
|
Arguments:
|
|
- script: the script associated with this event
|
|
- inputEvent: if not None, the input event that caused this action.
|
|
- index: The index of the message to read -- by default, the most
|
|
recent message. If we get an inputEvent, however, the value of
|
|
index is ignored and the index of the event_string with respect
|
|
to self.messageKeys is used instead.
|
|
"""
|
|
|
|
try:
|
|
index = self.messageKeys.index(inputEvent.event_string)
|
|
except:
|
|
pass
|
|
|
|
messageNumber = self.messageListLength - (index + 1)
|
|
message, chatRoomName = None, None
|
|
|
|
if _settingsManager.getSetting('chatRoomHistories'):
|
|
conversation = self.getConversation(orca_state.locusOfFocus)
|
|
if conversation:
|
|
message = conversation.getNthMessage(messageNumber)
|
|
chatRoomName = conversation.name
|
|
else:
|
|
message, chatRoomName = \
|
|
self._conversationList.getNthMessageAndName(messageNumber)
|
|
|
|
if message and chatRoomName:
|
|
self.utterMessage(chatRoomName, message, True)
|
|
|
|
def utterMessage(self, chatRoomName, message, focused=True):
|
|
""" Speak/braille a chat room message.
|
|
|
|
Arguments:
|
|
- chatRoomName: name of the chat room this message came from
|
|
- message: the chat room message
|
|
- focused: whether or not the current chatroom has focus. Defaults
|
|
to True so that we can use this method to present chat history
|
|
as well as incoming messages.
|
|
"""
|
|
|
|
# Only speak/braille the new message if it matches how the user
|
|
# wants chat messages spoken.
|
|
#
|
|
verbosity = _settingsManager.getAppSetting(self._script.app, 'chatMessageVerbosity')
|
|
if orca_state.activeScript.name != self._script.name \
|
|
and verbosity == settings.CHAT_SPEAK_ALL_IF_FOCUSED:
|
|
return
|
|
elif not focused and verbosity == settings.CHAT_SPEAK_FOCUSED_CHANNEL:
|
|
return
|
|
|
|
text = ""
|
|
if chatRoomName and \
|
|
_settingsManager.getAppSetting(self._script.app, 'chatSpeakRoomName'):
|
|
text = messages.CHAT_MESSAGE_FROM_ROOM % chatRoomName
|
|
|
|
if not settings.presentChatRoomLast:
|
|
text = self._script.utilities.appendString(text, message)
|
|
else:
|
|
text = self._script.utilities.appendString(message, text)
|
|
|
|
if len(text.strip()):
|
|
voice = self._script.speechGenerator.voice(string=text)
|
|
self._script.speakMessage(text, voice=voice)
|
|
self._script.displayBrailleMessage(text)
|
|
|
|
def getMessageFromEvent(self, event):
|
|
"""Get the actual displayed message. This will almost always be the
|
|
unaltered any_data from an event of type object:text-changed:insert.
|
|
|
|
Arguments:
|
|
- event: the Event from which to take the text.
|
|
|
|
Returns the string which should be presented as the newly-inserted
|
|
text. (Things like chatroom name prefacing get handled elsewhere.)
|
|
"""
|
|
|
|
return event.any_data
|
|
|
|
def presentInsertedText(self, event):
|
|
"""Gives the Chat class an opportunity to present the text from the
|
|
text inserted Event.
|
|
|
|
Arguments:
|
|
- event: the text inserted Event
|
|
|
|
Returns True if we handled this event here; otherwise False, which
|
|
tells the associated script that is not a chat event that requires
|
|
custom handling.
|
|
"""
|
|
|
|
if not event \
|
|
or not event.type.startswith("object:text-changed:insert") \
|
|
or not event.any_data:
|
|
return False
|
|
|
|
if self.isGenericTextObject(event.source):
|
|
# The script should handle non-chat specific text areas (e.g.,
|
|
# adding a new account).
|
|
#
|
|
return False
|
|
|
|
elif self.isInBuddyList(event.source):
|
|
# These are status changes. What the Pidgin script currently
|
|
# does for these is ignore them. It might be nice to add
|
|
# some options to allow the user to customize what status
|
|
# changes are presented. But for now, we'll ignore them
|
|
# across the board.
|
|
#
|
|
return True
|
|
|
|
elif self.isTypingStatusChangedEvent(event):
|
|
self.presentTypingStatusChange(event, event.any_data)
|
|
return True
|
|
|
|
elif self.isChatRoomMsg(event.source):
|
|
# We always automatically go back to focus tracking mode when
|
|
# someone sends us a message.
|
|
#
|
|
if self._script.flatReviewContext:
|
|
self._script.toggleFlatReviewMode()
|
|
|
|
if self.isNewConversation(event.source):
|
|
name = self.getChatRoomName(event.source)
|
|
conversation = Conversation(name, event.source)
|
|
else:
|
|
conversation = self.getConversation(event.source)
|
|
name = conversation.name
|
|
message = self.getMessageFromEvent(event).strip("\n")
|
|
if message:
|
|
self.addMessageToHistory(message, conversation)
|
|
|
|
# The user may or may not want us to present this message. Also,
|
|
# don't speak the name if it's the focused chat.
|
|
#
|
|
focused = self.isFocusedChat(event.source)
|
|
if focused:
|
|
name = ""
|
|
if message:
|
|
self.utterMessage(name, message, focused)
|
|
return True
|
|
|
|
elif self.isAutoCompletedTextEvent(event):
|
|
text = event.any_data
|
|
voice = self._script.speechGenerator.voice(string=text)
|
|
self._script.speakMessage(text, voice=voice)
|
|
return True
|
|
|
|
return False
|
|
|
|
def presentTypingStatusChange(self, event, status):
|
|
"""Presents a change in typing status for the current conversation
|
|
if the status has indeed changed and if the user wants to hear it.
|
|
|
|
Arguments:
|
|
- event: the accessible Event
|
|
- status: a string containing the status change
|
|
|
|
Returns True if we spoke the change; False otherwise
|
|
"""
|
|
|
|
if _settingsManager.getSetting('chatAnnounceBuddyTyping'):
|
|
conversation = self.getConversation(event.source)
|
|
if conversation and (status != conversation.getTypingStatus()):
|
|
voice = self._script.speechGenerator.voice(string=status)
|
|
self._script.speakMessage(status, voice=voice)
|
|
conversation.setTypingStatus(status)
|
|
return True
|
|
|
|
return False
|
|
|
|
def addMessageToHistory(self, message, conversation):
|
|
"""Adds message to both the individual conversation's history
|
|
as well as to the complete history stored in our conversation
|
|
list.
|
|
|
|
Arguments:
|
|
- message: a string containing the message to be added
|
|
- conversation: the instance of the Conversation class to which
|
|
this message belongs
|
|
"""
|
|
|
|
conversation.addMessage(message)
|
|
self._conversationList.addMessage(message, conversation)
|
|
|
|
########################################################################
|
|
# #
|
|
# Convenience methods for identifying, locating different accessibles #
|
|
# #
|
|
########################################################################
|
|
|
|
def isGenericTextObject(self, obj):
|
|
"""Returns True if the given accessible seems to be something
|
|
unrelated to the custom handling we're attempting to do here.
|
|
|
|
Arguments:
|
|
- obj: the accessible object to examine.
|
|
"""
|
|
|
|
state = obj.getState()
|
|
if state.contains(pyatspi.STATE_EDITABLE) \
|
|
and state.contains(pyatspi.STATE_SINGLE_LINE):
|
|
return True
|
|
|
|
return False
|
|
|
|
def isBuddyList(self, obj):
|
|
"""Returns True if obj is the list of buddies in the buddy list
|
|
window. Note that this method relies upon a hierarchical check,
|
|
using a list of hierarchies provided by the script. Scripts
|
|
which have more reliable means of identifying the buddy list
|
|
can override this method.
|
|
|
|
Arguments:
|
|
- obj: the accessible being examined
|
|
"""
|
|
|
|
if obj:
|
|
for roleList in self._buddyListAncestries:
|
|
if self._script.utilities.hasMatchingHierarchy(obj, roleList):
|
|
return True
|
|
|
|
return False
|
|
|
|
def isInBuddyList(self, obj, includeList=True):
|
|
"""Returns True if obj is, or is inside of, the buddy list.
|
|
|
|
Arguments:
|
|
- obj: the accessible being examined
|
|
- includeList: whether or not the list itself should be
|
|
considered "in" the buddy list.
|
|
"""
|
|
|
|
if includeList and self.isBuddyList(obj):
|
|
return True
|
|
|
|
for roleList in self._buddyListAncestries:
|
|
buddyListRole = roleList[0]
|
|
candidate = self._script.utilities.ancestorWithRole(
|
|
obj, [buddyListRole], [pyatspi.ROLE_FRAME])
|
|
if self.isBuddyList(candidate):
|
|
return True
|
|
|
|
return False
|
|
|
|
def isNewConversation(self, obj):
|
|
"""Returns True if the given accessible is the chat history
|
|
associated with a new conversation.
|
|
|
|
Arguments:
|
|
- obj: the accessible object to examine.
|
|
"""
|
|
|
|
conversation = self.getConversation(obj)
|
|
return not self._conversationList.hasConversation(conversation)
|
|
|
|
def getConversation(self, obj):
|
|
"""Attempts to locate the conversation associated with obj.
|
|
|
|
Arguments:
|
|
- obj: the accessible of interest
|
|
|
|
Returns the conversation if found; None otherwise
|
|
"""
|
|
|
|
if not obj:
|
|
return None
|
|
|
|
name = ""
|
|
# TODO - JD: If we have multiple chats going on and those
|
|
# chats have the same name, and we're in the input area,
|
|
# this approach will fail. What I should probably do instead
|
|
# is, upon creation of a new conversation, figure out where
|
|
# the input area is and save it. For now, I just want to get
|
|
# things working. And people should not be in multiple chat
|
|
# rooms with identical names anyway. :-)
|
|
#
|
|
if obj.getRole() in [pyatspi.ROLE_TEXT, pyatspi.ROLE_ENTRY] \
|
|
and obj.getState().contains(pyatspi.STATE_EDITABLE):
|
|
name = self.getChatRoomName(obj)
|
|
|
|
for conversation in self._conversationList.conversations:
|
|
if name:
|
|
if name == conversation.name:
|
|
return conversation
|
|
# Doing an equality check seems to be preferable here to
|
|
# utilities.isSameObject as a result of false positives.
|
|
#
|
|
elif obj == conversation.accHistory:
|
|
return conversation
|
|
|
|
return None
|
|
|
|
def isChatRoomMsg(self, obj):
|
|
"""Returns True if the given accessible is the text object for
|
|
associated with a chat room conversation.
|
|
|
|
Arguments:
|
|
- obj: the accessible object to examine.
|
|
"""
|
|
|
|
if obj and obj.getRole() == pyatspi.ROLE_TEXT \
|
|
and obj.parent.getRole() == pyatspi.ROLE_SCROLL_PANE:
|
|
state = obj.getState()
|
|
if not state.contains(pyatspi.STATE_EDITABLE) \
|
|
and state.contains(pyatspi.STATE_MULTI_LINE):
|
|
return True
|
|
|
|
return False
|
|
|
|
def isFocusedChat(self, obj):
|
|
"""Returns True if we plan to treat this chat as focused for
|
|
the purpose of deciding whether or not a message should be
|
|
presented to the user.
|
|
|
|
Arguments:
|
|
- obj: the accessible object to examine.
|
|
"""
|
|
|
|
if obj and obj.getState().contains(pyatspi.STATE_SHOWING):
|
|
active = self._script.utilities.topLevelObjectIsActiveAndCurrent(obj)
|
|
msg = "INFO: %s's window is focused chat: %s" % (obj, active)
|
|
debug.println(debug.LEVEL_INFO, msg, True)
|
|
return active
|
|
|
|
msg = "INFO: %s is not focused chat (not showing)" % obj
|
|
debug.println(debug.LEVEL_INFO, msg, True)
|
|
return False
|
|
|
|
def getChatRoomName(self, obj):
|
|
"""Attempts to find the name of the current chat room.
|
|
|
|
Arguments:
|
|
- obj: The accessible of interest
|
|
|
|
Returns a string containing what we think is the chat room name.
|
|
"""
|
|
|
|
# Most of the time, it seems that the name can be found in the
|
|
# page tab which is the ancestor of the chat history. Failing
|
|
# that, we'll look at the frame name. Failing that, scripts
|
|
# should override this method. :-)
|
|
#
|
|
ancestor = self._script.utilities.ancestorWithRole(
|
|
obj,
|
|
[pyatspi.ROLE_PAGE_TAB, pyatspi.ROLE_FRAME],
|
|
[pyatspi.ROLE_APPLICATION])
|
|
name = ""
|
|
try:
|
|
text = self._script.utilities.displayedText(ancestor)
|
|
if text.lower().strip() != self._script.name.lower().strip():
|
|
name = text
|
|
except:
|
|
pass
|
|
|
|
# Some applications don't trash their page tab list when there is
|
|
# only one active chat, but instead they remove the text or hide
|
|
# the item. Therefore, we'll give it one more shot.
|
|
#
|
|
if not name:
|
|
ancestor = self._script.utilities.ancestorWithRole(
|
|
ancestor, [pyatspi.ROLE_FRAME], [pyatspi.ROLE_APPLICATION])
|
|
try:
|
|
text = self._script.utilities.displayedText(ancestor)
|
|
if text.lower().strip() != self._script.name.lower().strip():
|
|
name = text
|
|
except:
|
|
pass
|
|
|
|
return name
|
|
|
|
def isAutoCompletedTextEvent(self, event):
|
|
"""Returns True if event is associated with text being autocompleted.
|
|
|
|
Arguments:
|
|
- event: the accessible event being examined
|
|
"""
|
|
|
|
if event.source.getRole() != pyatspi.ROLE_TEXT:
|
|
return False
|
|
|
|
lastKey, mods = self._script.utilities.lastKeyAndModifiers()
|
|
if lastKey == "Tab" and event.any_data and event.any_data != "\t":
|
|
return True
|
|
|
|
return False
|
|
|
|
def isTypingStatusChangedEvent(self, event):
|
|
"""Returns True if event is associated with a change in typing status.
|
|
|
|
Arguments:
|
|
- event: the accessible event being examined
|
|
"""
|
|
|
|
# TODO - JD: I still need to figure this one out. Pidgin seems to
|
|
# no longer be presenting this change in the conversation history
|
|
# as it was doing before. And I'm not yet sure what other apps do.
|
|
# In the meantime, scripts can override this.
|
|
#
|
|
return False
|