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 @@
__all__ = ['HistoryManager']
import re
import bisect
import cPickle as pickle
import re
from PyQt4.QtGui import QIcon
from application.notification import IObserver, NotificationCenter
from application.python import Null
from application.python.types import Singleton
from collections import deque
from datetime import date, datetime
from zope.interface import implements
......@@ -17,7 +19,7 @@ from sipsimple.account import BonjourAccount
from sipsimple.addressbook import AddressbookManager
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
......@@ -25,23 +27,25 @@ class HistoryManager(object):
__metaclass__ = Singleton
implements(IObserver)
history_size = 20
def __init__(self):
try:
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:
self.missed_calls = deque(maxlen=10)
self.placed_calls = deque(maxlen=10)
self.received_calls = deque(maxlen=10)
self.calls = []
else:
self.missed_calls, self.placed_calls, self.received_calls = data
self.calls = data[-self.history_size:]
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPSessionDidEnd')
notification_center.add_observer(self, name='SIPSessionDidFail')
@run_in_thread('file-io')
def save(self):
with open(ApplicationData.get('calls_history'), 'wb+') as f:
pickle.dump((self.missed_calls, self.placed_calls, self.received_calls), f)
with open(ApplicationData.get('calls_history'), 'wb+') as history_file:
pickle.dump(self.calls, history_file)
@run_in_gui_thread
def handle_notification(self, notification):
......@@ -53,10 +57,8 @@ class HistoryManager(object):
return
session = notification.sender
entry = HistoryEntry.from_session(session)
if session.direction == 'incoming':
self.received_calls.append(entry)
else:
self.placed_calls.append(entry)
bisect.insort(self.calls, entry)
self.calls = self.calls[-self.history_size:]
self.save()
def _NH_SIPSessionDidFail(self, notification):
......@@ -65,29 +67,76 @@ class HistoryManager(object):
session = notification.sender
entry = HistoryEntry.from_session(session)
if session.direction == 'incoming':
if notification.data.code == 487 and notification.data.failure_reason == 'Call completed elsewhere':
self.received_calls.append(entry)
else:
self.missed_calls.append(entry)
if notification.data.code != 487 or notification.data.failure_reason != 'Call completed elsewhere':
entry.failed = True
else:
if notification.data.code == 487:
entry.reason = 'cancelled'
else:
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()
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):
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.target_uri = target_uri
self.account_id = account_id
self.call_time = call_time
self.duration = duration
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
def from_session(cls, session):
......@@ -114,7 +163,7 @@ class HistoryEntry(object):
remote_identity = '%s <%s>' % (display_name, remote_uri)
else:
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):
result = unicode(self.remote_identity)
......
......@@ -192,12 +192,8 @@ class MainWindow(base_class, ui_class):
# History menu actions
self.history_manager = HistoryManager()
self.missed_calls_menu.aboutToShow.connect(self._SH_MissedCallsMenuAboutToShow)
self.missed_calls_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)
self.history_menu.aboutToShow.connect(self._SH_HistoryMenuAboutToShow)
self.history_menu.triggered.connect(self._AH_HistoryMenuTriggered)
# Tools menu actions
self.answering_machine_action.triggered.connect(self._AH_EnableAnsweringMachineTriggered)
......@@ -383,6 +379,16 @@ class MainWindow(base_class, ui_class):
session_manager = SessionManager()
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):
account_manager = AccountManager()
session_manager = SessionManager()
......@@ -626,24 +632,6 @@ class MainWindow(base_class, ui_class):
def _SH_SwitchViewButtonChangedView(self, view):
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):
self.pending_watcher_dialogs.remove(self.sender())
......
......@@ -965,38 +965,6 @@ padding: 2px;</string>
<property name="title">
<string>&amp;History</string>
</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 class="QMenu" name="devices_menu">
<property name="title">
......@@ -1234,23 +1202,6 @@ padding: 2px;</string>
<string>&amp;Release notes</string>
</property>
</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">
<property name="text">
<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