Commit 86e4609e authored by Dan Pascu's avatar Dan Pascu

Added preliminary chat support

parent 01f56d5c
include MANIFEST.in TODO run
include bin/blink
recursive-include resources *.ui *.png *.svg *.wav *.ico *.mng
recursive-include doc *.txt *.html install.*
recursive-include resources *.ui
recursive-include resources/chat style.xml *.style *.html *.css *.png *.svg *.jpg *.py
recursive-include resources/icons *.png *.svg *.ico *.mng
recursive-include resources/sounds *.wav
recursive-include resources/tls *.crt
recursive-include doc *.txt *.html install.*
......@@ -16,6 +16,75 @@ Code refactoring
Issues
------
- in Smooth Operator check if the src attribute is atill needed in elements
that have the x-color class. there used to be a rule that matched elements
with an x-color class which also had a src attribute pointing to a
particular image, but since that was removed, no other rule matching
an element with the x-color class cares for the src attribute. check if
the src attribute on a span tag means anything, if not it can probably be
safely removed from message.html and message_continuation.html
- don't show selected audio device on the incoming dialog for chat
- reconsider the busy button on the incoming dialog (replace with ignore?)
- if I have an audio(+chat) call and make another atempt to establish a call
(doesn't matter what kind), but cancel it, the hold button is left disabled
- when a session is closed the chat window shows: "Disconnected: Connection
was closed cleanly", but when calling oneself and a loop is detected is
only says "Disconnected". It should actually only say Disconnected when
the connection is ended normally and show the failure reason if not.
Also when a connection is ended voluntarily it should not care if there
is a failure while stopping the streams.
- 1 session active and aother on hold, it beeps every 15 seconds.
- new problem introduced: because we create streams early, now the session
tiles show the stream icons before we even attempt to connect.
- decide what to do about having keyboard shortcuts for hold/hangup in the
chat window (list may not be visible all the time and here we also
differentiate between hangup and delete session)
- modify the conferencing code to use move inside the model instead of
remove+add, to avoid/minimize selection changes
- move tray icon from the main window to Blink?
- find out what messes up the selection while dragging a contact into a
conference if ignore_selection_changes is not set.
- find a way to not have a chat session item selected when we press over
the close/expand indicators on the session tile.
- if I have an audio only session and I add chat, it rings using the audio
outgoing ringtone. to fix maybe add a proposed_streams to BlinkSession
and put the proposed streams in there until they are accepted. this way
we know what is proposed even when handling BlinkSessionDidChangeState
- there are session transitions that do not change the state (for example
a stream that is removed, either by local or remote, never switches the
state to sent/received_proposal and back. this means that one cannot
rely on BlinkSessionDidChangeState alone to handle session transitions,
but instead needs to also listed to BlinkSessionDidRemoveStream.
- If an audio+chat session is on hold and the audio stream is removed while
on hold, it remains showing it being on hold until a new audio stream is
added. (this is not true anymore. it only shows on hold until it is
removed from the audio session list. maybe we should reset the active flag
early?)
- I got an incoming call and the contact was found as a google contact, but
in history I have no name and the original uri. if I dial back, it doesn't
find the contact and says number@domain for the name.
- is the ringtone for incoming chat only sessions appropriate?
- not sure about passing a Contact object to the session instead of
contact.settings.
- the DummyContact should follow the other contact APIs (have an id, ...)
in order to be usable in their place.
- have a contact.default_streams that returns a list of StreamDescription?
- have a contact.account property that returns the best account for outgoing?
Ideas:
------
- On sessionAboutToBeRemoved, we should preselect the next item we want
selected, so when it is actually removed, we do not flip.flop the
selection (may also need the ignore_selection_changes flag to be set)
- check the selectionCommand from the audio session model. it may be also
useful for the chat session model to avoid selection on mouse press
events (make it only happen on mouse release events).
- exceptions:
Investigate this exception:
sip:nwpsefvl@10.0.0.1:52067 52067
......
......@@ -29,11 +29,6 @@ from gnutls.crypto import X509Certificate, X509PrivateKey
from gnutls.errors import GNUTLSError
from zope.interface import implements
try:
from blink import branding
except ImportError:
branding = Null
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.addressbook import Contact, Group
from sipsimple.application import SIPApplication
......@@ -42,6 +37,11 @@ from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import run_in_green_thread
try:
from blink import branding
except ImportError:
branding = Null
from blink.chatwindow import ChatWindow
from blink.configuration.account import AccountExtension, BonjourAccountExtension
from blink.configuration.addressbook import ContactExtension, GroupExtension
from blink.configuration.datatypes import InvalidToken
......@@ -109,15 +109,18 @@ class Blink(QApplication):
self.setApplicationVersion(__version__)
self.main_window = MainWindow()
self.chat_window = ChatWindow()
self.ip_address_monitor = IPAddressMonitor()
self.log_manager = LogManager()
self.presence_manager = PresenceManager()
self.session_manager = SessionManager()
self.update_manager = UpdateManager()
# Prevent application from exiting after last window is closed if system tray was initialized
if self.main_window.system_tray_icon:
self.setQuitOnLastWindowClosed(False)
self.update_manager = UpdateManager()
self.main_window.check_for_updates_action.triggered.connect(self.update_manager.check_for_updates)
self.main_window.check_for_updates_action.setVisible(self.update_manager != Null)
......@@ -126,8 +129,6 @@ class Blink(QApplication):
Contact.register_extension(ContactExtension)
Group.register_extension(GroupExtension)
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
session_manager = SessionManager()
session_manager.initialize(self.main_window, self.main_window.session_model)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.sip_application)
......@@ -143,6 +144,11 @@ class Blink(QApplication):
self.sip_application.thread.join()
self.log_manager.stop()
def quit(self):
self.chat_window.close()
self.main_window.close()
super(Blink, self).quit()
def fetch_account(self):
filename = os.path.expanduser('~/.blink_account')
if not os.path.exists(filename):
......
This diff is collapsed.
......@@ -10,11 +10,12 @@ from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtens
from sipsimple.configuration.datatypes import AudioCodecList, Hostname, MSRPConnectionModel, MSRPTransport, NonNegativeInteger, SIPTransportList, SRTPEncryption
from sipsimple.util import user_info
from blink.configuration.datatypes import ApplicationDataPath, CustomSoundFile, DefaultPath, HTTPURL, IconDescriptor
from blink.configuration.datatypes import ApplicationDataPath, HTTPURL, IconDescriptor, SoundFile
from blink.resources import Resources
class BonjourMSRPSettingsExtension(BonjourMSRPSettings):
transport = Setting(type=MSRPTransport, default='tcp')
transport = Setting(type=MSRPTransport, default='tls')
class BonjourSIPSettings(SettingsGroup):
......@@ -27,6 +28,7 @@ class MessageSummarySettingsExtension(MessageSummarySettings):
class MSRPSettingsExtension(MSRPSettings):
connection_model = Setting(type=MSRPConnectionModel, default='relay')
transport = Setting(type=MSRPTransport, default='tls')
class PresenceSettingsExtension(PresenceSettings):
......@@ -59,11 +61,11 @@ class ServerSettings(SettingsGroup):
class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=CustomSoundFile, default=CustomSoundFile(DefaultPath), nillable=True)
inbound_ringtone = Setting(type=SoundFile, default=None, nillable=True)
class TLSSettingsExtension(TLSSettings):
certificate = Setting(type=ApplicationDataPath, default=None, nillable=True)
certificate = Setting(type=ApplicationDataPath, default=ApplicationDataPath(Resources.get('tls/default.crt')), nillable=True)
class XCAPSettingsExtension(XCAPSettings):
......@@ -91,5 +93,6 @@ class BonjourAccountExtension(SettingsObjectExtension):
rtp = RTPSettingsExtension
sip = BonjourSIPSettings
sounds = SoundSettings
tls = TLSSettingsExtension
......@@ -63,7 +63,7 @@ class DefaultPath(object):
def __repr__(self):
return self.__class__.__name__
class CustomSoundFile(object):
class CustomSoundFile(object): # check if this data type is still needed -Dan
def __init__(self, path=DefaultPath, volume=100):
self.path = path
self.volume = int(volume)
......
......@@ -80,7 +80,7 @@ class SoundSettings(SettingsGroup):
class TLSSettingsExtension(TLSSettings):
ca_list = Setting(type=ApplicationDataPath, default=None, nillable=True)
ca_list = Setting(type=ApplicationDataPath, default=ApplicationDataPath(Resources.get('tls/ca.crt')), nillable=True)
class SIPSimpleSettingsExtension(SettingsObjectExtension):
......
This diff is collapsed.
This diff is collapsed.
......@@ -132,13 +132,8 @@ class AccountListView(QListView):
#self.setItemDelegate(AccountDelegate(self))
#self.setDropIndicatorShown(False)
def setModel(self, model):
selection_model = self.selectionModel() or Null
selection_model.selectionChanged.disconnect(self._SH_SelectionModelSelectionChanged)
super(AccountListView, self).setModel(model)
self.selectionModel().selectionChanged.connect(self._SH_SelectionModelSelectionChanged)
def _SH_SelectionModelSelectionChanged(self, selected, deselected):
def selectionChanged(self, selected, deselected):
super(AccountListView, self).selectionChanged(selected, deselected)
selection_model = self.selectionModel()
selection = selection_model.selection()
if selection_model.currentIndex() not in selection:
......
This diff is collapsed.
......@@ -13,11 +13,7 @@ from application.python.types import MarkerType
from blink.resources import IconManager
from blink.widgets.color import ColorHelperMixin
from blink.widgets.util import QtDynamicProperty
class ContextMenuActions(object):
pass
from blink.widgets.util import QtDynamicProperty, ContextMenuActions
class IconSelector(QLabel):
......
# Copyright (c) 2010-2013 AG Projects. See LICENSE for details.
#
__all__ = ['QtDynamicProperty']
__all__ = ['QtDynamicProperty', 'ContextMenuActions']
from PyQt4.QtCore import QPyNullVariant
......@@ -25,3 +25,7 @@ class QtDynamicProperty(object):
raise AttributeError("attribute cannot be deleted")
class ContextMenuActions(object):
pass
......@@ -654,7 +654,7 @@ padding: 2px;</string>
</widget>
</item>
<item>
<widget class="ToolButton" name="im_session_button">
<widget class="ToolButton" name="chat_session_button">
<property name="minimumSize">
<size>
<width>29</width>
......@@ -670,8 +670,9 @@ padding: 2px;</string>
<property name="toolTip">
<string>Start a chat session or send an SMS</string>
</property>
<property name="text">
<string>IM</string>
<property name="icon">
<iconset>
<normaloff>icons/chat.svg</normaloff>icons/chat.svg</iconset>
</property>
<property name="iconSize">
<size>
......@@ -685,7 +686,7 @@ padding: 2px;</string>
</widget>
</item>
<item>
<widget class="ToolButton" name="ss_session_button">
<widget class="ToolButton" name="screen_sharing_button">
<property name="minimumSize">
<size>
<width>29</width>
......@@ -780,7 +781,7 @@ padding: 2px;</string>
<number>0</number>
</property>
<item>
<widget class="SessionListView" name="session_list">
<widget class="AudioSessionListView" name="session_list">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
......@@ -1325,11 +1326,6 @@ padding: 2px;</string>
<extends>QComboBox</extends>
<header>blink.accounts</header>
</customwidget>
<customwidget>
<class>SessionListView</class>
<extends>QListView</extends>
<header>blink.sessions</header>
</customwidget>
<customwidget>
<class>SwitchViewButton</class>
<extends>QPushButton</extends>
......@@ -1345,6 +1341,11 @@ padding: 2px;</string>
<extends>QToolButton</extends>
<header>blink.widgets.buttons</header>
</customwidget>
<customwidget>
<class>AudioSessionListView</class>
<extends>QListView</extends>
<header>blink.sessions</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>search_box</tabstop>
......@@ -1359,8 +1360,8 @@ padding: 2px;</string>
<tabstop>switch_view_button</tabstop>
<tabstop>add_contact_button</tabstop>
<tabstop>audio_call_button</tabstop>
<tabstop>im_session_button</tabstop>
<tabstop>ss_session_button</tabstop>
<tabstop>chat_session_button</tabstop>
<tabstop>screen_sharing_button</tabstop>
<tabstop>silent_button</tabstop>
<tabstop>session_list</tabstop>
<tabstop>hangup_all_button</tabstop>
......
@charset "utf-8";
body
{
margin: 0px;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(149, 175, 219, 1)), to(rgba(255, 255 ,255, 1))) fixed repeat-x;
-webkit-background-size: auto;
word-wrap: break-word;
word-break: break-word;
}
#chat
{
padding: 8px;
padding-bottom: 10px;
}
#chat div:first-child
{
margin-top: 2px;
}
.first-focus:before
{
position: absolute;
margin-top: -4px;
right: 5px;
font-size: 9px;
content: "\2b07";
content: "\25bc";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
}
.first-focus.message:not(.consecutive):before
{
position: absolute;
margin-top: -8px;
right: 5px;
font-size: 9px;
content: "\2b07";
content: "\25bc";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
}
.regained-focus:not(.consecutive):before
{
position: absolute;
margin-top: -24px;
right: 5px;
font-size: 9px;
content: "\2b06";
content: "\25b2";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
}
.regained-focus:before
{
position: absolute;
margin-top: -4px;
right: 5px;
font-size: 9px;
content: "\2b06";
content: "\25b2";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
}
.last-focus > #insert:before
{
position: absolute;
margin-top: -4px;
right: 5px;
font-size: 9px;
content: "\2b06";
content: "\25b2";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
}
.message:not(.consecutive),
.status:not(.consecutive),
.event:not(.consecutive)
{
margin-right: 10px;
margin-top: 10px;
padding-top: 6px;
padding-bottom: 4px;
padding-right: 5px;
-webkit-border-radius: 5px;
min-width: 7em;
}
.message:not(.consecutive)
{
color: rgba(64, 64, 64, 1);
-webkit-box-shadow: 0px 1px 4px rgba(0, 0, 0, .4);
min-height: 30px;
}
.status:not(.consecutive),
.event:not(.consecutive)
{
margin-left: 36px;
color: rgba(244, 244, 244, 1);
background: -webkit-gradient(linear, left top, left bottom, from(rgba(96, 96, 96, 1)), to(rgba(64, 64, 64, 1)));
-webkit-box-shadow: 0px 1px 4px rgba(0, 0, 0, .5);
padding-left: 12px;
padding-top: 2px;
padding-bottom: 2px;
}
.history.status:not(.consecutive),
.history.event:not(.consecutive)
{
color: rgba(244, 244, 244, .75);
background: -webkit-gradient(linear, left top, left bottom, from(rgba(112, 112, 112, .625)), to(rgba(48, 48, 48, .625)));
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, .4);
}
.message img.x-icon
{
max-width: 32px;
max-height: 32px;
z-index: 99;
margin-top: -2px;
position: absolute;
left: 12px;
-webkit-border-radius: 5px;
background: rgba(240, 240, 240, 1);
}
.history.message .x-icon
{
opacity: .5;
background: rgba(240, 240, 240, .5);
}
.message.hide-icons img.x-icon,
.IRC.message img.x-icon,
.consecutive.message img.x-icon,
.consecutive.message .x-sender,
.message .x-iconmask
{
display: none !important;
}
.message .x-wrap
{
margin-left: 36px;
margin-top: -2px;
margin-bottom: -2px;
display: block;
padding-top: 2px;
padding-left: 12px;
padding-bottom: 5px;
}
.status .x-wrap,
.event .x-wrap
{
display: block;
padding-top: 0px;
padding-bottom: 1px;
margin-left: .18em;
}
.consecutive.message .x-wrap
{
padding-top: 0px;
}
.consecutive.status .x-wrap,
.consecutive.event .x-wrap
{
padding-top: 5px;
}
.status.hide-icons,
.event.hide-icons,
.message.hide-icons .x-wrap,
.IRC.status,
.IRC.event,
.IRC.message .x-wrap
{
margin-left: 0px;
}
.incoming .x-sender
{
color: rgba(168, 0, 40, 1);
}
.outgoing .x-sender
{
color: rgba(0, 16, 144, 1);
}
.history.message.incoming .x-sender
{
color: rgba(168, 0, 40, .5);
}
.history.message.outgoing .x-sender
{
color: rgba(0, 16, 144, .5);
}
.x-sender
{
font-weight: bold;
display: block;
padding-bottom: 3px;
margin-left: .18em;
}
.message.hide-icons .x-sender,
.IRC.message .x-sender
{
color: rgba(64, 64, 64, 1) !important;
}
.history.message.hide-icons .x-sender,
.IRC.history.message .x-sender
{
color: rgba(64, 64, 64, .5) !important;
}
.x-ltime
{
display: none;
}
.x-rtime
{
float: right;
color: rgba(184, 184, 184, 1);
font-size: .9em;
padding-left: 10px;
margin-top: .12em;
}
.message.history .x-rtime
{
color: rgba(152, 152, 152, 1);
}
.message.x-hover .x-rtime,
.message.history.x-hover .x-rtime
{
color: rgba(120, 120, 120, 1);
}
.status .x-rtime,
.event .x-rtime
{
color: rgba(244, 244, 244, .75);
}
.message .x-mark
{
font-size: 1.2em;
margin-left: -.74em;
margin-top: -.17em;
letter-spacing: -.2em;
float: left;
}
.message .x-mark:before
{
content: "\2023\2023";
}
.message:not(.history) .x-mark
{
color: rgba(208, 208, 208, 1) !important;
}
.message.history .x-mark
{
color: rgba(176, 176, 176, .5) !important;
}
.status .x-mark,
.event .x-mark
{
display: none;
}
.message.hide-icons .x-mark,
.status.hide-icons .x-mark,
.event.hide-icons .x-mark,
.IRC .x-mark
{
left: 10px;
}
.message .x-message
{
display: block;
margin-left: .18em;
}
.status .x-message
{
word-wrap: break-word;
padding-top: 20px;
}
.message.history.x-hover .x-message
{
color: rgba(112, 112, 112, 1);
}
.message.history .x-message
{
color: rgba(136, 136, 136, 1);
}
img.emoticon
{
vertical-align: top;
}
.history img.emoticon
{
opacity: .4;
}
a,
a:link
{
color: inherit;
text-decoration: underline;
}
a:hover
{
border-bottom: 1px solid;
}
a:active
{
border-bottom: 2px solid;
}
img.fullSizeImage
{
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
}
img.scaledToFitImage
{
width: auto;
max-height: 10px;
}
.message.mention .x-message:before
{
position: absolute;
margin-top: -1px;
content: "\23af";
right: 4px;
font-size: 12px;
content: "\2605";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
}
.history.message.mention .x-message:before
{
position: absolute;
margin-top: -1px;
content: "\23af";
right: 4px;
font-size: 12px;
content: "\2605";
color: rgba(64, 64, 64, 1);
text-shadow: 0px 1px 1px rgba(255, 255, 255, 1);
opacity: .5;
}
.message.hide-icons .x-color,
.IRC.message .x-color
{
display: block;
width: 5px;
height: 18px;
position: absolute;
left: 12px;
-webkit-border-top-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
z-index: 99;
margin-top: -2px;
background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, .75)), to(rgba(255, 255, 255, .25)));
}
.history.message .x-color
{
opacity: .5;
}
.status .x-color,
.event .x-color,
.consecutive .x-color
{
display: none !important;
}
.message
{
background: none !important;
}
.message:not(.consecutive)
{
background: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 1)), to(rgba(240, 240, 240, 1))) !important;
}
.message:not(.consecutive).x-hover
{
background: -webkit-gradient(linear, left bottom, left top, from(rgba(255, 255, 255, 1)), to(rgba(240, 240, 240, 1))) !important;
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, .4), 0px 0px 1px rgba(0, 0, 0, .4);
}
.message:not(.consecutive).history
{
background: -webkit-gradient(linear, left top, left bottom, from(rgba(232, 232, 232, .75)), to(rgba(208, 208, 208, .75))) !important;
}
.message:not(.consecutive).history.x-hover
{
background: -webkit-gradient(linear, left bottom, left top, from(rgba(232, 232, 232, .75)), to(rgba(208, 208, 208, .75))) !important;
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, .4), 0px 0px 1px rgba(0, 0, 0, .4);
}
/* header */
#x-wrap
{
-webkit-box-shadow: 0px 4px 16px rgba(0, 0, 0, .75), 8px -8px 8px rgba(149, 175, 219, 1), -8px -8px 8px rgba(149, 175, 219, 1), -8px 16px 16px rgba(163, 185, 223, 1), 8px 16px 16px rgba(163, 185, 223, 1) !important;
}
#x-header .x-iconmask
{
height: 48px !important;
width: 48px !important;
top: 4px !important;
}
#x-header img.x-icon
{
position: absolute;
background: rgba(240, 240, 240, 1);
-webkit-border-radius: 5px !important;
width: 48px !important;
min-width: 48px !important;
height: 48px !important;
min-height: 48px !important;
top: 4px !important;
}
#x-header .x-icon.x-incoming
{
left: 4px !important;
}
#x-header .x-icon.x-outgoing
{
right: 4px !important;
}
#x-header .x-sender.x-incoming {
left: 56px !important;
color: rgba(168, 0, 40, 1);
}
#x-header .x-sender.x-outgoing {
right: 56px !important;
color: rgba(0, 16, 144, 1);
}
#x-wrap .x-iconmask,
#x-wrap:not(.IRC) .x-color
{
display: none !important;
}
#x-wrap.IRC img.x-icon,
#x-wrap.IRC .x-iconmask
{
display: none;
}
#x-wrap.IRC .x-sender.x-incoming
{
left: 8px !important;
}
#x-wrap.IRC .x-sender.x-outgoing
{
right: 8px !important;
}
#x-wrap.IRC .x-color.x-incoming
{
left: 0px !important;
-webkit-border-top-left-radius: 5px !important;
-webkit-border-bottom-left-radius: 5px !important;
}
#x-wrap.IRC .x-color.x-outgoing
{
right: 0px !important;
-webkit-border-top-right-radius: 5px !important;
-webkit-border-bottom-right-radius: 5px !important;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
@charset "utf-8";
#x-topic #x-wrap
{
padding-top: 6px;
padding-bottom: 6px;
position: fixed;
top: 8px;
right: 18px;
left: 8px;
z-index: 100;
color: rgba(64, 64, 64, 1);
background:
-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 1)), to(rgba(248, 248, 248, 1))) 0px 0px no-repeat,
-webkit-gradient(linear, left top, left bottom, from(rgba(248, 248, 248, 1)), to(rgba(240,240, 240, 1))) 0px 16px no-repeat;
-webkit-background-size: 100% 16px, 100% 100%;
-webkit-border-radius: 5px;
-webkit-box-shadow: 0px 4px 16px rgba(0, 0, 0, .75), 8px -8px 8px rgba(255, 255, 255, 1), -8px -8px 8px rgba(255, 255, 255, 1), -8px 16px 16px rgba(248, 248, 248, 1), 8px 16px 16px rgba(248, 248, 248, 1);
opacity: 1;
-webkit-transition: opacity .4s linear;
min-width: 7em;
}
#x-topic[title=""]:not(:hover)
{
opacity: 0;
}
#x-topic #topicEdit
{
padding-left: 8px;
padding-right: 8px;
display: block;
min-height: 1.2em;
word-wrap: break-word;
word-break: break-word;
}
#x-topic .x-serviceIcon
{
position: relative;
right: 4px;
float: right;
margin-left: 10px;
}
#x-topic img.x-serviceIcon
{
height: 1.6em;
margin-top: -.2em;
margin-right: -.2em;
margin-bottom: -.2em;
}
/* toggle */
.x-toggle
{
position: fixed;
top: 4px;
left: 4px;
width: 16px;
height: 16px;
font-size: 21px !important;
font-family: "Apple Symbols";
z-index: 999;
cursor: pointer;
opacity: 0;
-webkit-transition: opacity .4s linear;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 1)), to(rgba(240, 240, 240, 1))) 0px 0px no-repeat;
-webkit-border-radius: 5px;
-webkit-box-shadow: 0px 4px 16px rgba(0, 0, 0, .5);
color: rgba(64, 64, 64, 1);
text-align: center;
}
.x-toggle:active
{
background: -webkit-gradient(linear, left top, left bottom, from(rgba(240, 240, 240, 1)), to(rgba(255, 255, 255, 1))) 0px 0px no-repeat;
}
body:hover .x-toggle
{
opacity: .5;
}
#x-topic:hover .x-toggle
{
opacity: 1;
}
#x-hide:before
{
content: "\2299";
position: fixed;
top: 3px;
left: 4px;
}
#x-hide:hover:before
{
content: "\2296";
}
#x-hide:active:before
{
content: "\2299";
}
#x-show:before
{
position: fixed;
top: 3px;
left: 4px;
content: "\2299";
}
#x-show:hover:before
{
content: "\2295";
}
#x-show:active:before
{
content: "\2299";
}
<div class="{message.css_classes} {user_icons}" user="{message.sender.name}" style="background-color: {message.sender.color};">
<img class="x-icon" src="{message.sender.iconpath}" />
<span class="x-iconmask" style="-webkit-mask-box-image: {message.sender.iconpath};"></span>
<span class="x-color" style="background-color: {message.sender.color};" src="{message.sender.iconpath}"></span>
<span class="x-wrap">
<span class="x-sender">{message.sender.name} </span>
<span class="x-rtime" title="{message.date}">{message.time} </span>
<span class="x-ltime" title="{message.date}">{message.time} </span>
<span class="x-mark"></span>
<span class="x-message" title="{message.time}">{message.message} </span>
</span>
<span id="insert"></span>
</div>
<div class="{message.css_classes} {user_icons}" user="{message.sender.name}" style="background-color: {message.sender.color};">
<img class="x-icon" src="{message.sender.iconpath}" />
<span class="x-iconmask" style="-webkit-mask-box-image: {message.sender.iconpath};"></span>
<span class="x-color" style="background-color: {message.sender.color};" src="{message.sender.iconpath}"></span>
<span class="x-wrap">
<span class="x-sender">{message.sender.name} </span>
<span class="x-rtime" title="{message.date}">{message.time} </span>
<span class="x-ltime" title="{message.date}">{message.time} </span>
<span class="x-mark"></span>
<span class="x-message" title="{message.time}">{message.message} </span>
</span>
<span id="insert"></span>
</div>
<div class="{message.css_classes} {user_icons}">
<span class="x-wrap">
<span class="x-rtime" title="{message.date}">{message.time} </span>
<span class="x-ltime" title="{message.date}">{message.time} </span>
<span class="x-mark"></span>
<span class="x-message">{message.message} </span>
</span>
<span id="insert"></span>
</div>
<div id="x-topic">
<style type="text/css">
@import url(../css/topic.css);
</style>
<div id="x-hide" class="x-toggle" onClick="hide_header();"></div>
<div id="x-show" class="x-toggle" onClick="show_header();" style="display: none"></div>
<div id="x-wrap" class="%service%">
<img class="x-serviceIcon" title="%service%" alt="%service%" src="%serviceIconPath%" />
%topic%
</div>
</div>
<?xml version="1.0" encoding="UTF-8"?>
<style version="1.0">
<default_variant>Icon-Time</default_variant>
<font_family>Tahoma, "Liberation Sans", sans-serif</font_family>
<font_size>11</font_size>
</style>
This diff is collapsed.
#!/usr/bin/python
from PyQt4.QtCore import QUrl
from PyQt4.QtGui import QApplication
from PyQt4.QtWebKit import QWebView, QWebSettings
app = QApplication([])
view = QWebView()
settings = view.settings()
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
view.load(QUrl('mockup.html'))
view.show()
app.exec_()
@import url("css/base.css");
@import url("css/incoming-blue.css");
@import url("css/outgoing-green.css");
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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