Commit 7c14a3b8 authored by Dan Pascu's avatar Dan Pascu

Refactored history menu to show an aggregated list with all calls

parent 7842c543
...@@ -3,13 +3,15 @@ ...@@ -3,13 +3,15 @@
__all__ = ['HistoryManager'] __all__ = ['HistoryManager']
import re import bisect
import cPickle as pickle import cPickle as pickle
import re
from PyQt4.QtGui import QIcon
from application.notification import IObserver, NotificationCenter from application.notification import IObserver, NotificationCenter
from application.python import Null from application.python import Null
from application.python.types import Singleton from application.python.types import Singleton
from collections import deque
from datetime import date, datetime from datetime import date, datetime
from zope.interface import implements from zope.interface import implements
...@@ -17,7 +19,7 @@ from sipsimple.account import BonjourAccount ...@@ -17,7 +19,7 @@ from sipsimple.account import BonjourAccount
from sipsimple.addressbook import AddressbookManager from sipsimple.addressbook import AddressbookManager
from sipsimple.threading import run_in_thread from sipsimple.threading import run_in_thread
from blink.resources import ApplicationData from blink.resources import ApplicationData, Resources
from blink.util import run_in_gui_thread from blink.util import run_in_gui_thread
...@@ -25,23 +27,25 @@ class HistoryManager(object): ...@@ -25,23 +27,25 @@ class HistoryManager(object):
__metaclass__ = Singleton __metaclass__ = Singleton
implements(IObserver) implements(IObserver)
history_size = 20
def __init__(self): def __init__(self):
try: try:
data = pickle.load(open(ApplicationData.get('calls_history'))) data = pickle.load(open(ApplicationData.get('calls_history')))
if not isinstance(data, list) or not all(isinstance(item, HistoryEntry) for item in data):
raise ValueError("invalid save data")
except Exception: except Exception:
self.missed_calls = deque(maxlen=10) self.calls = []
self.placed_calls = deque(maxlen=10)
self.received_calls = deque(maxlen=10)
else: else:
self.missed_calls, self.placed_calls, self.received_calls = data self.calls = data[-self.history_size:]
notification_center = NotificationCenter() notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPSessionDidEnd') notification_center.add_observer(self, name='SIPSessionDidEnd')
notification_center.add_observer(self, name='SIPSessionDidFail') notification_center.add_observer(self, name='SIPSessionDidFail')
@run_in_thread('file-io') @run_in_thread('file-io')
def save(self): def save(self):
with open(ApplicationData.get('calls_history'), 'wb+') as f: with open(ApplicationData.get('calls_history'), 'wb+') as history_file:
pickle.dump((self.missed_calls, self.placed_calls, self.received_calls), f) pickle.dump(self.calls, history_file)
@run_in_gui_thread @run_in_gui_thread
def handle_notification(self, notification): def handle_notification(self, notification):
...@@ -53,10 +57,8 @@ class HistoryManager(object): ...@@ -53,10 +57,8 @@ class HistoryManager(object):
return return
session = notification.sender session = notification.sender
entry = HistoryEntry.from_session(session) entry = HistoryEntry.from_session(session)
if session.direction == 'incoming': bisect.insort(self.calls, entry)
self.received_calls.append(entry) self.calls = self.calls[-self.history_size:]
else:
self.placed_calls.append(entry)
self.save() self.save()
def _NH_SIPSessionDidFail(self, notification): def _NH_SIPSessionDidFail(self, notification):
...@@ -65,29 +67,76 @@ class HistoryManager(object): ...@@ -65,29 +67,76 @@ class HistoryManager(object):
session = notification.sender session = notification.sender
entry = HistoryEntry.from_session(session) entry = HistoryEntry.from_session(session)
if session.direction == 'incoming': if session.direction == 'incoming':
if notification.data.code == 487 and notification.data.failure_reason == 'Call completed elsewhere': if notification.data.code != 487 or notification.data.failure_reason != 'Call completed elsewhere':
self.received_calls.append(entry) entry.failed = True
else:
self.missed_calls.append(entry)
else: else:
if notification.data.code == 487: if notification.data.code == 487:
entry.reason = 'cancelled' entry.reason = 'cancelled'
else: else:
entry.reason = '%s (%s)' % (notification.data.reason or notification.data.failure_reason, notification.data.code) entry.reason = '%s (%s)' % (notification.data.reason or notification.data.failure_reason, notification.data.code)
self.placed_calls.append(entry) entry.failed = True
bisect.insort(self.calls, entry)
self.calls = self.calls[-self.history_size:]
self.save() self.save()
class IconDescriptor(object):
def __init__(self, filename):
self.filename = filename
self.icon = None
def __get__(self, obj, objtype):
if self.icon is None:
self.icon = QIcon(self.filename)
self.icon.filename = self.filename
return self.icon
def __set__(self, obj, value):
raise AttributeError("attribute cannot be set")
def __delete__(self, obj):
raise AttributeError("attribute cannot be deleted")
class HistoryEntry(object): class HistoryEntry(object):
phone_number_re = re.compile(r'^(?P<number>(0|00|\+)[1-9]\d{7,14})@') phone_number_re = re.compile(r'^(?P<number>(0|00|\+)[1-9]\d{7,14})@')
def __init__(self, remote_identity, target_uri, account_id, call_time, duration, reason=None): incoming_normal_icon = IconDescriptor(Resources.get('icons/arrow-inward-blue.svg'))
outgoing_normal_icon = IconDescriptor(Resources.get('icons/arrow-outward-green.svg'))
incoming_failed_icon = IconDescriptor(Resources.get('icons/arrow-inward-red.svg'))
outgoing_failed_icon = IconDescriptor(Resources.get('icons/arrow-outward-red.svg'))
def __init__(self, direction, remote_identity, target_uri, account_id, call_time, duration, reason=None):
self.direction = direction
self.remote_identity = remote_identity self.remote_identity = remote_identity
self.target_uri = target_uri self.target_uri = target_uri
self.account_id = account_id self.account_id = account_id
self.call_time = call_time self.call_time = call_time
self.duration = duration self.duration = duration
self.reason = reason self.reason = reason
self.failed = False
def __eq__(self, other):
return self is other
def __ne__(self, other):
return self is not other
def __lt__(self, other):
return self.call_time < other.call_time
def __le__(self, other):
return self.call_time <= other.call_time
def __gt__(self, other):
return self.call_time > other.call_time
def __ge__(self, other):
return self.call_time >= other.call_time
@property
def icon(self):
if self.failed:
return self.incoming_failed_icon if self.direction == 'incoming' else self.outgoing_failed_icon
else:
return self.incoming_normal_icon if self.direction == 'incoming' else self.outgoing_normal_icon
@classmethod @classmethod
def from_session(cls, session): def from_session(cls, session):
...@@ -114,7 +163,7 @@ class HistoryEntry(object): ...@@ -114,7 +163,7 @@ class HistoryEntry(object):
remote_identity = '%s <%s>' % (display_name, remote_uri) remote_identity = '%s <%s>' % (display_name, remote_uri)
else: else:
remote_identity = remote_uri remote_identity = remote_uri
return cls(remote_identity, remote_uri, unicode(session.account.id), call_time, duration) return cls(session.direction, remote_identity, remote_uri, unicode(session.account.id), call_time, duration)
def __unicode__(self): def __unicode__(self):
result = unicode(self.remote_identity) result = unicode(self.remote_identity)
......
...@@ -192,12 +192,8 @@ class MainWindow(base_class, ui_class): ...@@ -192,12 +192,8 @@ class MainWindow(base_class, ui_class):
# History menu actions # History menu actions
self.history_manager = HistoryManager() self.history_manager = HistoryManager()
self.missed_calls_menu.aboutToShow.connect(self._SH_MissedCallsMenuAboutToShow) self.history_menu.aboutToShow.connect(self._SH_HistoryMenuAboutToShow)
self.missed_calls_menu.triggered.connect(self._AH_HistoryMenuTriggered) self.history_menu.triggered.connect(self._AH_HistoryMenuTriggered)
self.placed_calls_menu.aboutToShow.connect(self._SH_PlacedCallsMenuAboutToShow)
self.placed_calls_menu.triggered.connect(self._AH_HistoryMenuTriggered)
self.received_calls_menu.aboutToShow.connect(self._SH_ReceivedCallsMenuAboutToShow)
self.received_calls_menu.triggered.connect(self._AH_HistoryMenuTriggered)
# Tools menu actions # Tools menu actions
self.answering_machine_action.triggered.connect(self._AH_EnableAnsweringMachineTriggered) self.answering_machine_action.triggered.connect(self._AH_EnableAnsweringMachineTriggered)
...@@ -383,6 +379,16 @@ class MainWindow(base_class, ui_class): ...@@ -383,6 +379,16 @@ class MainWindow(base_class, ui_class):
session_manager = SessionManager() session_manager = SessionManager()
session_manager.create_session(contact, contact_uri, [StreamDescription('audio')], account=account) session_manager.create_session(contact, contact_uri, [StreamDescription('audio')], account=account)
def _SH_HistoryMenuAboutToShow(self):
self.history_menu.clear()
if self.history_manager.calls:
for entry in reversed(self.history_manager.calls):
action = self.history_menu.addAction(entry.icon, unicode(entry))
action.entry = entry
else:
action = self.history_menu.addAction("Call history is empty")
action.setEnabled(False)
def _AH_HistoryMenuTriggered(self, action): def _AH_HistoryMenuTriggered(self, action):
account_manager = AccountManager() account_manager = AccountManager()
session_manager = SessionManager() session_manager = SessionManager()
...@@ -626,24 +632,6 @@ class MainWindow(base_class, ui_class): ...@@ -626,24 +632,6 @@ class MainWindow(base_class, ui_class):
def _SH_SwitchViewButtonChangedView(self, view): def _SH_SwitchViewButtonChangedView(self, view):
self.main_view.setCurrentWidget(self.contacts_panel if view is SwitchViewButton.ContactView else self.sessions_panel) self.main_view.setCurrentWidget(self.contacts_panel if view is SwitchViewButton.ContactView else self.sessions_panel)
def _SH_MissedCallsMenuAboutToShow(self):
self.missed_calls_menu.clear()
for entry in reversed(self.history_manager.missed_calls):
action = self.missed_calls_menu.addAction(unicode(entry))
action.entry = entry
def _SH_PlacedCallsMenuAboutToShow(self):
self.placed_calls_menu.clear()
for entry in reversed(self.history_manager.placed_calls):
action = self.placed_calls_menu.addAction(unicode(entry))
action.entry = entry
def _SH_ReceivedCallsMenuAboutToShow(self):
self.received_calls_menu.clear()
for entry in reversed(self.history_manager.received_calls):
action = self.received_calls_menu.addAction(unicode(entry))
action.entry = entry
def _SH_PendingWatcherDialogFinished(self, result): def _SH_PendingWatcherDialogFinished(self, result):
self.pending_watcher_dialogs.remove(self.sender()) self.pending_watcher_dialogs.remove(self.sender())
......
...@@ -965,38 +965,6 @@ padding: 2px;</string> ...@@ -965,38 +965,6 @@ padding: 2px;</string>
<property name="title"> <property name="title">
<string>&amp;History</string> <string>&amp;History</string>
</property> </property>
<widget class="QMenu" name="missed_calls_menu">
<property name="title">
<string>&amp;Missed calls</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/missed-calls.png</normaloff>icons/missed-calls.png</iconset>
</property>
</widget>
<widget class="QMenu" name="placed_calls_menu">
<property name="title">
<string>&amp;Placed calls</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/placed-calls.png</normaloff>icons/placed-calls.png</iconset>
</property>
</widget>
<widget class="QMenu" name="received_calls_menu">
<property name="title">
<string>&amp;Received calls</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/received-calls.png</normaloff>icons/received-calls.png</iconset>
</property>
</widget>
<addaction name="history_action"/>
<addaction name="separator"/>
<addaction name="missed_calls_menu"/>
<addaction name="placed_calls_menu"/>
<addaction name="received_calls_menu"/>
</widget> </widget>
<widget class="QMenu" name="devices_menu"> <widget class="QMenu" name="devices_menu">
<property name="title"> <property name="title">
...@@ -1234,23 +1202,6 @@ padding: 2px;</string> ...@@ -1234,23 +1202,6 @@ padding: 2px;</string>
<string>&amp;Release notes</string> <string>&amp;Release notes</string>
</property> </property>
</action> </action>
<action name="history_action">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Sessions</string>
</property>
<property name="shortcut">
<string>Ctrl+H</string>
</property>
<property name="shortcutContext">
<enum>Qt::ApplicationShortcut</enum>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="manage_accounts_action"> <action name="manage_accounts_action">
<property name="text"> <property name="text">
<string>&amp;Manage accounts...</string> <string>&amp;Manage accounts...</string>
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="arrow_inward_blue.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="55.9375"
inkscape:cx="8"
inkscape:cy="8"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1693"
inkscape:window-height="1105"
inkscape:window-x="41"
inkscape:window-y="23"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Blue arrow"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1036.3622)"
style="display:inline">
<path
style="fill:none;stroke:#114c8f;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 13,1039.3622 -9.5,9.5"
id="path2987"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#114c8f;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 3,1044.3622 0,5 5,0"
id="path2989"
inkscape:connector-curvature="0" />
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="arrow_inward_red.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="55.9375"
inkscape:cx="8"
inkscape:cy="8"
inkscape:document-units="px"
inkscape:current-layer="g3753"
showgrid="true"
inkscape:window-width="1693"
inkscape:window-height="1105"
inkscape:window-x="41"
inkscape:window-y="23"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-1036.3622)"
id="g3753"
inkscape:groupmode="layer"
inkscape:label="Red arrow"
style="display:inline">
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3755"
d="m 13,1039.3622 -9.5,9.5"
style="fill:none;stroke:#a00000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="path3757"
d="m 3,1044.3622 0,5 5,0"
style="fill:none;stroke:#a00000;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="arrow_outward_green.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="55.9375"
inkscape:cx="8"
inkscape:cy="8"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1693"
inkscape:window-height="1105"
inkscape:window-x="41"
inkscape:window-y="23"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Green arrow"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1036.3622)"
style="display:inline">
<path
inkscape:connector-curvature="0"
id="path2987"
d="m 3,1049.3622 c 3.1666667,-3.1667 6.3333333,-6.3333 9.5,-9.5"
style="fill:none;stroke:#118f11;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="path2989"
d="m 13,1044.3622 c 0,-1.6667 0,-3.3333 0,-5 -1.666667,0 -3.3333333,0 -5,0"
style="fill:none;stroke:#118f11;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="arrow_outward_red.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="55.9375"
inkscape:cx="8"
inkscape:cy="8"
inkscape:document-units="px"
inkscape:current-layer="g4280"
showgrid="true"
inkscape:window-width="1693"
inkscape:window-height="1105"
inkscape:window-x="41"
inkscape:window-y="23"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-1036.3622)"
id="g4280"
inkscape:groupmode="layer"
inkscape:label="Red arrow"
style="display:inline">
<path
style="fill:none;stroke:#a00000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 3,1049.3622 c 3.1666667,-3.1667 6.3333333,-6.3333 9.5,-9.5"
id="path4282"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#a00000;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 13,1044.3622 c 0,-1.6667 0,-3.3333 0,-5 -1.666667,0 -3.3333333,0 -5,0"
id="path4284"
inkscape:connector-curvature="0" />
</g>
</svg>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment