PageRenderTime 40ms CodeModel.GetById 5ms RepoModel.GetById 1ms app.codeStats 0ms

/ChatViewController.py

https://github.com/wilane/blink-cocoa
Python | 435 lines | 416 code | 15 blank | 4 comment | 4 complexity | 1892fd4474cc511fe5c478111242d080 MD5 | raw file
  1. # Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
  2. #
  3. __all__ = ['ChatInputTextView', 'ChatViewController', 'processHTMLText',
  4. 'MSG_STATE_SENDING', 'MSG_STATE_FAILED', 'MSG_STATE_DELIVERED', 'MSG_STATE_DEFERRED']
  5. from Foundation import *
  6. from AppKit import *
  7. from WebKit import WebView
  8. from WebKit import WebViewProgressFinishedNotification, WebActionOriginalURLKey
  9. import calendar
  10. import cgi
  11. import datetime
  12. import os
  13. import re
  14. import time
  15. import urllib
  16. from dateutil.tz import tzlocal
  17. from application.notification import NotificationCenter
  18. from sipsimple.configuration.settings import SIPSimpleSettings
  19. from sipsimple.util import TimestampedNotificationData
  20. from SmileyManager import SmileyManager
  21. from util import escape_html, format_identity_to_string
  22. MSG_STATE_SENDING = "sending" # middleware told us the message is being sent
  23. MSG_STATE_FAILED = "failed" # msg delivery failed
  24. MSG_STATE_DELIVERED = "delivered" # msg successfully delivered
  25. MSG_STATE_DEFERRED = "deferred" # msg delivered to a server but deferred for later delivery
  26. # if user doesnt type for this time, we consider it idling
  27. TYPING_IDLE_TIMEOUT = 5
  28. # if user is typing, is-composing notificaitons will be sent in the following interval
  29. TYPING_NOTIFY_INTERVAL = 30
  30. _url_pattern = re.compile("((?:http://|https://|sip:|sips:)[^ )<>\r\n]+)")
  31. _url_pattern_exact = re.compile("^((?:http://|https://|sip:|sips:)[^ )<>\r\n]+)$")
  32. class ChatMessageObject(object):
  33. def __init__(self, msgid, text, is_html):
  34. self.msgid = msgid
  35. self.text = text
  36. self.is_html = is_html
  37. def processHTMLText(text, usesmileys=True, is_html=False):
  38. def suball(pat, repl, html):
  39. ohtml = ""
  40. while ohtml != html:
  41. html = pat.sub(repl, html)
  42. ohtml = html
  43. return html
  44. if is_html:
  45. text = text.replace('\n', '<br>')
  46. result = []
  47. tokens = _url_pattern.split(text)
  48. for token in tokens:
  49. if not is_html and _url_pattern_exact.match(token):
  50. type, d, rest = token.partition(":")
  51. url = type + d + urllib.quote(rest, "/%?&=;:,@+$#")
  52. token = r'<a href=\"%s\">%s</a>' % (url, escape_html(token))
  53. else:
  54. if not is_html:
  55. token = escape_html(token)
  56. else:
  57. token = token.replace('"', r'\"')
  58. if usesmileys:
  59. token = SmileyManager().subst_smileys_html(token)
  60. result.append(token)
  61. return "".join(result)
  62. class ChatInputTextView(NSTextView):
  63. owner = None
  64. maxLength = None
  65. def dealloc(self):
  66. super(ChatInputTextView, self).dealloc()
  67. def initWithRect_(self, rect):
  68. self = NSTextView.initWithRect_(self, rect)
  69. if self:
  70. pass
  71. return self
  72. def setOwner(self, owner):
  73. self.owner = owner # ChatViewController
  74. def setMaxLength_(self, l):
  75. self.maxLength = l
  76. def insertText_(self, text):
  77. if self.maxLength:
  78. oldText = self.textStorage().copy()
  79. NSTextView.insertText_(self, text)
  80. if self.maxLength and self.textStorage().length() > self.maxLength:
  81. self.textStorage().setAttributedString_(oldText)
  82. self.didChangeText()
  83. def readSelectionFromPasteboard_type_(self, pboard, type):
  84. if self.maxLength:
  85. text = pboard.stringForType_(type)
  86. if text:
  87. if self.textStorage().length() - self.rangeForUserTextChange().length + len(text) > self.maxLength:
  88. text = text.substringWithRange_(NSMakeRange(0, self.maxLength - (self.textStorage().length() - self.rangeForUserTextChange().length)))
  89. self.textStorage().replaceCharactersInRange_withString_(self.rangeForUserTextChange(), text)
  90. self.didChangeText()
  91. return True
  92. return False
  93. else:
  94. return NSTextView.readSelectionFromPasteboard_type_(self, pboard, type)
  95. def draggingEntered_(self, sender):
  96. pboard = sender.draggingPasteboard()
  97. if pboard.types().containsObject_(NSFilenamesPboardType) and hasattr(self.owner.delegate, "sendFiles"):
  98. pboard = sender.draggingPasteboard()
  99. fnames = pboard.propertyListForType_(NSFilenamesPboardType)
  100. for f in fnames:
  101. if not os.path.isfile(f):
  102. return NSDragOperationNone
  103. return NSDragOperationCopy
  104. return NSDragOperationNone
  105. def prepareForDragOperation_(self, sender):
  106. pboard = sender.draggingPasteboard()
  107. if pboard.types().containsObject_(NSFilenamesPboardType):
  108. fnames = pboard.propertyListForType_(NSFilenamesPboardType)
  109. for f in fnames:
  110. if not os.path.isfile(f):
  111. return False
  112. return True
  113. return False
  114. def performDragOperation_(self, sender):
  115. pboard = sender.draggingPasteboard()
  116. if hasattr(self.owner.delegate, "sendFiles") and pboard.types().containsObject_(NSFilenamesPboardType):
  117. ws = NSWorkspace.sharedWorkspace()
  118. filenames = pboard.propertyListForType_(NSFilenamesPboardType)
  119. return self.owner.delegate.sendFiles(filenames)
  120. return False
  121. def keyDown_(self, event):
  122. if event.keyCode() == 36 and (event.modifierFlags() & NSShiftKeyMask):
  123. self.insertText_('\r\n')
  124. elif (event.modifierFlags() & NSCommandKeyMask):
  125. keys = event.characters()
  126. if keys[0] == 'i' and self.owner.delegate.sessionController.info_panel is not None:
  127. self.owner.delegate.sessionController.info_panel.toggle()
  128. else:
  129. super(ChatInputTextView, self).keyDown_(event)
  130. class ChatWebView(WebView):
  131. def dealloc(self):
  132. super(ChatWebView, self).dealloc()
  133. def draggingEntered_(self, sender):
  134. pboard = sender.draggingPasteboard()
  135. if pboard.types().containsObject_(NSFilenamesPboardType) and hasattr(self.frameLoadDelegate().delegate, "sendFiles"):
  136. fnames = pboard.propertyListForType_(NSFilenamesPboardType)
  137. for f in fnames:
  138. if not os.path.isfile(f):
  139. return NSDragOperationNone
  140. return NSDragOperationCopy
  141. return NSDragOperationNone
  142. def performDragOperation_(self, sender):
  143. if hasattr(self.frameLoadDelegate().delegate, "sendFiles"):
  144. pboard = sender.draggingPasteboard()
  145. if pboard.types().containsObject_(NSFilenamesPboardType):
  146. ws = NSWorkspace.sharedWorkspace()
  147. filenames = pboard.propertyListForType_(NSFilenamesPboardType)
  148. return self.frameLoadDelegate().delegate.sendFiles(filenames)
  149. return False
  150. class ChatViewController(NSObject):
  151. view = objc.IBOutlet()
  152. outputView = objc.IBOutlet()
  153. inputText = objc.IBOutlet()
  154. inputView = objc.IBOutlet()
  155. splitterHeight = None
  156. delegate = objc.IBOutlet() # ChatController
  157. account = None
  158. rendered_messages = None
  159. finishedLoading = False
  160. expandSmileys = True
  161. editorStatus = False
  162. rendered_messages = set()
  163. pending_messages = {}
  164. video_source = None
  165. video_visible = False
  166. video_initialized = False
  167. lastTypedTime = None
  168. lastTypeNotifyTime = None
  169. # timer is triggered every TYPING_IDLE_TIMEOUT, and a new is-composing msg is sent
  170. typingTimer = None
  171. editor_has_changed = False
  172. def resetRenderedMessages(self):
  173. self.rendered_messages=set()
  174. def setAccount_(self, account):
  175. self.account = account
  176. def awakeFromNib(self):
  177. self.outputView.setShouldCloseWithWindow_(True)
  178. self.outputView.registerForDraggedTypes_(NSArray.arrayWithObject_(NSFilenamesPboardType))
  179. NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, "webviewFinishedLoading:", WebViewProgressFinishedNotification, self.outputView)
  180. if self.inputText:
  181. self.inputText.registerForDraggedTypes_(NSArray.arrayWithObject_(NSFilenamesPboardType))
  182. self.inputText.setOwner(self)
  183. NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, "textDidChange:", NSTextDidChangeNotification, self.inputText)
  184. self.messageQueue = []
  185. def setContentFile_(self, path):
  186. self.finishedLoading = False
  187. request = NSURLRequest.alloc().initWithURL_(NSURL.alloc().initFileURLWithPath_(path))
  188. self.outputView.mainFrame().loadRequest_(request)
  189. assert self.outputView.preferences().isJavaScriptEnabled()
  190. def appendAttributedString_(self, text):
  191. storage = self.inputText.textStorage()
  192. storage.beginEditing()
  193. storage.appendAttributedString_(text)
  194. storage.endEditing()
  195. def textDidChange_(self, notification):
  196. self.lastTypedTime = datetime.datetime.now()
  197. if self.inputText.textStorage().length() == 0:
  198. self.resetTyping()
  199. else:
  200. if not self.lastTypeNotifyTime or time.time() - self.lastTypeNotifyTime > TYPING_NOTIFY_INTERVAL:
  201. self.lastTypeNotifyTime = time.time()
  202. self.delegate.chatView_becameActive_(self, self.lastTypedTime)
  203. if self.typingTimer:
  204. # delay the timeout a bit more
  205. self.typingTimer.setFireDate_(NSDate.dateWithTimeIntervalSinceNow_(TYPING_IDLE_TIMEOUT))
  206. else:
  207. self.typingTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(TYPING_IDLE_TIMEOUT, self, "becameIdle:", None, False)
  208. def resetTyping(self):
  209. self.becameIdle_(None)
  210. def becameIdle_(self, timer):
  211. if self.typingTimer:
  212. self.typingTimer.invalidate()
  213. # if we got here, it means there was no typing activity in the last TYPING_IDLE_TIMEOUT seconds
  214. # so change state back to idle
  215. self.typingTimer = None
  216. self.delegate.chatView_becameIdle_(self, self.lastTypedTime)
  217. self.lastTypeNotifyTime = None
  218. def markMessage(self, msgid, state, private=False): # delegate
  219. if state == MSG_STATE_DELIVERED:
  220. is_private = 1 if private else "null"
  221. script = "markDelivered('%s',%s)"%(msgid, is_private)
  222. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  223. elif state == MSG_STATE_DEFERRED:
  224. script = "markDeferred('%s')"%msgid
  225. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  226. elif state == MSG_STATE_FAILED:
  227. script = "markFailed('%s')"%msgid
  228. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  229. def clear(self):
  230. if self.finishedLoading:
  231. self.outputView.stringByEvaluatingJavaScriptFromString_("clear()")
  232. else:
  233. self.messageQueue = []
  234. def showSystemMessage(self, text, timestamp=None, is_error=False):
  235. if timestamp is None:
  236. timestamp = datetime.datetime.now(tzlocal())
  237. if type(timestamp) is datetime.datetime:
  238. if timestamp.date() != datetime.date.today():
  239. timestamp = time.strftime("%F %T", time.localtime(calendar.timegm(timestamp.utctimetuple())))
  240. else:
  241. timestamp = time.strftime("%T", time.localtime(calendar.timegm(timestamp.utctimetuple())))
  242. is_error = 1 if is_error else "null"
  243. script = """renderSystemMessage("%s", "%s", %s)""" % (processHTMLText(text), timestamp, is_error)
  244. if self.finishedLoading:
  245. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  246. else:
  247. self.messageQueue.append(script)
  248. def showMessage(self, msgid, direction, sender, icon_path, text, timestamp, is_html=False, state='', recipient='', is_private=False, history_entry=False):
  249. if not history_entry and not self.delegate.isOutputFrameVisible():
  250. self.delegate.showChatViewWhileVideoActive()
  251. # keep track of rendered messages to toggle the smileys
  252. rendered_message = ChatMessageObject(msgid, text, is_html)
  253. self.rendered_messages.add(rendered_message)
  254. if timestamp.date() != datetime.date.today():
  255. displayed_timestamp = time.strftime("%F %T", time.localtime(calendar.timegm(timestamp.utctimetuple())))
  256. else:
  257. displayed_timestamp = time.strftime("%T", time.localtime(calendar.timegm(timestamp.utctimetuple())))
  258. text = processHTMLText(text, self.expandSmileys, is_html)
  259. private = 1 if is_private else "null"
  260. if is_private and recipient:
  261. label = 'Private message to %s' % cgi.escape(recipient) if direction == 'outgoing' else 'Private message from %s' % cgi.escape(sender)
  262. else:
  263. label = cgi.escape(format_identity_to_string(self.account, format='full')) if sender is None else cgi.escape(sender)
  264. script = """renderMessage('%s', '%s', '%s', '%s', "%s", '%s', '%s', %s)""" % (msgid, direction, label, icon_path, text, displayed_timestamp, state, private)
  265. if self.finishedLoading:
  266. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  267. else:
  268. self.messageQueue.append(script)
  269. if hasattr(self.delegate, "chatViewDidGetNewMessage_"):
  270. self.delegate.chatViewDidGetNewMessage_(self)
  271. def toggleSmileys(self, expandSmileys):
  272. for entry in self.rendered_messages:
  273. self.updateMessage(entry.msgid, entry.text, entry.is_html, expandSmileys)
  274. def updateMessage(self, msgid, text, is_html, expandSmileys):
  275. text = processHTMLText(text, expandSmileys, is_html)
  276. script = """updateMessageBodyContent('%s', "%s")""" % (msgid, text)
  277. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  278. def toggleCollaborationEditor(self, editor_status):
  279. if editor_status:
  280. self.showCollaborationEditor()
  281. else:
  282. self.hideCollaborationEditor()
  283. def showCollaborationEditor(self):
  284. settings = SIPSimpleSettings()
  285. frame=self.inputView.frame()
  286. self.splitterHeight = frame.size.height
  287. frame.size.height = 0
  288. self.inputView.setFrame_(frame)
  289. script = """showCollaborationEditor("%s", "%s")""" % (self.delegate.sessionController.collaboration_form_id, settings.server.collaboration_url)
  290. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  291. def hideCollaborationEditor(self):
  292. if self.splitterHeight is not None:
  293. frame=self.inputView.frame()
  294. frame.size.height = self.splitterHeight
  295. self.inputView.setFrame_(frame)
  296. script = "hideCollaborationEditor()"
  297. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  298. def scrollToBottom(self):
  299. script = "scrollToBottom()"
  300. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  301. def webviewFinishedLoading_(self, notification):
  302. self.document = self.outputView.mainFrameDocument()
  303. self.finishedLoading = True
  304. if hasattr(self.delegate, "chatViewDidLoad_"):
  305. self.delegate.chatViewDidLoad_(self)
  306. for script in self.messageQueue:
  307. self.outputView.stringByEvaluatingJavaScriptFromString_(script)
  308. self.messageQueue = []
  309. def webView_contextMenuItemsForElement_defaultMenuItems_(self, sender, element, defaultItems):
  310. for item in defaultItems:
  311. if item.title() == 'Reload':
  312. del defaultItems[defaultItems.index(item)]
  313. break
  314. return defaultItems
  315. def webView_decidePolicyForNavigationAction_request_frame_decisionListener_(self, webView, info, request, frame, listener):
  316. # intercept when user clicks on links so that we process them in different ways
  317. theURL = info[WebActionOriginalURLKey]
  318. if self.delegate and hasattr(self.delegate, 'getWindow'):
  319. window = self.delegate.getWindow()
  320. if window and window.startScreenSharingWithUrl(theURL.absoluteString()):
  321. return
  322. if theURL.scheme() == "file":
  323. listener.use()
  324. else:
  325. # use system wide web browser
  326. listener.ignore()
  327. NSWorkspace.sharedWorkspace().openURL_(theURL)
  328. # capture java-script function collaborativeEditorisTyping
  329. def isSelectorExcludedFromWebScript_(self, sel):
  330. if sel == "collaborativeEditorisTyping":
  331. return False
  332. return True
  333. def collaborativeEditorisTyping(self):
  334. self.editor_has_changed = True
  335. self.delegate.resetIsComposingTimer(5)
  336. NotificationCenter().post_notification("BlinkColaborativeEditorContentHasChanged", sender=self, data=TimestampedNotificationData())
  337. def webView_didClearWindowObject_forFrame_(self, sender, windowObject, frame):
  338. windowObject.setValue_forKey_(self, "blink")
  339. def close(self):
  340. # memory clean up
  341. self.rendered_messages = set()
  342. self.pending_messages = {}
  343. self.view.removeFromSuperview()
  344. self.inputText.setOwner(None)
  345. self.inputText.removeFromSuperview()
  346. self.outputView.close()
  347. self.outputView.removeFromSuperview()
  348. self.release()
  349. def dealloc(self):
  350. if self.typingTimer:
  351. self.typingTimer.invalidate()
  352. NSNotificationCenter.defaultCenter().removeObserver_(self)
  353. super(ChatViewController, self).dealloc()