Commit 99163a70 authored by Dan Pascu's avatar Dan Pascu

Added screen sharing support

parent 92ab48d0
......@@ -14,4 +14,5 @@
^MANIFEST$
(^|/)\.directory$
(^|/)\.komodotools$
(^|/)blink/screensharing/_rfb.c$
(^|/)resources/icons/work/sandbox($|/)
......@@ -125,6 +125,8 @@ class Blink(QApplication):
self.chat_window.addAction(self.main_window.preferences_action)
self.chat_window.addAction(self.main_window.transfers_window_action)
self.chat_window.addAction(self.main_window.logs_window_action)
self.chat_window.addAction(self.main_window.received_files_window_action)
self.chat_window.addAction(self.main_window.screenshots_window_action)
self.ip_address_monitor = IPAddressMonitor()
self.log_manager = LogManager()
......
......@@ -646,6 +646,8 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
sliding_panels = True
__streamtypes__ = {'chat', 'screen-sharing', 'video'} # the stream types for which we show the chat window
def __init__(self, parent=None):
super(ChatWindow, self).__init__(parent)
with Resources.directory:
......@@ -732,6 +734,9 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.control_button.actions.disconnect = QAction("Disconnect", self, triggered=self._AH_Disconnect)
self.control_button.actions.add_audio = QAction("Add audio", self, triggered=self._AH_AddAudio)
self.control_button.actions.remove_audio = QAction("Remove audio", self, triggered=self._AH_RemoveAudio)
self.control_button.actions.share_my_screen = QAction("Share my screen", self, triggered=self._AH_ShareMyScreen)
self.control_button.actions.request_screen = QAction("Request screen", self, triggered=self._AH_RequestScreen)
self.control_button.actions.end_screen_sharing = QAction("End screen sharing", self, triggered=self._AH_EndScreenSharing)
self.control_button.actions.dump_session = QAction("Dump session", self, triggered=self._AH_DumpSession) # remove later -Dan
self.control_button.actions.main_window = QAction("Main Window", self, triggered=self._AH_MainWindow, shortcut='Ctrl+B', shortcutContext=Qt.ApplicationShortcut)
......@@ -852,7 +857,16 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
else:
menu.addAction(self.control_button.actions.disconnect)
if state == 'connected':
menu.addAction(self.control_button.actions.add_audio if 'audio' not in blink_session.streams else self.control_button.actions.remove_audio)
stream_types = blink_session.streams.types
if 'audio' not in stream_types:
menu.addAction(self.control_button.actions.add_audio)
elif stream_types != {'audio'} and not stream_types.intersection({'screen-sharing', 'video'}):
menu.addAction(self.control_button.actions.remove_audio)
if 'screen-sharing' not in stream_types:
menu.addAction(self.control_button.actions.request_screen)
menu.addAction(self.control_button.actions.share_my_screen)
elif stream_types != {'screen-sharing'}:
menu.addAction(self.control_button.actions.end_screen_sharing)
#menu.addAction(self.control_button.actions.dump_session) # remove this later -Dan
self.control_button.setMenu(menu)
......@@ -865,6 +879,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
have_session = blink_session.state in ('connecting/*', 'connected/*', 'ending')
have_audio = 'audio' in blink_session.streams
have_chat = 'chat' in blink_session.streams
have_screen = 'screen-sharing' in blink_session.streams
if update_visibility:
self.status_value_label.setEnabled(have_session)
......@@ -877,11 +892,13 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.audio_ice_status_value_label.setEnabled(have_audio)
self.chat_value_widget.setEnabled(have_chat)
self.chat_addresses_value_label.setEnabled(have_chat)
self.screen_value_widget.setEnabled(have_screen)
session_info = blink_session.info
audio_info = blink_session.info.streams.audio
video_info = blink_session.info.streams.video
chat_info = blink_session.info.streams.chat
screen_info = blink_session.info.streams.screen_sharing
if 'status' in elements and blink_session.state in ('initialized', 'connecting/*', 'connected/*', 'ended'):
state_map = {'initialized': 'Disconnected',
......@@ -954,6 +971,14 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
else:
self.chat_addresses_value_label.setText(u'N/A')
if screen_info.remote_address is not None and screen_info.mode == 'active':
self.screen_value_label.setText(u'Viewing remote')
elif screen_info.remote_address is not None and screen_info.mode == 'passive':
self.screen_value_label.setText(u'Sharing local')
else:
self.screen_value_label.setText(u'N/A')
self.screen_encryption_label.setVisible(screen_info.remote_address is not None and screen_info.transport=='tls')
if 'statistics' in elements:
self.duration_value_label.value = session_info.duration
self.audio_latency_graph.data = audio_info.latency
......@@ -1132,11 +1157,11 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.traffic_graph.update()
def _NH_BlinkSessionNewIncoming(self, notification):
if 'chat' in notification.sender.streams.types:
if notification.sender.streams.types.intersection(self.__streamtypes__):
self.show()
def _NH_BlinkSessionNewOutgoing(self, notification):
if 'chat' in notification.sender.stream_descriptions.types:
if notification.sender.stream_descriptions.types.intersection(self.__streamtypes__):
self.show()
def _NH_BlinkSessionDidReinitializeForIncoming(self, notification):
......@@ -1145,7 +1170,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
selection_model = self.session_list.selectionModel()
selection_model.select(model.index(position), selection_model.ClearAndSelect)
self.session_list.scrollTo(model.index(position), QListView.EnsureVisible) # or PositionAtCenter
if 'chat' in notification.sender.streams.types:
if notification.sender.streams.types.intersection(self.__streamtypes__):
self.show()
def _NH_BlinkSessionDidReinitializeForOutgoing(self, notification):
......@@ -1154,7 +1179,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
selection_model = self.session_list.selectionModel()
selection_model.select(model.index(position), selection_model.ClearAndSelect)
self.session_list.scrollTo(model.index(position), QListView.EnsureVisible) # or PositionAtCenter
if 'chat' in notification.sender.stream_descriptions.types:
if notification.sender.stream_descriptions.types.intersection(self.__streamtypes__):
self.show()
# use BlinkSessionNewIncoming/Outgoing to show the chat window if there is a chat stream available (like with reinitialize) instead of using the sessionAdded signal from the model -Dan
......@@ -1163,7 +1188,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
# different class for them that posts different notifications. in that case we can do in in NewIncoming/Outgoing -Dan
def _NH_BlinkSessionWillAddStream(self, notification):
if notification.data.stream.type == 'chat':
if notification.data.stream.type in self.__streamtypes__:
self.show()
def _NH_BlinkSessionDidRemoveStream(self, notification):
......@@ -1421,6 +1446,21 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
def _AH_RemoveAudio(self):
self.selected_session.blink_session.remove_stream(self.selected_session.blink_session.streams.get('audio'))
def _AH_RequestScreen(self):
if 'audio' in self.selected_session.blink_session.streams:
self.selected_session.blink_session.add_stream(StreamDescription('screen-sharing', mode='viewer'))
else:
self.selected_session.blink_session.add_streams([StreamDescription('screen-sharing', mode='viewer'), StreamDescription('audio')])
def _AH_ShareMyScreen(self):
if 'audio' in self.selected_session.blink_session.streams:
self.selected_session.blink_session.add_stream(StreamDescription('screen-sharing', mode='server'))
else:
self.selected_session.blink_session.add_streams([StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _AH_EndScreenSharing(self):
self.selected_session.blink_session.remove_stream(self.selected_session.blink_session.streams.get('screen-sharing'))
def _AH_DumpSession(self):
blink_session = self.selected_session.blink_session
print "state: %r" % blink_session.state
......
......@@ -114,6 +114,13 @@ class ChatWindowSettings(SettingsGroup):
font_size = Setting(type=int, default=None, nillable=True)
class BlinkScreenSharingSettings(SettingsGroup):
screenshots_directory = Setting(type=Path, default=Path(os.path.expanduser('~/Downloads')))
scale = Setting(type=bool, default=True)
open_fullscreen = Setting(type=bool, default=False)
open_viewonly = Setting(type=bool, default=False)
class BlinkPresenceSettings(SettingsGroup):
current_state = Setting(type=PresenceState, default=PresenceState('Available'))
state_history = Setting(type=PresenceStateList, default=PresenceStateList())
......@@ -126,4 +133,5 @@ class BlinkSettings(SettingsObject):
chat_window = ChatWindowSettings
presence = BlinkPresenceSettings
screen_sharing = BlinkScreenSharingSettings
......@@ -3060,8 +3060,8 @@ class ContactListView(QListView):
menu.addAction(self.actions.start_chat_session)
#menu.addAction(self.actions.send_sms)
menu.addAction(self.actions.send_files)
#menu.addAction(self.actions.request_screen)
#menu.addAction(self.actions.share_my_screen)
menu.addAction(self.actions.request_screen)
menu.addAction(self.actions.share_my_screen)
menu.addSeparator()
menu.addAction(self.actions.add_group)
menu.addAction(self.actions.add_contact)
......@@ -3307,10 +3307,14 @@ class ContactListView(QListView):
session_manager.send_file(contact, contact.uri, filename)
def _AH_RequestScreen(self):
pass
contact = self.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
session_manager = SessionManager()
session_manager.create_session(contact, contact.uri, [StreamDescription('screen-sharing', mode='viewer'), StreamDescription('audio')])
def _AH_ShareMyScreen(self):
pass
contact = self.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
session_manager = SessionManager()
session_manager.create_session(contact, contact.uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _DH_ApplicationXBlinkGroupList(self, event, index, rect, item):
model = self.model()
......@@ -3459,8 +3463,8 @@ class ContactSearchListView(QListView):
menu.addAction(self.actions.start_chat_session)
#menu.addAction(self.actions.send_sms)
menu.addAction(self.actions.send_files)
#menu.addAction(self.actions.request_screen)
#menu.addAction(self.actions.share_my_screen)
menu.addAction(self.actions.request_screen)
menu.addAction(self.actions.share_my_screen)
menu.addSeparator()
menu.addAction(self.actions.edit_item)
menu.addAction(self.actions.delete_item)
......@@ -3640,10 +3644,14 @@ class ContactSearchListView(QListView):
session_manager.send_file(contact, contact.uri, filename)
def _AH_RequestScreen(self):
pass
contact = self.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
session_manager = SessionManager()
session_manager.create_session(contact, contact.uri, [StreamDescription('screen-sharing', mode='viewer'), StreamDescription('audio')])
def _AH_ShareMyScreen(self):
pass
contact = self.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
session_manager = SessionManager()
session_manager.create_session(contact, contact.uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _DH_TextUriList(self, event, index, rect, item):
if index.isValid():
......@@ -3736,8 +3744,8 @@ class ContactDetailView(QListView):
menu.addAction(self.actions.start_chat_session)
#menu.addAction(self.actions.send_sms)
menu.addAction(self.actions.send_files)
#menu.addAction(self.actions.request_screen)
#menu.addAction(self.actions.share_my_screen)
menu.addAction(self.actions.request_screen)
menu.addAction(self.actions.share_my_screen)
menu.addSeparator()
if isinstance(selected_item, ContactURI) and model.contact_detail.editable:
menu.addAction(self.actions.make_uri_default)
......@@ -3875,10 +3883,26 @@ class ContactDetailView(QListView):
session_manager.send_file(contact, selected_uri, filename)
def _AH_RequestScreen(self):
pass
contact = self.contact_list.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
selected_indexes = self.selectionModel().selectedIndexes()
item = selected_indexes[0].data(Qt.UserRole) if selected_indexes else None
if isinstance(item, ContactURI):
selected_uri = item.uri
else:
selected_uri = contact.uri
session_manager = SessionManager()
session_manager.create_session(contact, selected_uri, [StreamDescription('screen-sharing', mode='viewer'), StreamDescription('audio')])
def _AH_ShareMyScreen(self):
pass
contact = self.contact_list.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
selected_indexes = self.selectionModel().selectedIndexes()
item = selected_indexes[0].data(Qt.UserRole) if selected_indexes else None
if isinstance(item, ContactURI):
selected_uri = item.uri
else:
selected_uri = contact.uri
session_manager = SessionManager()
session_manager.create_session(contact, selected_uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _DH_ApplicationXBlinkSession(self, event, index, rect, item):
event.ignore(rect)
......
......@@ -205,6 +205,8 @@ class MainWindow(base_class, ui_class):
self.chat_window_action.triggered.connect(self._AH_ChatWindowActionTriggered)
self.transfers_window_action.triggered.connect(self._AH_TransfersWindowActionTriggered)
self.logs_window_action.triggered.connect(self._AH_LogsWindowActionTriggered)
self.received_files_window_action.triggered.connect(self._AH_ReceivedFilesWindowActionTriggered)
self.screenshots_window_action.triggered.connect(self._AH_ScreenshotsWindowActionTriggered)
def setupUi(self):
super(MainWindow, self).setupUi(self)
......@@ -216,6 +218,11 @@ class MainWindow(base_class, ui_class):
self.input_devices_group = QActionGroup(self)
self.alert_devices_group = QActionGroup(self)
self.request_screen_action = QAction('Request screen', self, triggered=self._AH_RequestScreenActionTriggered)
self.share_my_screen_action = QAction('Share my screen', self, triggered=self._AH_ShareMyScreenActionTriggered)
self.screen_sharing_button.addAction(self.request_screen_action)
self.screen_sharing_button.addAction(self.share_my_screen_action)
# adjust search box height depending on theme as the value set in designer isn't suited for all themes
search_box = self.search_box
option = QStyleOptionFrameV2()
......@@ -251,7 +258,7 @@ class MainWindow(base_class, ui_class):
def enable_call_buttons(self, enabled):
self.audio_call_button.setEnabled(enabled)
self.chat_session_button.setEnabled(enabled)
self.screen_sharing_button.setEnabled(False)
self.screen_sharing_button.setEnabled(enabled)
def load_audio_devices(self):
settings = SIPSimpleSettings()
......@@ -386,6 +393,18 @@ class MainWindow(base_class, ui_class):
makedirs(directory)
QDesktopServices.openUrl(QUrl.fromLocalFile(directory))
def _AH_ReceivedFilesWindowActionTriggered(self, checked):
settings = SIPSimpleSettings()
directory = settings.file_transfer.directory.normalized
makedirs(directory)
QDesktopServices.openUrl(QUrl.fromLocalFile(directory))
def _AH_ScreenshotsWindowActionTriggered(self, checked):
settings = BlinkSettings()
directory = settings.screen_sharing.screenshots_directory.normalized
makedirs(directory)
QDesktopServices.openUrl(QUrl.fromLocalFile(directory))
def _AH_VoicemailActionTriggered(self, action, checked):
account = action.data()
contact, contact_uri = URIUtils.find_contact(account.voicemail_uri, display_name='Voicemail')
......@@ -496,6 +515,34 @@ class MainWindow(base_class, ui_class):
session_manager = SessionManager()
session_manager.create_session(contact, contact_uri, [StreamDescription('chat')], connect=False)
def _AH_RequestScreenActionTriggered(self):
list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
if list_view.detail_view.isVisible():
list_view.detail_view._AH_RequestScreen()
else:
selected_indexes = list_view.selectionModel().selectedIndexes()
if selected_indexes:
contact = selected_indexes[0].data(Qt.UserRole)
contact_uri = contact.uri
else:
contact, contact_uri = URIUtils.find_contact(self.search_box.text())
session_manager = SessionManager()
session_manager.create_session(contact, contact_uri, [StreamDescription('screen-sharing', mode='viewer'), StreamDescription('audio')])
def _AH_ShareMyScreenActionTriggered(self):
list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
if list_view.detail_view.isVisible():
list_view.detail_view._AH_ShareMyScreen()
else:
selected_indexes = list_view.selectionModel().selectedIndexes()
if selected_indexes:
contact = selected_indexes[0].data(Qt.UserRole)
contact_uri = contact.uri
else:
contact, contact_uri = URIUtils.find_contact(self.search_box.text())
session_manager = SessionManager()
session_manager.create_session(contact, contact_uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _SH_BreakConference(self):
active_session = self.session_list.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
self.session_model.breakConference(active_session.client_conference)
......
......@@ -286,6 +286,12 @@ class PreferencesWindow(base_class, ui_class):
self.session_info_style_button.clicked.connect(self._SH_SessionInfoStyleButtonClicked)
self.traffic_units_button.clicked.connect(self._SH_TrafficUnitsButtonClicked)
# Screen sharing
self.screenshots_directory_browse_button.clicked.connect(self._SH_ScreenshotsDirectoryBrowseButtonClicked)
self.screen_sharing_scale_button.clicked.connect(self._SH_ScreenSharingScaleButtonClicked)
self.screen_sharing_fullscreen_button.clicked.connect(self._SH_ScreenSharingFullscreenButtonClicked)
self.screen_sharing_viewonly_button.clicked.connect(self._SH_ScreenSharingViewonlyButtonClicked)
# File transfer
self.download_directory_browse_button.clicked.connect(self._SH_DownloadDirectoryBrowseButtonClicked)
self.file_transfer_alert_button.clicked.connect(self._SH_FileTransferAlertButtonClicked)
......@@ -591,6 +597,12 @@ class PreferencesWindow(base_class, ui_class):
self.session_info_style_button.setChecked(blink_settings.chat_window.session_info.alternate_style)
self.traffic_units_button.setChecked(blink_settings.chat_window.session_info.bytes_per_second)
# Screen sharing settings
self.screenshots_directory_editor.setText(blink_settings.screen_sharing.screenshots_directory or u'')
self.screen_sharing_scale_button.setChecked(blink_settings.screen_sharing.scale)
self.screen_sharing_fullscreen_button.setChecked(blink_settings.screen_sharing.open_fullscreen)
self.screen_sharing_viewonly_button.setChecked(blink_settings.screen_sharing.open_viewonly)
# File transfer settings
self.download_directory_editor.setText(settings.file_transfer.directory or u'')
self.file_transfer_alert_button.setChecked(settings.sounds.play_file_alerts)
......@@ -1290,6 +1302,34 @@ class PreferencesWindow(base_class, ui_class):
settings.chat_window.session_info.bytes_per_second = checked
settings.save()
# Screen sharing signal handlers
def _SH_ScreenshotsDirectoryBrowseButtonClicked(self, checked):
# TODO: open the file selection dialog in non-modal mode. Same for the one for TLS CA list and the IconSelector from contacts. -Dan
settings = BlinkSettings()
directory = settings.screen_sharing.screenshots_directory or os.path.expanduser('~')
directory = QFileDialog.getExistingDirectory(self, u'Select Screenshots Directory', directory) or None
if directory is not None:
directory = os.path.normpath(directory)
if directory != settings.screen_sharing.screenshots_directory:
self.screenshots_directory_editor.setText(directory)
settings.screen_sharing.screenshots_directory = directory
settings.save()
def _SH_ScreenSharingScaleButtonClicked(self, checked):
settings = BlinkSettings()
settings.screen_sharing.scale = checked
settings.save()
def _SH_ScreenSharingFullscreenButtonClicked(self, checked):
settings = BlinkSettings()
settings.screen_sharing.open_fullscreen = checked
settings.save()
def _SH_ScreenSharingViewonlyButtonClicked(self, checked):
settings = BlinkSettings()
settings.screen_sharing.open_viewonly = checked
settings.save()
# File transfer signal handlers
def _SH_DownloadDirectoryBrowseButtonClicked(self, checked):
# TODO: open the file selection dialog in non-modal mode. Same for the one for TLS CA list and the IconSelector from contacts. -Dan
......
......@@ -93,8 +93,8 @@ class BlinkPresenceState(object):
service.capabilities.text = False
service.capabilities.message = True
service.capabilities.file_transfer = True
service.capabilities.screen_sharing_server = False
service.capabilities.screen_sharing_client = False
service.capabilities.screen_sharing_server = True
service.capabilities.screen_sharing_client = True
service.display_name = self.account.display_name or None
service.icon = "%s#blink-icon%s" % (self.account.xcap.icon.url, self.account.xcap.icon.etag) if self.account.xcap.icon is not None else None
service.device_info = pidf.DeviceInfo(instance_id, description=hostname, user_agent=settings.user_agent)
......
# Copyright (C) 2014 AG Projects. See LICENSE for details.
#
__all__ = ['ScreensharingWindow', 'VNCViewer', 'VNCClient', 'RFBSettings', 'ServerDefault', 'TrueColor', 'HighColor', 'LowColor']
from blink.screensharing.vncclient import VNCClient, RFBSettings, ServerDefault, TrueColor, HighColor, LowColor
from blink.screensharing.vncviewer import ScreensharingWindow, VNCViewer
# Copyright (C) 2014 AG Projects. See LICENSE for details.
#
__all__ = ['RFBClient', 'RFBClientError']
from sip import voidptr
from PyQt4.QtCore import QThread
from PyQt4.QtGui import QImage
from application.notification import NotificationCenter, NotificationData
from libc.stdint cimport uint8_t, uint16_t, uint32_t
from libc.stdlib cimport calloc, malloc, free
from libc.string cimport memcpy, strlen
# external declarations
#
cdef extern from "stdarg.h":
ctypedef struct va_list:
pass
void va_start(va_list, void *arg)
void va_end(va_list)
cdef extern from "Python.h":
object PyString_FromStringAndSize(const char *u, Py_ssize_t size)
object PyUnicode_FromStringAndSize(const char *u, Py_ssize_t size)
int PyOS_vsnprintf(char *buf, size_t size, const char *format, va_list va)
cdef extern from "rfb/rfbclient.h":
ctypedef int rfbBool
# forward declarations
ctypedef struct _rfbClient
enum: rfbCredentialTypeX509=1, rfbCredentialTypeUser=2
ctypedef struct UserCredential:
char *username
char *password
ctypedef union rfbCredential:
#X509Credential x509Credential
UserCredential userCredential
ctypedef struct AppData:
const char* encodingsString
int compressLevel
int qualityLevel
int requestedDepth
rfbBool enableJPEG
rfbBool useRemoteCursor
ctypedef struct rfbPixelFormat:
uint8_t bitsPerPixel
uint8_t depth
uint16_t redMax
uint16_t greenMax
uint16_t blueMax
uint8_t redShift
uint8_t greenShift
uint8_t blueShift
ctypedef struct rfbClientData:
void *data
ctypedef struct UpdateRect:
int x, y, w, h
ctypedef struct rfbServerInitMsg:
uint16_t framebufferWidth
uint16_t framebufferHeight
rfbPixelFormat format # the server's preferred pixel format
# callbacks
ctypedef void (*rfbClientLogProc)(const char *format, ...)
ctypedef rfbBool (*MallocFrameBufferProc)(_rfbClient *client) nogil
ctypedef void (*GotFrameBufferUpdateProc)(_rfbClient *client, int x, int y, int w, int h) nogil
ctypedef char* (*GetPasswordProc)(_rfbClient *client) nogil
ctypedef rfbCredential* (*GetCredentialProc)(_rfbClient *client, int credentialType) nogil
ctypedef void (*GotXCutTextProc)(_rfbClient *client, const char *text, int textlen) nogil
ctypedef void (*GotCursorShapeProc)(_rfbClient *client, int xhot, int yhot, int width, int height, int bytesPerPixel)
ctypedef rfbBool (*HandleCursorPosProc)(_rfbClient *client, int x, int y)
#ctypedef void (*BellProc)(_rfbClient *client)
ctypedef struct _rfbClient:
char *serverHost
int serverPort
int sock
int width, height
uint8_t *frameBuffer
UpdateRect updateRect
AppData appData
rfbPixelFormat format
rfbServerInitMsg si
rfbClientData *clientData
# cursor
uint8_t *rcSource
uint8_t *rcMask
rfbBool canHandleNewFBSize
# callbacks
MallocFrameBufferProc MallocFrameBuffer
GotFrameBufferUpdateProc GotFrameBufferUpdate
GotCursorShapeProc GotCursorShape
HandleCursorPosProc HandleCursorPos
GetPasswordProc GetPassword # the pointer returned will be freed after use!
GetCredentialProc GetCredential # the pointer returned will be freed after use!
GotXCutTextProc GotXCutText
#BellProc Bell
ctypedef _rfbClient rfbClient
# functions
rfbClient* rfbGetClient(int bitsPerSample, int samplesPerPixel, int bytesPerPixel) nogil
void rfbClientCleanup(rfbClient *client) nogil
rfbBool ConnectToRFBServer(rfbClient *client, const char *hostname, int port) nogil
rfbBool InitialiseRFBConnection(rfbClient *client) nogil
rfbBool SetFormatAndEncodings(rfbClient *client) nogil
rfbBool SendFramebufferUpdateRequest(rfbClient *client, int x, int y, int w, int h, rfbBool incremental) nogil
rfbBool SendPointerEvent(rfbClient *client, int x, int y, int buttonMask) nogil
rfbBool SendKeyEvent(rfbClient *client, uint32_t key, rfbBool down) nogil
rfbBool SendClientCutText(rfbClient *client, char *str, int len) nogil
int WaitForMessage(rfbClient *client, unsigned int usecs) nogil
rfbBool HandleRFBServerMessage(rfbClient *client) nogil
# Provide our own strdup implementation because Windows is ... well, Windows
#
cdef char* strdup(const char *string):
cdef size_t len = strlen(string) + 1
cdef void *copy = malloc(len)
return <char*> memcpy(copy, string, len) if copy else NULL
# RFB client implementation
#
class RFBClientError(Exception): pass
cdef class RFBClient:
cdef rfbClient *client
cdef uint8_t *framebuffer
cdef unsigned int framebuffer_size
cdef int connected
cdef readonly object parent
cdef readonly object image
def __cinit__(self, parent, *args, **kw):
cdef char *server_host = NULL
cdef rfbClientData *client_data = NULL
try:
with nogil:
self.client = rfbGetClient(8, 3, 4) # 24 bit color depth in 32 bits per pixel. Will change color depth and bpp later if needed.
server_host = strdup(parent.host)
client_data = <rfbClientData*> calloc(1, sizeof(rfbClientData))
if not server_host or not client_data or not self.client:
raise MemoryError("could not allocate RFB client")
free(self.client.serverHost)
client_data.data = <void*>self
except:
free(server_host)
free(client_data)
raise
self.client.clientData = client_data
self.client.serverHost = server_host
self.client.serverPort = parent.port
self.client.canHandleNewFBSize = True
self.client.appData.useRemoteCursor = False
self.client.MallocFrameBuffer = _malloc_framebuffer_callback
self.client.GotFrameBufferUpdate = _update_framebuffer_callback
self.client.GotCursorShape = _update_cursor_callback
self.client.HandleCursorPos = _update_cursor_position_callback
self.client.GetPassword = _get_password_callback
self.client.GetCredential = _get_credentials_callback
self.client.GotXCutText = _text_cut_callback
self.connected = False
def __init__(self, parent, *args, **kw):
self.parent = parent
self.image = QImage()
def __dealloc__(self):
if self.client:
with nogil:
rfbClientCleanup(self.client)
if self.framebuffer:
if not self.image.isNull():
self.image.setPixel(0, 0, self.image.pixel(0, 0)) # detach the image from the framebuffer we're about to release to avoid race conditions when painting in the GUI thread
free(self.framebuffer)
property framebuffer:
def __get__(self):
return voidptr(<long>self.framebuffer, size=self.framebuffer_size)
property server_depth:
def __get__(self):
return self.client.si.format.depth or None
property socket:
def __get__(self):
return self.client.sock
def configure(self):
depth = self.parent.settings.depth or self.server_depth
format_changed = depth != self.client.format.depth
if depth == 8:
self.client.format.depth = 8
self.client.format.bitsPerPixel = 8
self.client.format.redShift = 0
self.client.format.greenShift = 3
self.client.format.blueShift = 6
self.client.format.redMax = 7
self.client.format.greenMax = 7
self.client.format.blueMax = 3
elif depth == 16:
self.client.format.depth = 16
self.client.format.bitsPerPixel = 16
self.client.format.redShift = 11
self.client.format.greenShift = 5
self.client.format.blueShift = 0
self.client.format.redMax = 0x1f
self.client.format.greenMax = 0x3f
self.client.format.blueMax = 0x1f
elif depth in (24, 32):
self.client.format.depth = depth
self.client.format.bitsPerPixel = 32
self.client.format.redShift = 16
self.client.format.greenShift = 8
self.client.format.blueShift = 0
self.client.format.redMax = 0xff
self.client.format.greenMax = 0xff
self.client.format.blueMax = 0xff
self.client.appData.requestedDepth = self.client.format.depth
self.client.appData.enableJPEG = bool(self.client.format.bitsPerPixel != 8)
self.client.appData.encodingsString = self.parent.settings.encodings
self.client.appData.compressLevel = self.parent.settings.compression
self.client.appData.qualityLevel = self.parent.settings.quality
if self.connected:
with nogil:
result = SetFormatAndEncodings(self.client)
if not result:
raise RFBClientError("failed to set format and encodings")
with nogil:
result = SendFramebufferUpdateRequest(self.client, self.client.updateRect.x, self.client.updateRect.y, self.client.updateRect.w, self.client.updateRect.h, False)
if not result:
raise RFBClientError("failed to refresh screen after changing format and encodings")
if format_changed:
if not self.framebuffer:
self.image = QImage(self.client.width, self.client.height, QImage.Format_Invalid)
elif self.client.format.bitsPerPixel == 32:
self.image = QImage(voidptr(<long>self.framebuffer, size=self.framebuffer_size), self.client.width, self.client.height, QImage.Format_RGB32)
elif self.client.format.bitsPerPixel == 16:
self.image = QImage(voidptr(<long>self.framebuffer, size=self.framebuffer_size), self.client.width, self.client.height, QImage.Format_RGB16)
elif self.client.format.bitsPerPixel == 8:
self.image = QImage(voidptr(<long>self.framebuffer, size=self.framebuffer_size), self.client.width, self.client.height, QImage.Format_Indexed8)
else:
self.image = QImage(self.client.width, self.client.height, QImage.Format_Invalid)
def connect(self):
cdef rfbBool result
if self.connected:
return
with nogil:
result = ConnectToRFBServer(self.client, self.client.serverHost, self.client.serverPort)
if not result:
raise RFBClientError("could not connect")
with nogil:
result = InitialiseRFBConnection(self.client)
if not result:
raise RFBClientError("could not initialise connection")
self.client.width = self.client.si.framebufferWidth
self.client.height = self.client.si.framebufferHeight
self.client.updateRect.x = 0
self.client.updateRect.y = 0
self.client.updateRect.w = self.client.width
self.client.updateRect.h = self.client.height
self.configure()
self._malloc_framebuffer_callback()
if not self.framebuffer:
raise RFBClientError("could not allocate framebuffer memory")
with nogil:
result = SetFormatAndEncodings(self.client)
if not result:
raise RFBClientError("could not set format and encodings")
with nogil:
result = SendFramebufferUpdateRequest(self.client, self.client.updateRect.x, self.client.updateRect.y, self.client.updateRect.w, self.client.updateRect.h, False)
if not result:
raise RFBClientError("could not request framebuffer update")
self.connected = True
def handle_server_message(self):
cdef int result
with nogil:
result = HandleRFBServerMessage(self.client)
if not result:
raise RFBClientError("could not process server message")
def send_key_event(self, uint32_t key, rfbBool down):
cdef int result
if not self.connected:
return
with nogil:
result = SendKeyEvent(self.client, key, down)
if not result:
raise RFBClientError("could not send key event")
def send_pointer_event(self, int x, int y, int button_mask):
cdef int result
if not self.connected:
return
with nogil:
result = SendPointerEvent(self.client, x, y, button_mask)
if not result:
raise RFBClientError("could not send pointer event")
def send_client_cut_text(self, unicode text):
cdef int result, strlen
cdef bytes text_utf8
cdef char *string
if not self.connected:
return
text_utf8 = text.encode('utf8')
string = text_utf8
strlen = len(text_utf8)
with nogil:
result = SendClientCutText(self.client, string, strlen)
if not result:
raise RFBClientError("could not send client cut text")
cdef rfbBool _malloc_framebuffer_callback(self):
if self.framebuffer:
if not self.image.isNull():
self.image.setPixel(0, 0, self.image.pixel(0, 0)) # detach the image from the framebuffer we're about to release to avoid race conditions when painting in the GUI thread
free(self.framebuffer)
self.framebuffer_size = self.client.width * self.client.height * 4 # always allocate a framebuffer that can hold 32bpp so we can change the depth mid-session without reallocating
self.framebuffer = <uint8_t*> malloc(self.framebuffer_size)
self.client.frameBuffer = self.framebuffer
if not self.framebuffer:
self.image = QImage(self.client.width, self.client.height, QImage.Format_Invalid)
elif self.client.format.bitsPerPixel == 32:
self.image = QImage(voidptr(<long>self.framebuffer, size=self.framebuffer_size), self.client.width, self.client.height, QImage.Format_RGB32)
elif self.client.format.bitsPerPixel == 16:
self.image = QImage(voidptr(<long>self.framebuffer, size=self.framebuffer_size), self.client.width, self.client.height, QImage.Format_RGB16)
elif self.client.format.bitsPerPixel == 8:
self.image = QImage(voidptr(<long>self.framebuffer, size=self.framebuffer_size), self.client.width, self.client.height, QImage.Format_Indexed8)
else:
self.image = QImage(self.client.width, self.client.height, QImage.Format_Invalid)
self.parent.imageSizeChanged.emit(self.image.size())
return bool(<long>self.framebuffer)
cdef void _update_framebuffer_callback(self, int x, int y, int w, int h):
self.parent.imageChanged.emit(x, y, w, h)
cdef void _update_cursor_callback(self, int xhot, int yhot, int width, int height, int bytes_per_pixel):
image = PyString_FromStringAndSize(<const char*>self.client.rcSource, width*height*bytes_per_pixel)
mask = PyString_FromStringAndSize(<const char*>self.client.rcMask, width*height)
#print "-- update cursor shape:", xhot, yhot, width, height, bytes_per_pixel, repr(image), repr(mask)
cdef rfbBool _update_cursor_position_callback(self, int x, int y):
#print "-- update cursor position to:", x, y
return True
cdef char* _get_password_callback(self):
self.parent.passwordRequested.emit(False)
return NULL if self.parent.password is None else strdup(<bytes>self.parent.password.encode('utf8'))
cdef rfbCredential* _get_credentials_callback(self, int credentials_type):
cdef rfbCredential *credential = NULL
if credentials_type == rfbCredentialTypeUser:
self.parent.passwordRequested.emit(True)
if self.parent.username is not None is not self.parent.password:
credential = <rfbCredential*> malloc(sizeof(rfbCredential))
credential.userCredential.username = strdup(<bytes>self.parent.username.encode('utf8'))
credential.userCredential.password = strdup(<bytes>self.parent.password.encode('utf8'))
return credential
cdef void _text_cut_callback(self, const char *text, int length):
cut_text = PyUnicode_FromStringAndSize(text, length)
if cut_text:
self.parent.textCut.emit(cut_text)
# callbacks
#
cdef rfbBool _malloc_framebuffer_callback(rfbClient *client) with gil:
instance = <RFBClient> client.clientData.data
return instance._malloc_framebuffer_callback()
cdef void _update_framebuffer_callback(rfbClient *client, int x, int y, int w, int h) with gil:
instance = <RFBClient> client.clientData.data
instance._update_framebuffer_callback(x, y, w, h)
cdef void _update_cursor_callback(rfbClient *client, int xhot, int yhot, int width, int height, int bytes_per_pixel) with gil:
instance = <RFBClient> client.clientData.data
instance._update_cursor_callback(xhot, yhot, width, height, bytes_per_pixel)
cdef rfbBool _update_cursor_position_callback(rfbClient *client, int x, int y) with gil:
instance = <RFBClient> client.clientData.data
return instance._update_cursor_position_callback(x, y)
cdef char* _get_password_callback(rfbClient *client) with gil:
instance = <RFBClient> client.clientData.data
return instance._get_password_callback()
cdef rfbCredential* _get_credentials_callback(rfbClient *client, int credentials_type) with gil:
instance = <RFBClient> client.clientData.data
return instance._get_credentials_callback(credentials_type)
cdef void _text_cut_callback(rfbClient *client, const char *text, int length) with gil:
instance = <RFBClient> client.clientData.data
instance._text_cut_callback(text, length)
cdef void _rfb_client_log(const char *format, ...) with gil:
cdef char buffer[512]
cdef va_list args
va_start(args, format)
PyOS_vsnprintf(buffer, sizeof(buffer), format, args)
va_end(args)
message = (<bytes>buffer).rstrip()
NotificationCenter().post_notification('RFBClientLog', data=NotificationData(message=message.decode('utf8'), thread=QThread.currentThread()))
cdef extern rfbClientLogProc rfbClientLog = _rfb_client_log
cdef extern rfbClientLogProc rfbClientErr = _rfb_client_log
# Copyright (C) 2014 AG Projects. See LICENSE for details.
#
__all__ = ['VNCClient', 'RFBSettings', 'ServerDefault', 'TrueColor', 'HighColor', 'LowColor']
from PyQt4.QtCore import QObject, QSize, QSocketNotifier, QThread, pyqtSignal
from PyQt4.QtGui import QApplication
from application.notification import NotificationCenter
from application.python import Null
from application.python.descriptor import WriteOnceAttribute
from blink.event import EventBase
from blink.screensharing._rfb import RFBClient, RFBClientError
class RFBSettings(object):
depth = WriteOnceAttribute()
quality = WriteOnceAttribute()
compression = WriteOnceAttribute()
encodings = WriteOnceAttribute()
def __init__(self, depth, quality, compression, encodings):
if depth not in (8, 16, 24, 32, None):
raise ValueError("invalid depth value: %r (should be one of 8, 16, 24, 32 or None)" % depth)
allowed_levels = range(10)
if quality not in allowed_levels:
raise ValueError("invalid quality value: %r (should be between 0..9)" % quality)
if compression not in allowed_levels:
raise ValueError("invalid compression value: %r (should be between 0..9)" % compression)
if not isinstance(encodings, str) or not encodings:
raise ValueError("invalid encodings value: %r (should be a non-empty string)" % encodings)
self.depth = depth
self.quality = quality
self.compression = compression
self.encodings = encodings
def __eq__(self, other):
if isinstance(other, RFBSettings):
return (self.depth, self.quality, self.compression, self.encodings) == (other.depth, other.quality, other.compression, other.encodings)
return NotImplemented
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return '{0.__class__.__name__}(depth={0.depth!r}, quality={0.quality!r}, compression={0.compression!r}, encodings={0.encodings!r})'.format(self)
ServerDefault = RFBSettings(depth=None, quality=9, compression=9, encodings='copyrect zlib zrle ultra hextile corre rre raw')
TrueColor = RFBSettings(depth=24, quality=9, compression=9, encodings='copyrect zlib zrle ultra hextile corre rre raw')
HighColor = RFBSettings(depth=16, quality=9, compression=9, encodings='copyrect zlib zrle ultra hextile corre rre raw')
LowColor = RFBSettings(depth=8, quality=9, compression=9, encodings='copyrect zlib zrle ultra hextile corre rre raw')
class RFBConfigureClientEvent(EventBase):
pass
class RFBKeyEvent(EventBase):
def __init__(self, key, down):
super(RFBKeyEvent, self).__init__()
self.key = key
self.down = down
class RFBMouseEvent(EventBase):
def __init__(self, x, y, button_mask):
super(RFBMouseEvent, self).__init__()
self.x = x
self.y = y
self.button_mask = button_mask
class RFBCutTextEvent(EventBase):
def __init__(self, text):
super(RFBCutTextEvent, self).__init__()
self.text = text
class VNCClient(QObject):
started = pyqtSignal()
finished = pyqtSignal()
imageSizeChanged = pyqtSignal(QSize)
imageChanged = pyqtSignal(int, int, int, int)
passwordRequested = pyqtSignal(bool)
textCut = pyqtSignal(unicode)
def __init__(self, host, port, settings, parent=None):
super(VNCClient, self).__init__(parent)
self.thread = QThread()
self.moveToThread(self.thread)
self.host = host
self.port = port
self.settings = settings
self.username = None
self.password = None
self.rfb_client = None
self.socket_notifier = None
self.thread.started.connect(self._SH_ThreadStarted)
self.thread.finished.connect(self._SH_ThreadFinished)
def _get_settings(self):
return self.__dict__['settings']
def _set_settings(self, settings):
old_settings = self.__dict__.get('settings', None)
if settings == old_settings:
return
self.__dict__['settings'] = settings
if self.thread.isRunning():
QApplication.postEvent(self, RFBConfigureClientEvent())
settings = property(_get_settings, _set_settings)
del _get_settings, _set_settings
@property
def image(self):
return self.rfb_client.image if self.rfb_client is not None else None
def start(self):
self.thread.start()
def stop(self):
self.thread.quit()
def key_event(self, key, down):
if self.thread.isRunning():
QApplication.postEvent(self, RFBKeyEvent(key, down))
def mouse_event(self, x, y, button_mask):
if self.thread.isRunning():
QApplication.postEvent(self, RFBMouseEvent(x, y, button_mask))
def cut_text_event(self, text):
if text and self.thread.isRunning():
QApplication.postEvent(self, RFBCutTextEvent(text))
def _SH_ThreadStarted(self):
self.started.emit()
notification_center = NotificationCenter()
notification_center.post_notification('VNCClientWillStart', sender=self)
self.rfb_client = RFBClient(parent=self)
try:
self.rfb_client.connect()
except RFBClientError:
self.thread.quit()
else:
self.socket_notifier = QSocketNotifier(self.rfb_client.socket, QSocketNotifier.Read, self)
self.socket_notifier.activated.connect(self._SH_SocketNotifierActivated)
notification_center.post_notification('VNCClientDidStart', sender=self)
def _SH_ThreadFinished(self):
self.finished.emit()
notification_center = NotificationCenter()
notification_center.post_notification('VNCClientWillEnd', sender=self)
if self.socket_notifier is not None:
self.socket_notifier.activated.disconnect(self._SH_SocketNotifierActivated)
self.socket_notifier = None
self.rfb_client = None
notification_center.post_notification('VNCClientDidEnd', sender=self)
def _SH_SocketNotifierActivated(self, sock):
self.socket_notifier.setEnabled(False)
try:
self.rfb_client.handle_server_message()
except RFBClientError:
self.thread.quit()
else:
self.socket_notifier.setEnabled(True)
def _SH_ConfigureRFBClient(self):
if self.rfb_client is not None:
self.rfb_client.configure()
def customEvent(self, event):
handler = getattr(self, '_EH_%s' % event.name, Null)
handler(event)
def _EH_RFBConfigureClientEvent(self, event):
if self.rfb_client is not None:
self.rfb_client.configure()
def _EH_RFBKeyEvent(self, event):
if self.rfb_client is not None:
self.rfb_client.send_key_event(event.key, event.down)
def _EH_RFBMouseEvent(self, event):
if self.rfb_client is not None:
self.rfb_client.send_pointer_event(event.x, event.y, event.button_mask)
def _EH_RFBCutTextEvent(self, event):
if self.rfb_client is not None:
self.rfb_client.send_client_cut_text(event.text)
# Copyright (C) 2014 AG Projects. See LICENSE for details.
#
from __future__ import division
__all__ = ['ScreensharingWindow', 'VNCViewer']
import os
import platform
from PyQt4 import uic
from PyQt4.QtCore import Qt, QEasingCurve, QEvent, QPoint, QPropertyAnimation, QRect, QTimer, QUrl
from PyQt4.QtGui import QApplication, QDesktopServices, QFrame, QIcon, QImage, QMenu, QPainter, QStyle, QStyleOption, QStylePainter, QTransform, QWidget, qRgb
from application.system import makedirs
from collections import defaultdict
from datetime import datetime
from functools import reduce
from itertools import count
from operator import __or__
from sipsimple.application import SIPApplication
from sipsimple.audio import WavePlayer
from sipsimple.threading import run_in_thread
from blink.configuration.settings import BlinkSettings
from blink.resources import Resources
from blink.screensharing.vncclient import ServerDefault, TrueColor, HighColor, LowColor
class ButtonMaskMapper(dict):
class qt:
NoButton = Qt.NoButton
LeftButton = Qt.LeftButton
MidButton = Qt.MidButton
RightButton = Qt.RightButton
class vnc:
NoButton = 0b00000000
LeftButton = 0b00000001
MidButton = 0b00000010
RightButton = 0b00000100
WheelUp = 0b00001000
WheelDown = 0b00010000
WheelLeft = 0b00100000 # not sure about the horizontal wheel button mappings as button 6&7 -Dan
WheelRight = 0b01000000 # it seems different on windows (i.e. buttons 6&7 do not translate to horizontal wheel) -Dan
def __init__(self):
mapping = {self.qt.NoButton: self.vnc.NoButton, self.qt.LeftButton: self.vnc.LeftButton, self.qt.MidButton: self.vnc.MidButton, self.qt.RightButton: self.vnc.RightButton}
super(ButtonMaskMapper, self).__init__({int(b1|b2|b3): mapping[b1]|mapping[b2]|mapping[b3] for b1 in mapping for b2 in mapping for b3 in mapping})
self._button_mask = int(self.qt.LeftButton|self.qt.MidButton|self.qt.RightButton)
def __getitem__(self, key):
return super(ButtonMaskMapper, self).__getitem__(int(key & self._button_mask))
class VNCNativeKeyMap(object):
def __getitem__(self, event):
return event.nativeVirtualKey()
class VNCWindowsKeyMap(object):
__keymap__ = {
0x10: 0xffe1, # VK_SHIFT : XK_Shift_L
0x11: 0xffe3, # VK_CONTROL : XK_Control_L
0x12: 0xffe9, # VK_MENU : XK_Alt_L
0x5b: 0xffe7, # VK_LWIN : XK_Meta_L
0x1b: 0xff1b, # VK_ESCAPE : XK_Escape
0x09: 0xff09, # VK_TAB : XK_Tab
0x08: 0xff08, # VK_BACK : XK_BackSpace
0x0d: 0xff0d, # VK_RETURN : XK_Return
0x2d: 0xff63, # VK_INSERT : XK_Insert
0x2e: 0xffff, # VK_DELETE : XK_Delete
0x13: 0xff13, # VK_PAUSE : XK_Pause
0x2c: 0xff61, # VK_SNAPSHOT : XK_Print
0x24: 0xff50, # VK_HOME : XK_Home
0x23: 0xff57, # VK_END : XK_End
0x25: 0xff51, # VK_LEFT : XK_Left
0x26: 0xff52, # VK_UP : XK_Up
0x27: 0xff53, # VK_RIGHT : XK_Right
0x28: 0xff54, # VK_DOWN : XK_Down
0x21: 0xff55, # VK_PRIOR : XK_Prior
0x22: 0xff56, # VK_NEXT : XK_Next
0x14: 0xffe5, # VK_CAPITAL : XK_Caps_Lock
0x90: 0xff7f, # VK_NUMLOCK : XK_Num_Lock
0x91: 0xff14, # VK_SCROLL : XK_Scroll_Lock
0x5d: 0xff67, # VK_APPS : XK_Menu
0x2f: 0xff6a, # VK_HELP : XK_Help
0x1f: 0xff7e, # VK_MODECHANGE : XK_Mode_switch
0x70: 0xffbe, # VK_F1 : XK_F1
0x71: 0xffbf, # VK_F2 : XK_F2
0x72: 0xffc0, # VK_F3 : XK_F3
0x73: 0xffc1, # VK_F4 : XK_F4
0x74: 0xffc2, # VK_F5 : XK_F5
0x75: 0xffc3, # VK_F6 : XK_F6
0x76: 0xffc4, # VK_F7 : XK_F7
0x77: 0xffc5, # VK_F8 : XK_F8
0x78: 0xffc6, # VK_F9 : XK_F9
0x79: 0xffc7, # VK_F10 : XK_F10
0x7a: 0xffc8, # VK_F11 : XK_F11
0x7b: 0xffc9, # VK_F12 : XK_F12
0x7c: 0xffca, # VK_F13 : XK_F13
0x7d: 0xffcb, # VK_F14 : XK_F14
0x7e: 0xffcc, # VK_F15 : XK_F15
0x7f: 0xffcd, # VK_F16 : XK_F16
0x80: 0xffce, # VK_F17 : XK_F17
0x81: 0xffcf, # VK_F18 : XK_F18
0x82: 0xffd0, # VK_F19 : XK_F19
0x83: 0xffd1, # VK_F20 : XK_F20
0x84: 0xffd2, # VK_F21 : XK_F21
0x85: 0xffd3, # VK_F22 : XK_F22
0x86: 0xffd4, # VK_F23 : XK_F23
0x87: 0xffd5, # VK_F24 : XK_F24
0x93: 0xff2c, # VK_OEM_FJ_MASSHOU : XK_Massyo
0x94: 0xff2b, # VK_OEM_FJ_TOUROKU : XK_Touroku
}
__capsmodifier__ = 0x100 # the native CapsLock modifier
def __getitem__(self, event):
key = event.key()
if Qt.Key_A <= key <= Qt.Key_Z:
if bool(event.modifiers() & Qt.ShiftModifier) ^ bool(event.nativeModifiers() & self.__capsmodifier__):
return key
else:
return key + 0x20 # make it lowercase
else:
native_key = event.nativeVirtualKey()
return self.__keymap__.get(native_key, native_key)
class VNCKeyMap(object):
"""Return the key mapper for the platform"""
__keymaps__ = defaultdict(VNCNativeKeyMap)
__keymaps__['windows'] = VNCWindowsKeyMap()
def __new__(cls):
return cls.__keymaps__[platform.system().lower()]
class VNCKey(int):
__modifiermap__ = {Qt.Key_Shift: Qt.ShiftModifier, Qt.Key_Control: Qt.ControlModifier, Qt.Key_Alt: Qt.AltModifier, Qt.Key_Meta: Qt.MetaModifier, Qt.Key_AltGr: Qt.GroupSwitchModifier}
__keymap__ = VNCKeyMap()
def __new__(cls, key, qt_key):
instance = super(VNCKey, cls).__new__(cls, key)
instance.qt_key = qt_key
return instance
@property
def qt_modifier(self):
return self.__modifiermap__.get(self.qt_key, None)
@classmethod
def from_event(cls, event):
return cls(cls.__keymap__[event], event.key())
class ActiveKeys(set):
@property
def modifiers(self):
return reduce(__or__, (key.qt_modifier for key in self if key.qt_modifier is not None), Qt.NoModifier)
class VNCViewer(QWidget):
button_mask_map = ButtonMaskMapper()
def __init__(self, vncclient=None, parent=None):
super(VNCViewer, self).__init__(parent)
self.setMouseTracking(True)
self.setFocusPolicy(Qt.WheelFocus)
#self.setCursor(Qt.BlankCursor)
self.client = vncclient or parent.client
self.client.started.connect(self._SH_ClientStarted)
self.client.finished.connect(self._SH_ClientFinished)
self.client.imageSizeChanged.connect(self._SH_ImageSizeChanged)
self.client.imageChanged.connect(self._SH_ImageUpdated)
self.client.passwordRequested.connect(self._SH_PasswordRequested, Qt.BlockingQueuedConnection)
self.colors_8bit = [qRgb((i&0x07) << 5, (i&0x38) << 2, i&0xc0) for i in range(256)]
self.scale = False
self.view_only = False
self._client_active = False
self._has_mouse_over = False
self._active_keys = ActiveKeys()
def _get_scale(self):
return self.__dict__['scale']
def _set_scale(self, scale):
self.__dict__['scale'] = scale
if not scale:
image = self.client.image or QImage()
if not image.isNull():
self.resize(image.size())
elif self.parent() is not None:
self.resize(self.parent().size())
self.update()
scale = property(_get_scale, _set_scale)
del _get_scale, _set_scale
def _get_view_only(self):
return self.__dict__['view_only']
def _set_view_only(self, view_only):
old_value = self.__dict__.get('view_only', None)
new_value = self.__dict__['view_only'] = view_only
if old_value is None or old_value == new_value:
return
if self._client_active and self._has_mouse_over and self.hasFocus() and not view_only:
self.grabKeyboard()
else:
self.releaseKeyboard()
view_only = property(_get_view_only, _set_view_only)
del _get_view_only, _set_view_only
@property
def transform(self):
try:
return self.__dict__['transform'] if self.scale else QTransform()
except KeyError:
transform = QTransform()
image = self.client.image
if image is not None and not image.isNull():
scale = min(self.width()/image.width(), self.height()/image.height())
transform.translate((self.width() - image.width()*scale)/2, (self.height() - image.height()*scale)/2)
transform.scale(scale, scale)
transform = self.__dict__.setdefault('transform', transform)
return transform
def event(self, event):
event_type = event.type()
if event_type in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick, QEvent.MouseMove, QEvent.Wheel):
if not self.view_only:
event.accept()
self.mouseEvent(event)
else:
event.ignore()
return True
elif event_type in (QEvent.KeyPress, QEvent.KeyRelease):
if not self.view_only:
event.accept()
self.keyEvent(event)
else:
event.ignore()
return True
return super(VNCViewer, self).event(event)
def paintEvent(self, event):
event_rect = event.rect()
image = self.client.image
painter = QPainter(self)
if image is None or image.isNull():
pass
elif self.scale:
inverse_transform, invertible = self.transform.inverted()
image_rect = inverse_transform.mapRect(event_rect).adjusted(-1, -1, 1, 1).intersected(image.rect())
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
painter.setTransform(self.transform)
if image.format() == QImage.Format_Indexed8:
image_area = image.copy(image_rect)
image_area.setColorTable(self.colors_8bit)
painter.drawImage(image_rect, image_area)
else:
painter.drawImage(image_rect, image, image_rect)
elif image.format() == QImage.Format_Indexed8:
image_area = image.copy(event_rect)
image_area.setColorTable(self.colors_8bit)
painter.drawImage(event_rect, image_area)
else:
painter.drawImage(event_rect, image, event_rect)
def resizeEvent(self, event):
self.__dict__.pop('transform', None)
super(VNCViewer, self).resizeEvent(event)
def enterEvent(self, event):
self._has_mouse_over = True
if self._client_active and self.hasFocus() and not self.view_only:
self.grabKeyboard()
super(VNCViewer, self).enterEvent(event)
def leaveEvent(self, event):
self._has_mouse_over = False
self.releaseKeyboard()
super(VNCViewer, self).leaveEvent(event)
def focusInEvent(self, event):
if self._client_active and self._has_mouse_over and not self.view_only:
self.grabKeyboard()
super(VNCViewer, self).focusInEvent(event)
def focusOutEvent(self, event):
self.releaseKeyboard()
self._reset_keyboard()
super(VNCViewer, self).focusOutEvent(event)
def mouseEvent(self, event):
if self.scale:
inverse_transform, invertible = self.transform.inverted()
image_pos = inverse_transform.map(event.pos())
x = round(image_pos.x())
y = round(image_pos.y())
else:
x = event.x()
y = event.y()
button_mask = self.button_mask_map[event.buttons()]
if event.type() == QEvent.Wheel:
if event.delta() > 0:
wheel_button_mask = self.button_mask_map.vnc.WheelUp if event.orientation()==Qt.Vertical else self.button_mask_map.vnc.WheelLeft
else:
wheel_button_mask = self.button_mask_map.vnc.WheelDown if event.orientation()==Qt.Vertical else self.button_mask_map.vnc.WheelRight
self.client.mouse_event(x, y, button_mask | wheel_button_mask)
self.client.mouse_event(x, y, button_mask)
def keyEvent(self, event):
vnc_key = VNCKey.from_event(event)
key_down = event.type()==QEvent.KeyPress
if vnc_key:
expected_modifiers = self._active_keys.modifiers
keyboard_modifiers = event.modifiers()
if vnc_key.qt_key in VNCKey.__modifiermap__ and vnc_key.qt_key != Qt.Key_AltGr and vnc_key != 0xffe7:
keyboard_modifiers ^= vnc_key.qt_modifier # we want the modifier mask as it was before this modifier key was pressed/released
if (keyboard_modifiers ^ expected_modifiers) & expected_modifiers:
self._reset_keyboard(preserve_modifiers=keyboard_modifiers)
if key_down:
if vnc_key in self._active_keys:
self.client.key_event(vnc_key, False) # key was already pressed and now we got another press event. emulate the missing key release event.
else:
self._active_keys.add(vnc_key)
self.client.key_event(vnc_key, True)
else:
if vnc_key in self._active_keys:
self._active_keys.remove(vnc_key)
self.client.key_event(vnc_key, False)
else:
self._reset_keyboard(preserve_modifiers=event.modifiers())
def _reset_keyboard(self, preserve_modifiers=Qt.NoModifier):
dated_keys = {key for key in self._active_keys if key.qt_modifier is None or not (key.qt_modifier & preserve_modifiers)}
for vnc_key in dated_keys:
self.client.key_event(vnc_key, False)
self._active_keys -= dated_keys
def _SH_ClientStarted(self):
self._client_active = True
if self._has_mouse_over and self.hasFocus() and not self.view_only:
self.grabKeyboard()
def _SH_ClientFinished(self):
self._client_active = False
self.releaseKeyboard()
def _SH_ImageSizeChanged(self, size):
self.__dict__.pop('transform', None)
if not self.scale:
self.resize(size)
elif self.parent() is not None:
self.resize(self.parent().size())
def _SH_ImageUpdated(self, x, y, w, h):
if self.scale:
self.update(self.transform.mapRect(QRect(x, y, w, h)).adjusted(-1, -1, 1, 1).intersected(self.rect()))
else:
self.update(x, y, w, h)
def _SH_PasswordRequested(self, with_username):
dialog = ScreensharingDialog(self)
if with_username:
username, password = dialog.get_credentials()
else:
username, password = None, dialog.get_password()
self.client.username = username
self.client.password = password
ui_class, base_class = uic.loadUiType(Resources.get('screensharing_dialog.ui'))
class ScreensharingDialog(base_class, ui_class):
def __init__(self, parent=None):
super(ScreensharingDialog, self).__init__(parent)
with Resources.directory:
self.setupUi(self)
self.setWindowModality(Qt.WindowModal)
parent.installEventFilter(self)
def eventFilter(self, watched, event):
if watched is self.parent() and event.type() in (QEvent.Close, QEvent.Hide):
self.reject()
return False
def get_credentials(self):
self.message_label.setText(u'Screen sharing requires authentication')
self.username_label.show()
self.username_editor.show()
self.username_editor.clear()
self.password_editor.clear()
self.username_editor.setFocus(Qt.OtherFocusReason)
self.setMinimumHeight(190)
self.resize(self.minimumSize())
result = self.exec_()
return (self.username_editor.text(), self.password_editor.text()) if result==self.Accepted else (None, None)
def get_password(self):
self.message_label.setText(u'Screen sharing requires a password')
self.username_label.hide()
self.username_editor.hide()
self.username_editor.clear()
self.password_editor.clear()
self.password_editor.setFocus(Qt.OtherFocusReason)
self.setMinimumHeight(165)
self.resize(self.minimumSize())
result = self.exec_()
return self.password_editor.text() if result==self.Accepted else None
del ui_class, base_class
ui_class, base_class = uic.loadUiType(Resources.get('screensharing_toolbox.ui'))
class ScreensharingToolbox(base_class, ui_class):
exposedPixels = 3
def __init__(self, parent):
super(ScreensharingToolbox, self).__init__(parent)
with Resources.directory:
self.setupUi()
parent.installEventFilter(self)
self.animation = QPropertyAnimation(self, 'pos')
self.animation.setDuration(250)
self.animation.setDirection(QPropertyAnimation.Forward)
self.animation.setEasingCurve(QEasingCurve.Linear) # or OutCirc with 300ms
self.retract_timer = QTimer(self)
self.retract_timer.setInterval(3000)
self.retract_timer.setSingleShot(True)
self.retract_timer.timeout.connect(self.retract)
self.resize(self.size().expandedTo(self.toolbox_layout.minimumSize()))
def setupUi(self):
super(ScreensharingToolbox, self).setupUi(self)
# fix the SVG icons, as the generated code loads them as pixmaps, losing their ability to scale -Dan
scale_icon = QIcon()
scale_icon.addFile(Resources.get('icons/scale.svg'), mode=QIcon.Normal, state=QIcon.Off)
viewonly_icon = QIcon()
viewonly_icon.addFile(Resources.get('icons/viewonly.svg'), mode=QIcon.Normal, state=QIcon.Off)
screenshot_icon = QIcon()
screenshot_icon.addFile(Resources.get('icons/screenshot.svg'), mode=QIcon.Normal, state=QIcon.Off)
fullscreen_icon = QIcon()
fullscreen_icon.addFile(Resources.get('icons/fullscreen.svg'), mode=QIcon.Normal, state=QIcon.Off)
fullscreen_icon.addFile(Resources.get('icons/fullscreen-exit.svg'), mode=QIcon.Normal, state=QIcon.On)
fullscreen_icon.addFile(Resources.get('icons/fullscreen-exit.svg'), mode=QIcon.Active, state=QIcon.On)
fullscreen_icon.addFile(Resources.get('icons/fullscreen-exit.svg'), mode=QIcon.Disabled, state=QIcon.On)
fullscreen_icon.addFile(Resources.get('icons/fullscreen-exit.svg'), mode=QIcon.Selected, state=QIcon.On)
minimize_icon = QIcon()
minimize_icon.addFile(Resources.get('icons/minimize.svg'), mode=QIcon.Normal, state=QIcon.Off)
minimize_icon.addFile(Resources.get('icons/minimize-active.svg'), mode=QIcon.Active, state=QIcon.Off)
close_icon = QIcon()
close_icon.addFile(Resources.get('icons/close.svg'), mode=QIcon.Normal, state=QIcon.Off)
close_icon.addFile(Resources.get('icons/close-active.svg'), mode=QIcon.Active, state=QIcon.Off)
self.scale_action.setIcon(scale_icon)
self.viewonly_action.setIcon(viewonly_icon)
self.screenshot_action.setIcon(screenshot_icon)
self.fullscreen_action.setIcon(fullscreen_icon)
self.minimize_action.setIcon(minimize_icon)
self.close_action.setIcon(close_icon)
self.scale_button.setIcon(scale_icon)
self.viewonly_button.setIcon(viewonly_icon)
self.screenshot_button.setIcon(screenshot_icon)
self.fullscreen_button.setIcon(fullscreen_icon)
self.minimize_button.setIcon(minimize_icon)
self.close_button.setIcon(close_icon)
self.scale_button.setDefaultAction(self.scale_action)
self.viewonly_button.setDefaultAction(self.viewonly_action)
self.screenshot_button.setDefaultAction(self.screenshot_action)
self.fullscreen_button.setDefaultAction(self.fullscreen_action)
self.minimize_button.setDefaultAction(self.minimize_action)
self.close_button.setDefaultAction(self.close_action)
self.color_depth_button.clear()
self.color_depth_button.addItem('Default Color Depth', ServerDefault)
self.color_depth_button.addItem('TrueColor (24 bits)', TrueColor)
self.color_depth_button.addItem('HighColor (16 bits)', HighColor)
self.color_depth_button.addItem('LowColor (8 bits)', LowColor)
def eventFilter(self, watched, event):
if watched is self.parent() and event.type() == QEvent.Resize:
new_x = (watched.width() - self.width()) / 2
self.move(new_x, self.y())
self.animation.setStartValue(QPoint(new_x, -self.height() + self.exposedPixels))
self.animation.setEndValue(QPoint(new_x, 0))
return False
def enterEvent(self, event):
super(ScreensharingToolbox, self).enterEvent(event)
self.retract_timer.stop()
self.expose()
def leaveEvent(self, event):
super(ScreensharingToolbox, self).leaveEvent(event)
self.retract_timer.start()
def paintEvent(self, event): # make the widget style aware
option = QStyleOption()
option.init(self)
painter = QStylePainter(self)
painter.drawPrimitive(QStyle.PE_Widget, option)
def expose(self):
if self.animation.state() == QPropertyAnimation.Running and self.animation.direction() == QPropertyAnimation.Forward:
return
elif self.animation.state() == QPropertyAnimation.Stopped and self.pos() == self.animation.endValue():
return
self.animation.setDirection(QPropertyAnimation.Forward)
self.animation.start()
def retract(self):
if self.animation.state() == QPropertyAnimation.Running and self.animation.direction() == QPropertyAnimation.Backward:
return
elif self.animation.state() == QPropertyAnimation.Stopped and self.pos() == self.animation.startValue():
return
self.animation.setDirection(QPropertyAnimation.Backward)
self.animation.start()
del ui_class, base_class
ui_class, base_class = uic.loadUiType(Resources.get('screensharing_window.ui'))
class ScreensharingWindow(base_class, ui_class):
def __init__(self, vncclient, parent=None):
super(ScreensharingWindow, self).__init__(parent)
self.vncclient = vncclient
self.clipboard = QApplication.clipboard()
with Resources.directory:
self.setupUi()
self.scale_action.triggered.connect(self._SH_ScaleActionTriggered)
self.viewonly_action.triggered.connect(self._SH_ViewOnlyActionTriggered)
self.screenshot_action.triggered.connect(self._SH_ScreenshotActionTriggered)
self.fullscreen_action.triggered.connect(self._SH_FullscreenActionTriggered)
self.minimize_action.triggered.connect(self._SH_MinimizeActionTriggered)
self.close_action.triggered.connect(self._SH_CloseActionTriggered)
self.screenshots_folder_action.triggered.connect(self._SH_ScreenshotsFolderActionTriggered)
self.screenshot_button.customContextMenuRequested.connect(self._SH_ScreenshotButtonContextMenuRequested)
self.color_depth_button.currentIndexChanged[int].connect(self._SH_ColorDepthButtonCurrentIndexChanged)
self.fullscreen_toolbox.color_depth_button.currentIndexChanged[int].connect(self._SH_ColorDepthButtonCurrentIndexChanged)
self.vncclient.textCut.connect(self._SH_VNCClientTextCut)
self.clipboard.changed.connect(self._SH_ClipboardChanged)
def setupUi(self):
super(ScreensharingWindow, self).setupUi(self)
self.scroll_area.viewport().setObjectName('vnc_viewport')
self.scroll_area.viewport().setStyleSheet('QWidget#vnc_viewport { background-color: #101010; }') # #101010, #004488, #004480, #0044aa, #0055ff, #0066cc, #3366cc, #0066d5
self.vncviewer = VNCViewer(self.vncclient)
self.vncviewer.setGeometry(self.scroll_area.viewport().geometry())
self.scroll_area.setWidget(self.vncviewer)
self.vncviewer.setFocus(Qt.OtherFocusReason)
self.fullscreen_toolbox = ScreensharingToolbox(self)
self.fullscreen_toolbox.hide()
self.scale_action = self.fullscreen_toolbox.scale_action
self.viewonly_action = self.fullscreen_toolbox.viewonly_action
self.screenshot_action = self.fullscreen_toolbox.screenshot_action
self.fullscreen_action = self.fullscreen_toolbox.fullscreen_action
self.minimize_action = self.fullscreen_toolbox.minimize_action
self.close_action = self.fullscreen_toolbox.close_action
self.scale_button.setIcon(self.scale_action.icon())
self.viewonly_button.setIcon(self.viewonly_action.icon())
self.screenshot_button.setIcon(self.screenshot_action.icon())
self.fullscreen_button.setIcon(self.fullscreen_action.icon())
self.scale_button.setDefaultAction(self.scale_action)
self.viewonly_button.setDefaultAction(self.viewonly_action)
self.screenshot_button.setDefaultAction(self.screenshot_action)
self.fullscreen_button.setDefaultAction(self.fullscreen_action)
self.color_depth_button.clear()
self.color_depth_button.addItem('Default Color Depth', ServerDefault)
self.color_depth_button.addItem('TrueColor (24 bits)', TrueColor)
self.color_depth_button.addItem('HighColor (16 bits)', HighColor)
self.color_depth_button.addItem('LowColor (8 bits)', LowColor)
self.screenshot_button_menu = QMenu(self)
self.screenshots_folder_action = self.screenshot_button_menu.addAction('Open screenshots folder')
def closeEvent(self, event):
super(ScreensharingWindow, self).closeEvent(event)
self.vncclient.stop()
def _SH_ColorDepthButtonCurrentIndexChanged(self, index):
# synchronize the 2 color depth buttons
self.color_depth_button.blockSignals(True)
self.fullscreen_toolbox.color_depth_button.blockSignals(True)
self.color_depth_button.setCurrentIndex(index)
self.fullscreen_toolbox.color_depth_button.setCurrentIndex(index)
self.color_depth_button.blockSignals(False)
self.fullscreen_toolbox.color_depth_button.blockSignals(False)
self.vncclient.settings = self.color_depth_button.itemData(index, Qt.UserRole)
def _SH_ScaleActionTriggered(self):
if self.scale_action.isChecked():
self.vncviewer.scale = True
self.scroll_area.setWidgetResizable(True)
else:
self.scroll_area.setWidgetResizable(False)
self.vncviewer.scale = False
def _SH_ViewOnlyActionTriggered(self):
self.vncviewer.view_only = self.viewonly_action.isChecked()
def _SH_ScreenshotActionTriggered(self):
screenshot = Screenshot(self.vncclient)
screenshot.capture()
screenshot.save()
def _SH_FullscreenActionTriggered(self):
if self.fullscreen_action.isChecked():
self.scroll_area.setFrameShape(QFrame.NoFrame)
self.toolbar.hide()
self.showFullScreen()
self.fullscreen_toolbox.animation.stop()
self.fullscreen_toolbox.move(self.fullscreen_toolbox.x(), 0)
self.fullscreen_toolbox.show()
self.fullscreen_toolbox.retract_timer.start()
else:
self.fullscreen_toolbox.retract_timer.stop()
self.fullscreen_toolbox.animation.stop()
self.fullscreen_toolbox.hide()
self.scroll_area.setFrameShape(QFrame.StyledPanel)
self.toolbar.show()
self.showNormal()
def _SH_MinimizeActionTriggered(self):
self.showMinimized()
def _SH_CloseActionTriggered(self):
self.fullscreen_action.trigger()
self.close()
def _SH_ScreenshotButtonContextMenuRequested(self, pos):
if self.fullscreen_action.isChecked():
self.screenshot_button_menu.exec_(self.fullscreen_toolbox.screenshot_button.mapToGlobal(pos))
else:
self.screenshot_button_menu.exec_(self.screenshot_button.mapToGlobal(pos))
def _SH_ScreenshotsFolderActionTriggered(self, pos):
settings = BlinkSettings()
QDesktopServices.openUrl(QUrl.fromLocalFile(settings.screen_sharing.screenshots_directory.normalized))
def _SH_VNCClientTextCut(self, text):
self.clipboard.blockSignals(True)
self.clipboard.setText(text)
self.clipboard.blockSignals(False)
def _SH_ClipboardChanged(self, mode):
data = self.clipboard.mimeData(mode)
self.vncclient.cut_text_event(data.html() or data.text())
del ui_class, base_class
class Screenshot(object):
def __init__(self, vncclient):
self.vncclient = vncclient
self.image = None
@classmethod
def filename_generator(cls):
settings = BlinkSettings()
name = os.path.join(settings.screen_sharing.screenshots_directory.normalized, 'ScreenSharing-{:%Y%m%d-%H.%M.%S}'.format(datetime.now()))
yield '%s.png' % name
for x in count(1):
yield "%s-%d.png" % (name, x)
def capture(self):
try:
self.image = self.vncclient.image.copy()
except AttributeError:
pass
else:
player = WavePlayer(SIPApplication.alert_audio_bridge.mixer, Resources.get('sounds/screenshot.wav'), volume=30)
SIPApplication.alert_audio_bridge.add(player)
player.start()
@run_in_thread('file-io')
def save(self):
if self.image is not None:
filename = next(filename for filename in self.filename_generator() if not os.path.exists(filename))
makedirs(os.path.dirname(filename))
self.image.save(filename)
......@@ -19,7 +19,7 @@ from operator import attrgetter
from threading import Event
from PyQt4 import uic
from PyQt4.QtCore import Qt, QAbstractListModel, QByteArray, QEasingCurve, QEvent, QMimeData, QModelIndex, QObject, QPointF, QPropertyAnimation, QRect, QRectF, QSize, QTimer, QUrl, pyqtSignal
from PyQt4.QtCore import Qt, QAbstractListModel, QByteArray, QEasingCurve, QEvent, QMimeData, QModelIndex, QObject, QPointF, QProcess, QPropertyAnimation, QRect, QRectF, QSize, QTimer, QUrl, pyqtSignal
from PyQt4.QtGui import QApplication, QBrush, QColor, QDesktopServices, QDrag, QIcon, QLabel, QLinearGradient, QListView, QMenu, QPainter, QPainterPath, QPalette, QPen, QPixmap, QPolygonF, QShortcut
from PyQt4.QtGui import QStyle, QStyledItemDelegate, QStyleOption
......@@ -28,6 +28,7 @@ from application.python import Null, limit
from application.python.types import MarkerType, Singleton
from application.python.weakref import weakobjectmap
from application.system import makedirs, unlink
from eventlib.proc import spawn
from zope.interface import implements
from sipsimple.account import Account, AccountManager, BonjourAccount
......@@ -38,10 +39,12 @@ from sipsimple.core import SIPCoreError, SIPURI, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.session import Session
from sipsimple.streams import MediaStreamRegistry
from sipsimple.streams.msrp import FileSelector
from sipsimple.threading import run_in_thread
from sipsimple.streams.msrp import FileSelector, ExternalVNCServerHandler, ExternalVNCViewerHandler, ScreenSharingStream
from sipsimple.threading import run_in_thread, run_in_twisted_thread
from blink.configuration.settings import BlinkSettings
from blink.resources import ApplicationData, Resources
from blink.screensharing import ScreensharingWindow, VNCClient, ServerDefault
from blink.util import call_later, call_in_gui_thread, run_in_gui_thread
from blink.widgets.buttons import LeftSegment, MiddleSegment, RightSegment
from blink.widgets.labels import Status
......@@ -133,15 +136,28 @@ class MSRPStreamInfo(object):
self.__init__()
class ScreenSharingStreamInfo(MSRPStreamInfo):
def __init__(self):
super(ScreenSharingStreamInfo, self).__init__()
self.mode = None
def _update(self, stream):
super(ScreenSharingStreamInfo, self)._update(stream)
if stream is not None:
self.mode = stream.handler.type
class StreamsInfo(object):
__slots__ = 'audio', 'video', 'chat'
__slots__ = 'audio', 'video', 'chat', 'screen_sharing'
def __init__(self):
self.audio = RTPStreamInfo()
self.video = RTPStreamInfo()
self.chat = MSRPStreamInfo()
self.screen_sharing = ScreenSharingStreamInfo()
def __getitem__(self, key):
key = key.replace('-', '_')
try:
return getattr(self, key)
except AttributeError:
......@@ -151,6 +167,7 @@ class StreamsInfo(object):
self.audio._update(streams.get('audio'))
self.video._update(streams.get('video'))
self.chat._update(streams.get('chat'))
self.screen_sharing._update(streams.get('screen-sharing'))
class SessionInfo(object):
......@@ -622,6 +639,20 @@ class BlinkSession(QObject):
notification_center.post_notification('BlinkSessionWillAddStream', sender=self, data=NotificationData(stream=stream))
notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media', 'statistics'}))
def add_streams(self, stream_descriptions):
assert self.state == 'connected'
for stream_description in stream_descriptions:
if stream_description.type in self.streams:
raise RuntimeError('session already has a stream of type %s' % stream_description.type)
self.info.streams[stream_description.type]._reset()
streams = [stream_description.create_stream() for stream_description in stream_descriptions]
self.sip_session.add_streams(streams)
self.streams.extend(streams)
notification_center = NotificationCenter()
for stream in streams:
notification_center.post_notification('BlinkSessionWillAddStream', sender=self, data=NotificationData(stream=stream))
notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media', 'statistics'}))
def remove_stream(self, stream):
assert self.state == 'connected'
if stream not in self.streams:
......@@ -630,6 +661,15 @@ class BlinkSession(QObject):
notification_center = NotificationCenter()
notification_center.post_notification('BlinkSessionWillRemoveStream', sender=self, data=NotificationData(stream=stream))
def remove_streams(self, streams):
assert self.state == 'connected'
if not set(self.streams).issuperset(streams):
raise RuntimeError('not all streams are part of the current session')
self.sip_session.remove_streams(streams)
notification_center = NotificationCenter()
for stream in streams:
notification_center.post_notification('BlinkSessionWillRemoveStream', sender=self, data=NotificationData(stream=stream))
def accept_proposal(self, streams):
assert self.state == 'connected/received_proposal'
self.sip_session.accept_proposal(streams)
......@@ -1592,8 +1632,11 @@ class AudioSessionItem(object):
def end(self):
if self.audio_stream in self.blink_session.streams.proposed and self.blink_session.state == 'connected/sent_proposal':
self.blink_session.sip_session.cancel_proposal()
elif len(self.blink_session.streams) > 1 and self.blink_session.state == 'connected':
self.blink_session.remove_stream(self.audio_stream)
# review this -Dan
#elif len(self.blink_session.streams) > 1 and self.blink_session.state == 'connected':
# self.blink_session.remove_stream(self.audio_stream)
elif 'chat' in self.blink_session.streams and self.blink_session.state == 'connected':
self.blink_session.remove_streams([stream for stream in self.blink_session.streams if stream.type != 'chat'])
else:
self.blink_session.end()
......@@ -2583,9 +2626,9 @@ class ChatSessionWidget(base_class, ui_class):
self.hold_icon.setVisible(session.blink_session.on_hold)
self.composing_icon.setVisible(session.remote_composing)
self.chat_icon.setVisible('chat' in session.blink_session.streams)
self.audio_icon.setVisible('audio' in session.blink_session.streams)
self.video_icon.setVisible('video' in session.blink_session.streams)
self.screen_sharing_icon.setVisible('screen-sharing' in session.blink_session.streams)
self.audio_icon.setVisible(session.blink_session.streams.types.intersection(('audio', 'video', 'screen-sharing')) == {'audio'})
del ui_class, base_class
......@@ -3171,6 +3214,133 @@ class ChatSessionListView(QListView):
self.ignore_selection_changes = False
# Screen sharing
#
class VNCServerProcess(QProcess):
__running__ = set()
ready = pyqtSignal()
def __init__(self, parent=None):
super(VNCServerProcess, self).__init__(parent)
self.server_host = 'localhost'
self.server_port = None
self.started.connect(self._SH_Started)
self.error.connect(self._SH_Error)
self.finished.connect(self._SH_Finished)
self.readyReadStandardOutput.connect(self._SH_ReadyReadStandardOutput)
self.readyReadStandardError.connect(self._SH_ReadyReadStandardError)
@property
def command(self):
if sys.platform == 'win32':
return '"%s"' % os.path.abspath(os.path.join(Resources.directory, '..', 'blinkvnc.exe'))
else:
return 'x11vnc -quiet -localhost -once -noshared -nopw -noncache -repeat'
def start(self):
super(VNCServerProcess, self).start(self.command)
def _SH_Started(self):
if self.server_port is not None:
self.ready.emit()
self.__running__.add(self)
def _SH_Error(self, error):
self.server_port = None
def _SH_Finished(self, exit_code, exit_status):
self.server_port = None
self.__running__.remove(self)
def _SH_ReadyReadStandardOutput(self):
server_output = str(self.readAllStandardOutput())
if self.server_port is None:
match = re.search(r'^PORT\s*=\s*(?P<port>\d+)\s*$', server_output, re.IGNORECASE | re.MULTILINE)
if match:
self.server_port = int(match.group('port'))
if self.state() == QProcess.Running:
self.ready.emit()
def _SH_ReadyReadStandardError(self):
self.readAllStandardError()
class ExternalVNCServerHandler(ExternalVNCServerHandler):
address = property(lambda self: None if self.vnc_process.server_port is None else (self.vnc_process.server_host, self.vnc_process.server_port))
def __init__(self):
super(ExternalVNCServerHandler, self).__init__()
self.vnc_process = None
@run_in_gui_thread
def _NH_MediaStreamDidStart(self, notification):
self.vnc_process = VNCServerProcess()
self.vnc_process.ready.connect(self._SH_VNCProcessReady)
self.vnc_process.error.connect(self._SH_VNCProcessError)
self.vnc_process.start()
def _NH_MediaStreamWillEnd(self, notification):
super(ExternalVNCServerHandler, self)._NH_MediaStreamWillEnd(notification)
if self.vnc_process is not None:
self.vnc_process.kill()
@run_in_twisted_thread
def _SH_VNCProcessReady(self):
self.vnc_starter_thread = spawn(self._start_vnc_connection)
@run_in_twisted_thread
def _SH_VNCProcessError(self, error):
NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='starting', failure=None, reason='failed to start VNC server'))
class EmbeddedVNCViewerHandler(ExternalVNCViewerHandler):
def __init__(self):
super(EmbeddedVNCViewerHandler, self).__init__()
self.window = None
@run_in_gui_thread
def _start_vnc_viewer(self):
settings = BlinkSettings()
notification_center = NotificationCenter()
host, port = self.address
vncclient = VNCClient(host, port, settings=ServerDefault)
notification_center.add_observer(self, sender=vncclient)
self.window = ScreensharingWindow(vncclient)
self.window.show()
if settings.screen_sharing.scale:
self.window.scale_action.trigger()
if settings.screen_sharing.open_viewonly:
self.window.viewonly_action.trigger()
if settings.screen_sharing.open_fullscreen:
self.window.fullscreen_action.trigger()
self.window.vncclient.start()
@run_in_gui_thread
def _stop_vnc_viewer(self):
if self.window is not None:
self.window.vncclient.stop()
self.window.hide()
@run_in_gui_thread
def _NH_VNCClientDidEnd(self, notification):
notification.center.remove_observer(self, sender=notification.sender)
self.window = None
def _NH_MediaStreamDidStart(self, notification):
super(EmbeddedVNCViewerHandler, self)._NH_MediaStreamDidStart(notification)
self._start_vnc_viewer()
def _NH_MediaStreamWillEnd(self, notification):
super(EmbeddedVNCViewerHandler, self)._NH_MediaStreamWillEnd(notification)
self._stop_vnc_viewer()
ScreenSharingStream.ServerHandler = ExternalVNCServerHandler
ScreenSharingStream.ViewerHandler = EmbeddedVNCViewerHandler
# File transfers
#
......@@ -4120,9 +4290,9 @@ class ConferenceParticipantWidget(ChatSessionWidget):
self.hold_icon.setVisible(participant.on_hold)
self.composing_icon.setVisible(participant.is_composing)
self.chat_icon.setVisible('chat' in participant.active_media)
self.audio_icon.setVisible('audio' in participant.active_media)
self.video_icon.setVisible('video' in participant.active_media)
self.screen_sharing_icon.setVisible('screen-sharing' in participant.active_media)
self.audio_icon.setVisible(participant.active_media.intersection(('audio', 'video', 'screen-sharing')) == {'audio'})
class ConferenceParticipantItem(object):
......@@ -4667,7 +4837,7 @@ class IncomingRequest(QObject):
self.dialog.screensharing_label.setText(u'is offering to share his screen')
else:
self.dialog.screensharing_label.setText(u'is asking to share your screen')
self.dialog.screensharing_stream.accepted = False # Remove when implemented later -Luci
#self.dialog.screensharing_stream.accepted = True if proposal else False
self.dialog.screensharing_stream.show()
self.dialog.audio_device_label.setText(u'Selected audio device is: %s' % SIPApplication.voice_audio_bridge.mixer.real_output_device)
......
#!/bin/sh
python setup.py build_ext --inplace $@
......@@ -657,12 +657,12 @@ padding: 2px;</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/green-handset.png</normaloff>icons/green-handset.png</iconset>
<normaloff>icons/handset.svg</normaloff>icons/handset.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>18</width>
<height>18</height>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
......@@ -690,8 +690,8 @@ padding: 2px;</string>
</property>
<property name="iconSize">
<size>
<width>18</width>
<height>18</height>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="popupMode">
......@@ -718,12 +718,12 @@ padding: 2px;</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/display18.png</normaloff>icons/display18.png</iconset>
<normaloff>icons/work/screen.svg</normaloff>icons/work/screen.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>18</width>
<height>18</height>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="popupMode">
......@@ -1052,6 +1052,8 @@ padding: 2px;</string>
<addaction name="chat_window_action"/>
<addaction name="transfers_window_action"/>
<addaction name="logs_window_action"/>
<addaction name="received_files_window_action"/>
<addaction name="screenshots_window_action"/>
</widget>
<addaction name="blink_menu"/>
<addaction name="call_menu"/>
......@@ -1276,6 +1278,28 @@ padding: 2px;</string>
<string>Play a sound for &amp;received messages</string>
</property>
</action>
<action name="received_files_window_action">
<property name="text">
<string>Received &amp;Files</string>
</property>
<property name="shortcut">
<string>Ctrl+D</string>
</property>
<property name="shortcutContext">
<enum>Qt::ApplicationShortcut</enum>
</property>
</action>
<action name="screenshots_window_action">
<property name="text">
<string>&amp;Screenshots</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
<property name="shortcutContext">
<enum>Qt::ApplicationShortcut</enum>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
......
......@@ -1100,99 +1100,42 @@ QToolButton:pressed {
<property name="verticalSpacing">
<number>2</number>
</property>
<item row="12" column="0" colspan="2">
<widget class="QWidget" name="packet_loss_widget" native="true">
<layout class="QVBoxLayout" name="packet_loss_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="packet_loss_label">
<property name="styleSheet">
<string notr="true">QLabel {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border: 1px solid #606060;
background-color: rgba(0, 0, 0, 60);
}
</string>
</property>
<property name="text">
<string>Packet Loss: 0.0%, max=0.0%</string>
</property>
<property name="indent">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="GraphWidget" name="packet_loss_graph" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QWidget#packet_loss_graph {
border: 1px solid #606060;
border-top: 1px solid transparent;
background-color: #f8f8f8;
}</string>
</property>
<property name="minHeight" stdset="0">
<double>10.000000000000000</double>
</property>
<property name="boundary" stdset="0">
<double>3.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="account_title_label">
<item row="0" column="0">
<widget class="QLabel" name="status_title_label">
<property name="text">
<string>Account</string>
<string>Status</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="duration_title_label">
<property name="text">
<string>Duration</string>
<item row="5" column="0" colspan="2">
<spacer name="session_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
</widget>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="1">
<widget class="ElidedLabel" name="chat_addresses_value_label">
<item row="1" column="1">
<widget class="DurationLabel" name="duration_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>10.0.0.1 ⇄ tls:1.2.3.4</string>
<string>01:27</string>
</property>
<property name="indent">
<number>0</number>
......@@ -1202,34 +1145,21 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="remote_agent_title_label">
<property name="text">
<string>Remote Agent</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="ElidedLabel" name="remote_agent_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Blink Qt 1.0.0</string>
<item row="15" column="0" colspan="2">
<spacer name="info_panel_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="indent">
<number>0</number>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="sip_addresses_title_label">
......@@ -1241,8 +1171,8 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="ElidedLabel" name="account_value_label">
<item row="4" column="1">
<widget class="ElidedLabel" name="sip_addresses_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1250,26 +1180,7 @@ QToolButton:pressed {
</sizepolicy>
</property>
<property name="text">
<string>user@domain</string>
</property>
<property name="indent">
<number>0</number>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="DurationLabel" name="duration_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>01:27</string>
<string>10.0.0.1 ⇄ tls:1.2.3.4:5061</string>
</property>
<property name="indent">
<number>0</number>
......@@ -1279,28 +1190,12 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="14" column="0" colspan="2">
<spacer name="info_panel_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="8" column="1">
<widget class="QWidget" name="audio_value_widget" native="true">
<item row="6" column="1">
<widget class="QWidget" name="chat_value_widget" native="true">
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
<layout class="QHBoxLayout" name="audio_value_layout">
<layout class="QHBoxLayout" name="chat_value_layout">
<property name="spacing">
<number>2</number>
</property>
......@@ -1308,7 +1203,7 @@ QToolButton:pressed {
<number>0</number>
</property>
<item>
<widget class="ElidedLabel" name="audio_value_label">
<widget class="ElidedLabel" name="chat_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1316,7 +1211,7 @@ QToolButton:pressed {
</sizepolicy>
</property>
<property name="text">
<string>opus 48kHz</string>
<string>Using relay</string>
</property>
<property name="indent">
<number>0</number>
......@@ -1324,7 +1219,7 @@ QToolButton:pressed {
</widget>
</item>
<item>
<widget class="QLabel" name="audio_encryption_label">
<widget class="QLabel" name="chat_encryption_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1367,9 +1262,64 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<widget class="QWidget" name="latency_widget" native="true">
<layout class="QVBoxLayout" name="latency_layout">
<item row="7" column="0">
<widget class="QLabel" name="chat_addresses_title_label">
<property name="text">
<string>Addresses</string>
</property>
<property name="indent">
<number>12</number>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="ElidedLabel" name="chat_addresses_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>10.0.0.1 ⇄ tls:1.2.3.4</string>
</property>
<property name="indent">
<number>0</number>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="remote_agent_title_label">
<property name="text">
<string>Remote Agent</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="audio_ice_status_title_label">
<property name="text">
<string>ICE Status</string>
</property>
<property name="indent">
<number>12</number>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<widget class="QWidget" name="packet_loss_widget" native="true">
<layout class="QVBoxLayout" name="packet_loss_layout">
<property name="spacing">
<number>0</number>
</property>
......@@ -1377,7 +1327,7 @@ QToolButton:pressed {
<number>0</number>
</property>
<property name="topMargin">
<number>5</number>
<number>3</number>
</property>
<property name="rightMargin">
<number>0</number>
......@@ -1386,7 +1336,7 @@ QToolButton:pressed {
<number>0</number>
</property>
<item>
<widget class="QLabel" name="latency_label">
<widget class="QLabel" name="packet_loss_label">
<property name="styleSheet">
<string notr="true">QLabel {
border-top-left-radius: 4px;
......@@ -1397,7 +1347,7 @@ QToolButton:pressed {
</string>
</property>
<property name="text">
<string>Network Latency: 0ms, max=0ms</string>
<string>Packet Loss: 0.0%, max=0.0%</string>
</property>
<property name="indent">
<number>5</number>
......@@ -1405,7 +1355,7 @@ QToolButton:pressed {
</widget>
</item>
<item>
<widget class="GraphWidget" name="latency_graph" native="true">
<widget class="GraphWidget" name="packet_loss_graph" native="true">
<property name="minimumSize">
<size>
<width>0</width>
......@@ -1413,25 +1363,95 @@ QToolButton:pressed {
</size>
</property>
<property name="styleSheet">
<string notr="true">QWidget#latency_graph {
<string notr="true">QWidget#packet_loss_graph {
border: 1px solid #606060;
border-top: 1px solid transparent;
background-color: #f8f8f8;
}</string>
</property>
<property name="boundary" stdset="0">
<double>300.000000000000000</double>
</property>
<property name="minHeight" stdset="0">
<double>10.000000000000000</double>
</property>
<property name="boundary" stdset="0">
<double>3.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="1">
<widget class="ElidedLabel" name="sip_addresses_value_label">
<item row="2" column="0">
<widget class="QLabel" name="account_title_label">
<property name="text">
<string>Account</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="14" column="0" colspan="2">
<widget class="QWidget" name="traffic_widget" native="true">
<layout class="QVBoxLayout" name="speed_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="traffic_label">
<property name="styleSheet">
<string notr="true">QLabel {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border: 1px solid #606060;
background-color: rgba(0, 0, 0, 60);
}
</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Traffic: &lt;span style=&quot; color:#d70000;&quot;&gt;&lt;/span&gt; 0kbps &lt;span style=&quot; color:#0064d7;&quot;&gt;&lt;/span&gt; 0kbps&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="indent">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="GraphWidget" name="traffic_graph" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QWidget#traffic_graph {
border: 1px solid #606060;
border-top: 1px solid transparent;
background-color: #f8f8f8;
}</string>
</property>
<property name="fillTransparency" stdset="0">
<number>25</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="10" column="1">
<widget class="ElidedLabel" name="audio_ice_status_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1439,7 +1459,7 @@ QToolButton:pressed {
</sizepolicy>
</property>
<property name="text">
<string>10.0.0.1 ⇄ tls:1.2.3.4:5061</string>
<string>Peer to peer</string>
</property>
<property name="indent">
<number>0</number>
......@@ -1449,8 +1469,164 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="chat_addresses_title_label">
<item row="2" column="1">
<widget class="ElidedLabel" name="account_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>user@domain</string>
</property>
<property name="indent">
<number>0</number>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ElidedLabel" name="status_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>144</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>144</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>144</red>
<green>96</green>
<blue>96</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string>Connected</string>
</property>
<property name="indent">
<number>0</number>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="audio_addresses_title_label">
<property name="text">
<string>Addresses</string>
</property>
......@@ -1462,10 +1638,135 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="chat_title_label">
<item row="12" column="0" colspan="2">
<widget class="QWidget" name="latency_widget" native="true">
<layout class="QVBoxLayout" name="latency_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="latency_label">
<property name="styleSheet">
<string notr="true">QLabel {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border: 1px solid #606060;
background-color: rgba(0, 0, 0, 60);
}
</string>
</property>
<property name="text">
<string>Chat</string>
<string>Network Latency: 0ms, max=0ms</string>
</property>
<property name="indent">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="GraphWidget" name="latency_graph" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QWidget#latency_graph {
border: 1px solid #606060;
border-top: 1px solid transparent;
background-color: #f8f8f8;
}</string>
</property>
<property name="boundary" stdset="0">
<double>300.000000000000000</double>
</property>
<property name="minHeight" stdset="0">
<double>10.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="8" column="1">
<widget class="QWidget" name="audio_value_widget" native="true">
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
<layout class="QHBoxLayout" name="audio_value_layout">
<property name="spacing">
<number>2</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="ElidedLabel" name="audio_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>opus 48kHz</string>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="audio_encryption_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>icons/lock-grey-12.svg</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="duration_title_label">
<property name="text">
<string>Duration</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
......@@ -1491,21 +1792,18 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="audio_addresses_title_label">
<item row="6" column="0">
<widget class="QLabel" name="chat_title_label">
<property name="text">
<string>Addresses</string>
</property>
<property name="indent">
<number>12</number>
<string>Chat</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="ElidedLabel" name="audio_ice_status_value_label">
<item row="3" column="1">
<widget class="ElidedLabel" name="remote_agent_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1513,7 +1811,7 @@ QToolButton:pressed {
</sizepolicy>
</property>
<property name="text">
<string>Peer to peer</string>
<string>Blink Qt 1.0.0</string>
</property>
<property name="indent">
<number>0</number>
......@@ -1523,25 +1821,22 @@ QToolButton:pressed {
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="audio_ice_status_title_label">
<item row="11" column="0">
<widget class="QLabel" name="screen_title_label">
<property name="text">
<string>ICE Status</string>
</property>
<property name="indent">
<number>12</number>
<string>Screen</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QWidget" name="chat_value_widget" native="true">
<item row="11" column="1">
<widget class="QWidget" name="screen_value_widget" native="true">
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
<layout class="QHBoxLayout" name="chat_value_layout">
<layout class="QHBoxLayout" name="screen_value_layout">
<property name="spacing">
<number>2</number>
</property>
......@@ -1549,7 +1844,7 @@ QToolButton:pressed {
<number>0</number>
</property>
<item>
<widget class="ElidedLabel" name="chat_value_label">
<widget class="ElidedLabel" name="screen_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1557,7 +1852,7 @@ QToolButton:pressed {
</sizepolicy>
</property>
<property name="text">
<string>Using relay</string>
<string>Viewing remote</string>
</property>
<property name="indent">
<number>0</number>
......@@ -1565,7 +1860,7 @@ QToolButton:pressed {
</widget>
</item>
<item>
<widget class="QLabel" name="chat_encryption_label">
<widget class="QLabel" name="screen_encryption_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -1598,229 +1893,6 @@ QToolButton:pressed {
</layout>
</widget>
</item>
<item row="13" column="0" colspan="2">
<widget class="QWidget" name="traffic_widget" native="true">
<layout class="QVBoxLayout" name="speed_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="traffic_label">
<property name="styleSheet">
<string notr="true">QLabel {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border: 1px solid #606060;
background-color: rgba(0, 0, 0, 60);
}
</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Traffic: &lt;span style=&quot; color:#d70000;&quot;&gt;&lt;/span&gt; 0kbps &lt;span style=&quot; color:#0064d7;&quot;&gt;&lt;/span&gt; 0kbps&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="indent">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="GraphWidget" name="traffic_graph" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QWidget#traffic_graph {
border: 1px solid #606060;
border-top: 1px solid transparent;
background-color: #f8f8f8;
}</string>
</property>
<property name="fillTransparency" stdset="0">
<number>25</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="ElidedLabel" name="status_value_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>144</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>144</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="10">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="AlternateBase">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>144</red>
<green>96</green>
<blue>96</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string>Connected</string>
</property>
<property name="indent">
<number>0</number>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="status_title_label">
<property name="text">
<string>Status</string>
</property>
<property name="role" stdset="0">
<string notr="true">title</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<spacer name="session_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
......@@ -2116,6 +2188,11 @@ QToolButton:hover {
<extends>QWidget</extends>
<header>QtWebKit/QWebView</header>
</customwidget>
<customwidget>
<class>ToolButton</class>
<extends>QToolButton</extends>
<header>blink.widgets.buttons</header>
</customwidget>
<customwidget>
<class>ElidedLabel</class>
<extends>QLabel</extends>
......@@ -2136,11 +2213,6 @@ QToolButton:hover {
<extends>QWebView</extends>
<header>blink.chatwindow</header>
</customwidget>
<customwidget>
<class>ToolButton</class>
<extends>QToolButton</extends>
<header>blink.widgets.buttons</header>
</customwidget>
<customwidget>
<class>SlidingStackedWidget</class>
<extends>QStackedWidget</extends>
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="close-active.svg"
inkscape:version="0.48.4 r9939"
xml:space="preserve"
width="96px"
viewBox="0 0 96 96"
version="1.1"
id="bigger"
height="96px"
enable-background="new 0 0 96 96"><metadata
id="metadata11"><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><defs
id="defs9"><linearGradient
id="linearGradient3983"><stop
style="stop-color:#00ff00;stop-opacity:1;"
offset="0"
id="stop3985" /><stop
style="stop-color:#00d000;stop-opacity:1;"
offset="1"
id="stop3987" /></linearGradient><linearGradient
id="linearGradient12315"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop12317" /><stop
style="stop-color:#d30000;stop-opacity:1;"
offset="1"
id="stop12319" /></linearGradient><linearGradient
id="linearGradient11765"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop11767" /><stop
style="stop-color:#d00000;stop-opacity:1;"
offset="1"
id="stop11769" /></linearGradient><linearGradient
id="linearGradient11102"><stop
style="stop-color:#000000;stop-opacity:0.43137255;"
offset="0"
id="stop11104" /><stop
style="stop-color:#464646;stop-opacity:1;"
offset="1"
id="stop11106" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11765"
id="radialGradient11771"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12315"
id="radialGradient12321"
cx="48"
cy="48"
fx="48"
fy="48"
r="44"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3983"
id="radialGradient3989"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="layer15"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:groupmode="layer"
id="layer15"
inkscape:label="Close active"
style="display:inline"><path
sodipodi:type="arc"
style="fill:url(#radialGradient11771);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path11668"
sodipodi:cx="12"
sodipodi:cy="18"
sodipodi:rx="26.947977"
sodipodi:ry="26.947977"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952000000025;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 63.556404,32.443596 32.443596,63.556404"
id="path11670"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952000000025;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 63.556404,63.556404 32.443596,32.443596"
id="path11670-8"
inkscape:connector-curvature="0" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="close.svg"
inkscape:version="0.48.4 r9939"
xml:space="preserve"
width="96px"
viewBox="0 0 96 96"
version="1.1"
id="bigger"
height="96px"
enable-background="new 0 0 96 96"><metadata
id="metadata11"><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><defs
id="defs9"><linearGradient
id="linearGradient12315"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop12317" /><stop
style="stop-color:#d30000;stop-opacity:1;"
offset="1"
id="stop12319" /></linearGradient><linearGradient
id="linearGradient11765"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop11767" /><stop
style="stop-color:#d30000;stop-opacity:1;"
offset="1"
id="stop11769" /></linearGradient><linearGradient
id="linearGradient11102"><stop
style="stop-color:#000000;stop-opacity:0.43137255;"
offset="0"
id="stop11104" /><stop
style="stop-color:#464646;stop-opacity:1;"
offset="1"
id="stop11106" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11765"
id="radialGradient11771"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12315"
id="radialGradient12321"
cx="48"
cy="48"
fx="48"
fy="48"
r="44"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g12004"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:label="Close"
id="g12004"
inkscape:groupmode="layer"
style="display:inline"><path
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
sodipodi:ry="26.947977"
sodipodi:rx="26.947977"
sodipodi:cy="18"
sodipodi:cx="12"
id="path12006"
style="fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:5.38959551000000037;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path12008"
d="M 63.556404,32.443596 32.443596,63.556404"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path12010"
d="M 63.556404,63.556404 32.443596,32.443596"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
enable-background="new 0 0 96 96"
height="96px"
id="bigger"
version="1.1"
viewBox="0 0 96 96"
width="96px"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
sodipodi:docname="fullscreen-exit.svg"><metadata
id="metadata11"><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><defs
id="defs9" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g7032"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:groupmode="layer"
id="g7032"
inkscape:label="Fullscreen exit"
style="display:inline"><path
inkscape:connector-curvature="0"
id="path7034"
d="M 60,36 C 84.000001,12 84.000001,12 84.000001,12"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 80,39.999999 c -24,0 -24,0 -24,0 l 0,-24"
id="path7036"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path7038"
d="M 36.000001,59.999999 C 12,83.999999 12,83.999999 12,83.999999"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 16.000001,56 c 24,0 24,0 24,0 l 0,24"
id="path7040"
inkscape:connector-curvature="0" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
enable-background="new 0 0 96 96"
height="96px"
id="bigger"
version="1.1"
viewBox="0 0 96 96"
width="96px"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
sodipodi:docname="fullscreen.svg"><metadata
id="metadata11"><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><defs
id="defs9" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g6591"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
style="display:inline"
inkscape:label="Fullscreen"
id="g6591"
inkscape:groupmode="layer"><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 80,15.999999 c -24.000001,24 -24.000001,24 -24.000001,24"
id="path6595"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path6597"
d="m 60,12 c 24,0 24,0 24,0 l 0,24"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 16,80.000001 c 24.000001,-24 24.000001,-24 24.000001,-24"
id="path6601"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path6603"
d="M 36,84 C 12,84 12,84 12,84 L 12,60"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
enable-background="new 0 0 50 50"
height="16"
id="Layer_1"
version="1.1"
viewBox="0 0 16 16"
width="16"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
sodipodi:docname="handset.svg"><metadata
id="metadata11"><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></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs9" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1782"
inkscape:window-height="1130"
id="namedview7"
showgrid="true"
inkscape:zoom="57.5"
inkscape:cx="8"
inkscape:cy="8"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"><inkscape:grid
type="xygrid"
id="grid3756"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" /></sodipodi:namedview><rect
height="50"
width="50"
id="rect3"
x="0"
y="-34"
style="fill:none" /><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Handset"><path
d="m 5.5396163,9.252177 c 0,0 0.5563174,0.971756 0.6899968,1.226155 0.1341593,0.253679 0.1679992,0.516477 0.07056,0.676077 -0.1478397,0.241679 -1.4978334,2.271109 -1.6173529,2.441988 -0.1197594,0.17088 -0.4617578,0.253198 -0.7967963,0.01824 C 3.5507455,13.380878 2.2259516,12.301363 2.2401116,11.843925 2.2549916,11.385768 2.2967516,9.480896 5.8897746,5.888833 9.481838,2.29725 11.387189,2.25477 11.845107,2.24013 c 0.457919,-0.01464 1.537914,1.310154 1.772152,1.645192 0.233999,0.335519 0.14592,0.677997 -0.01872,0.797037 -0.18744,0.135359 -2.210391,1.469753 -2.44247,1.616632 -0.158159,0.10056 -0.422398,0.0636 -0.676316,-0.07056 C 10.225114,6.094992 9.2535981,5.538675 9.2535981,5.538675 c 0,0 -0.6688769,0.392158 -1.9948708,1.718392 -1.3262339,1.326474 -1.719112,1.99511 -1.719112,1.99511 z"
stroke-miterlimit="10"
id="path5"
inkscape:connector-curvature="0"
style="fill:#333333;fill-opacity:1;stroke:#222222;stroke-width:0.47999778;stroke-miterlimit:10;stroke-opacity:1" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="minimize-active.svg"
inkscape:version="0.48.4 r9939"
xml:space="preserve"
width="96px"
viewBox="0 0 96 96"
version="1.1"
id="bigger"
height="96px"
enable-background="new 0 0 96 96"><metadata
id="metadata11"><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><defs
id="defs9"><linearGradient
id="linearGradient3983"><stop
style="stop-color:#00ff00;stop-opacity:1;"
offset="0"
id="stop3985" /><stop
style="stop-color:#00d000;stop-opacity:1;"
offset="1"
id="stop3987" /></linearGradient><linearGradient
id="linearGradient12315"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop12317" /><stop
style="stop-color:#d30000;stop-opacity:1;"
offset="1"
id="stop12319" /></linearGradient><linearGradient
id="linearGradient11765"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop11767" /><stop
style="stop-color:#d00000;stop-opacity:1;"
offset="1"
id="stop11769" /></linearGradient><linearGradient
id="linearGradient11102"><stop
style="stop-color:#000000;stop-opacity:0.43137255;"
offset="0"
id="stop11104" /><stop
style="stop-color:#464646;stop-opacity:1;"
offset="1"
id="stop11106" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11765"
id="radialGradient11771"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12315"
id="radialGradient12321"
cx="48"
cy="48"
fx="48"
fy="48"
r="44"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3983"
id="radialGradient3989"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g3101"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:label="Minimize active"
id="g3101"
inkscape:groupmode="layer"
style="display:inline"><path
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
sodipodi:ry="26.947977"
sodipodi:rx="26.947977"
sodipodi:cy="18"
sodipodi:cx="12"
id="path3103"
style="fill:url(#radialGradient3989);fill-opacity:1;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path3105"
d="M 70.000078,48 25.999922,48"
style="fill:none;stroke:#000000;stroke-width:7.99999952000000025;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="minimize.svg"
inkscape:version="0.48.4 r9939"
xml:space="preserve"
width="96px"
viewBox="0 0 96 96"
version="1.1"
id="bigger"
height="96px"
enable-background="new 0 0 96 96"><metadata
id="metadata11"><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><defs
id="defs9"><linearGradient
id="linearGradient3983"><stop
style="stop-color:#00ff00;stop-opacity:1;"
offset="0"
id="stop3985" /><stop
style="stop-color:#00d000;stop-opacity:1;"
offset="1"
id="stop3987" /></linearGradient><linearGradient
id="linearGradient12315"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop12317" /><stop
style="stop-color:#d30000;stop-opacity:1;"
offset="1"
id="stop12319" /></linearGradient><linearGradient
id="linearGradient11765"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop11767" /><stop
style="stop-color:#d00000;stop-opacity:1;"
offset="1"
id="stop11769" /></linearGradient><linearGradient
id="linearGradient11102"><stop
style="stop-color:#000000;stop-opacity:0.43137255;"
offset="0"
id="stop11104" /><stop
style="stop-color:#464646;stop-opacity:1;"
offset="1"
id="stop11106" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11765"
id="radialGradient11771"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12315"
id="radialGradient12321"
cx="48"
cy="48"
fx="48"
fy="48"
r="44"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3983"
id="radialGradient3989"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g3082"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
style="display:inline"
inkscape:groupmode="layer"
id="g3082"
inkscape:label="Minimize"><path
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3084"
sodipodi:cx="12"
sodipodi:cy="18"
sodipodi:rx="26.947977"
sodipodi:ry="26.947977"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 70.000078,48 25.999922,48"
id="path3088"
inkscape:connector-curvature="0" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
enable-background="new 0 0 96 96"
height="96px"
id="bigger"
version="1.1"
viewBox="0 0 96 96"
width="96px"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
sodipodi:docname="scale.svg"><metadata
id="metadata11"><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><defs
id="defs9" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g4423"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
style="display:inline"
inkscape:label="Scale"
id="g4423"
inkscape:groupmode="layer"><path
inkscape:connector-curvature="0"
id="path4425"
d="M 80,4 H 68 c -2.209,0 -4,1.791 -4,4 0,2.209 1.791,4 4,4 h 12 c 2.21,0 4,1.79 4,4 v 12 c 0,2.209 1.791,4 4,4 2.209,0 4,-1.791 4,-4 V 16 C 92,9.373 86.627,4 80,4 z" /><path
inkscape:connector-curvature="0"
id="path4427"
d="M 28,4 H 16 C 9.373,4 4,9.373 4,16 v 12 c 0,2.209 1.791,4 4,4 2.209,0 4,-1.791 4,-4 V 16 c 0,-2.21 1.79,-4 4,-4 h 12 c 2.209,0 4,-1.791 4,-4 0,-2.209 -1.791,-4 -4,-4 z" /><path
inkscape:connector-curvature="0"
id="path4429"
d="m 88,64 c -2.209,0 -4,1.791 -4,4 v 12 c 0,2.21 -1.79,4 -4,4 H 68 c -2.209,0 -4,1.791 -4,4 0,2.209 1.791,4 4,4 h 12 c 6.627,0 12,-5.373 12,-12 V 68 c 0,-2.209 -1.791,-4 -4,-4 z" /><path
inkscape:connector-curvature="0"
id="path4431"
d="M 28,84 H 16 c -2.21,0 -4,-1.79 -4,-4 V 68 c 0,-2.209 -1.791,-4 -4,-4 -2.209,0 -4,1.791 -4,4 v 12 c 0,6.627 5.373,12 12,12 h 12 c 2.209,0 4,-1.791 4,-4 0,-2.209 -1.791,-4 -4,-4 z" /><rect
ry="10"
y="32"
x="24"
height="32"
width="48"
id="rect4433"
style="fill:#000000;fill-opacity:1;stroke:none" /></g></svg>
\ No newline at end of file
<?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="svg6921"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="screen.svg">
<defs
id="defs6923" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="53.9375"
inkscape:cx="8"
inkscape:cy="8"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:grid-bbox="true"
inkscape:document-units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1667"
inkscape:window-height="1073"
inkscape:window-x="83"
inkscape:window-y="22"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid6929"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata6926">
<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
id="layer1"
inkscape:label="Screen"
inkscape:groupmode="layer">
<rect
style="fill:#545454;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="rect7463"
width="11"
height="7"
x="2.5"
y="2.5" />
<rect
style="fill:none;stroke:#333333;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect7465"
width="7"
height="0.19999999"
x="4.5"
y="13.3" />
<rect
style="fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:0.81199998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect7465-2"
width="4.1880002"
height="1.188"
x="-13.594"
y="7.4060001"
transform="matrix(0,-1,1,0,0,0)" />
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="screenshot.svg"
inkscape:version="0.48.4 r9939"
xml:space="preserve"
width="96px"
viewBox="0 0 96 96"
version="1.1"
id="bigger"
height="96px"
enable-background="new 0 0 96 96"><metadata
id="metadata11"><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><defs
id="defs9"><linearGradient
id="linearGradient11102"><stop
style="stop-color:#000000;stop-opacity:0.43137255;"
offset="0"
id="stop11104" /><stop
style="stop-color:#464646;stop-opacity:1;"
offset="1"
id="stop11106" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11102"
id="radialGradient11108"
cx="14.5"
cy="13"
fx="14.5"
fy="13"
r="10.060094"
gradientTransform="matrix(1,0,0,0.95029867,0,0.64611723)"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g8714"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:groupmode="layer"
id="g8714"
inkscape:label="Screenshot"
style="display:inline"><rect
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline"
id="rect9869"
width="42"
height="14"
x="27"
y="19"
rx="4.5778842"
ry="3.5097113" /><rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:6.00000048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline"
id="rect9845"
width="82"
height="46"
x="7"
y="31"
ry="5.0138731"
rx="5.0138731" /><path
sodipodi:type="arc"
style="fill:none;stroke:#ffffff;stroke-width:3.72409511;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
id="path9681"
sodipodi:cx="14.5"
sodipodi:cy="13"
sodipodi:rx="8.5"
sodipodi:ry="8"
d="M 23,13 A 8.5,8 0 1 1 6,13 8.5,8 0 1 1 23,13 z"
transform="matrix(1.8235294,0,0,1.9375002,21.558824,28.812497)" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
enable-background="new 0 0 96 96"
height="96px"
id="bigger"
version="1.1"
viewBox="0 0 96 96"
width="96px"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
sodipodi:docname="viewonly.svg"><metadata
id="metadata11"><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><defs
id="defs9" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g5470"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:label="View only"
id="g5470"
inkscape:groupmode="layer"
style="display:inline"><path
transform="translate(-2.0000003,-10.000001)"
d="m 66,46 a 28,28 0 1 1 -56,0 28,28 0 1 1 56,0 z"
sodipodi:ry="28"
sodipodi:rx="28"
sodipodi:cy="46"
sodipodi:cx="38"
id="path5472"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path5474"
d="M 88,87.999999 C 56,55.39701 56,56.000768 56,56.000768"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="actions.svg"
inkscape:version="0.48.4 r9939"
xml:space="preserve"
width="96px"
viewBox="0 0 96 96"
version="1.1"
id="bigger"
height="96px"
enable-background="new 0 0 96 96"><metadata
id="metadata11"><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></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs9"><linearGradient
id="linearGradient3983"><stop
style="stop-color:#00ff00;stop-opacity:1;"
offset="0"
id="stop3985" /><stop
style="stop-color:#00d000;stop-opacity:1;"
offset="1"
id="stop3987" /></linearGradient><linearGradient
id="linearGradient12315"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop12317" /><stop
style="stop-color:#d30000;stop-opacity:1;"
offset="1"
id="stop12319" /></linearGradient><linearGradient
id="linearGradient11765"><stop
style="stop-color:#ff0000;stop-opacity:1;"
offset="0"
id="stop11767" /><stop
style="stop-color:#d00000;stop-opacity:1;"
offset="1"
id="stop11769" /></linearGradient><linearGradient
id="linearGradient11102"><stop
style="stop-color:#000000;stop-opacity:0.43137255;"
offset="0"
id="stop11104" /><stop
style="stop-color:#464646;stop-opacity:1;"
offset="1"
id="stop11106" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11765"
id="radialGradient11771"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12315"
id="radialGradient12321"
cx="48"
cy="48"
fx="48"
fy="48"
r="44"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3983"
id="radialGradient3989"
cx="12"
cy="18"
fx="12"
fy="18"
r="29.642775"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1075"
id="namedview7"
showgrid="true"
inkscape:zoom="9.0104167"
inkscape:cx="48"
inkscape:cy="48"
inkscape:window-x="114"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="g4423"
showguides="true"
inkscape:guide-bbox="true"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><g
inkscape:label="Close"
id="g12004"
inkscape:groupmode="layer"
style="display:none"><path
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
sodipodi:ry="26.947977"
sodipodi:rx="26.947977"
sodipodi:cy="18"
sodipodi:cx="12"
id="path12006"
style="fill:none;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path12008"
d="M 63.556404,32.443596 32.443596,63.556404"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path12010"
d="M 63.556404,63.556404 32.443596,32.443596"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
inkscape:groupmode="layer"
id="layer15"
inkscape:label="Close active"
style="display:none"><path
sodipodi:type="arc"
style="fill:url(#radialGradient11771);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path11668"
sodipodi:cx="12"
sodipodi:cy="18"
sodipodi:rx="26.947977"
sodipodi:ry="26.947977"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 63.556404,32.443596 32.443596,63.556404"
id="path11670"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 63.556404,63.556404 32.443596,32.443596"
id="path11670-8"
inkscape:connector-curvature="0" /></g><g
style="display:none"
inkscape:groupmode="layer"
id="g12232"
inkscape:label="Close alt"><rect
ry="8.780488"
y="8"
x="8"
height="80"
width="80"
id="rect5204-8"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 63.556404,32.443596 32.443596,63.556404"
id="path12236"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 63.556404,63.556404 32.443596,32.443596"
id="path12238"
inkscape:connector-curvature="0" /></g><g
inkscape:label="Close active alt"
id="g12269"
inkscape:groupmode="layer"
style="display:none"><rect
style="fill:url(#radialGradient12321);fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline"
id="rect12271"
width="80"
height="80"
x="8"
y="8"
ry="8.780488" /><path
inkscape:connector-curvature="0"
id="path12273"
d="M 63.556404,32.443596 32.443596,63.556404"
style="fill:none;stroke:#ffffff;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path12275"
d="M 63.556404,63.556404 32.443596,32.443596"
style="fill:none;stroke:#ffffff;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
style="display:none"
inkscape:groupmode="layer"
id="g3082"
inkscape:label="Minimize"><path
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3084"
sodipodi:cx="12"
sodipodi:cy="18"
sodipodi:rx="26.947977"
sodipodi:ry="26.947977"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 70.000078,48 25.999922,48"
id="path3088"
inkscape:connector-curvature="0" /></g><g
inkscape:label="Minimize active"
id="g3101"
inkscape:groupmode="layer"
style="display:none"><path
transform="matrix(1.4843415,0,0,1.4843415,30.187902,21.281853)"
d="m 38.947977,18 a 26.947977,26.947977 0 1 1 -53.895954,0 26.947977,26.947977 0 1 1 53.895954,0 z"
sodipodi:ry="26.947977"
sodipodi:rx="26.947977"
sodipodi:cy="18"
sodipodi:cx="12"
id="path3103"
style="fill:url(#radialGradient3989);fill-opacity:1;stroke:#000000;stroke-width:5.38959551;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path3105"
d="M 70.000078,48 25.999922,48"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
style="display:none"
inkscape:label="Fullscreen enter"
id="g6591"
inkscape:groupmode="layer"><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 80,15.999999 c -24.000001,24 -24.000001,24 -24.000001,24"
id="path6595"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path6597"
d="m 60,12 c 24,0 24,0 24,0 l 0,24"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 16,80.000001 c 24.000001,-24 24.000001,-24 24.000001,-24"
id="path6601"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path6603"
d="M 36,84 C 12,84 12,84 12,84 L 12,60"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
inkscape:groupmode="layer"
id="g7032"
inkscape:label="Fullscreen exit"
style="display:none"><path
inkscape:connector-curvature="0"
id="path7034"
d="M 60,36 C 84.000001,12 84.000001,12 84.000001,12"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 80,39.999999 c -24,0 -24,0 -24,0 l 0,-24"
id="path7036"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path7038"
d="M 36.000001,59.999999 C 12,83.999999 12,83.999999 12,83.999999"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 16.000001,56 c 24,0 24,0 24,0 l 0,24"
id="path7040"
inkscape:connector-curvature="0" /></g><g
inkscape:groupmode="layer"
id="g6442"
inkscape:label="Fullscreen enter 28"
style="display:none"><path
inkscape:connector-curvature="0"
id="path6446"
d="m 80,15.999999 c -24.000001,24 -24.000001,24 -24.000001,24"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 64,12 c 20,0 20,0 20,0 l 0,20"
id="path6463"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
id="path6446-2"
d="m 16,80.000001 c 24.000001,-24 24.000001,-24 24.000001,-24"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 32,84 C 12,84 12,84 12,84 L 12,64"
id="path6463-5"
inkscape:connector-curvature="0" /></g><g
style="display:none"
inkscape:label="Fullscreen enter full 28"
id="g6203"
inkscape:groupmode="layer"><path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 64,12 20,0 0,20 z"
id="path6209"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 80,16 C 56,40 56,40 56,40"
id="path6211"
inkscape:connector-curvature="0" /><path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 32,84 12,84 12,64 z"
id="path6209-7"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 15.999999,79.999996 C 39.999998,56 39.999998,56 39.999998,56"
id="path6211-7"
inkscape:connector-curvature="0" /></g><g
inkscape:groupmode="layer"
id="g6925"
inkscape:label="Fullscreen enter full 32"
style="display:none"><path
inkscape:connector-curvature="0"
id="path6927"
d="m 60,12 24,0 0,24 z"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path6929"
d="M 80,16 C 56,40 56,40 56,40"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path6931"
d="M 36,84 12,84 12,60 z"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path6933"
d="M 15.999999,79.999996 C 39.999998,56 39.999998,56 39.999998,56"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Rectangle expand"
style="display:none"><path
style="display:inline"
inkscape:connector-curvature="0"
d="m 26.000001,40 c 2.039366,0 3.693084,1.748132 3.699621,3.910841 l 0,34.741523 11.987818,-12.672226 c 1.444553,-1.527024 3.784594,-1.527024 5.229147,0 1.444551,1.527024 1.444551,4.000664 0,5.527688 l -18.302012,19.34691 c -1.444553,1.527019 -3.784595,1.527019 -5.229147,0 L 5.0834144,71.507826 c -1.4445526,-1.527024 -1.4445526,-4.000665 0,-5.527689 1.4445516,-1.527024 3.7845932,-1.527024 5.2291466,0 l 11.987818,12.672226 0,-34.741523 C 22.300379,41.755041 23.960633,40 26.000001,40 z"
id="path3-4" /><path
inkscape:connector-curvature="0"
d="m 40,26 c 0,-2.039367 1.748132,-3.693085 3.910841,-3.699622 l 34.741523,0 -12.672226,-11.987817 c -1.527024,-1.444554 -1.527024,-3.784596 0,-5.2291472 1.527024,-1.4445519 4.000664,-1.4445517 5.527688,1e-7 l 19.34691,18.3020121 c 1.527019,1.444553 1.527019,3.784596 0,5.229147 l -19.34691,18.302013 c -1.527024,1.444553 -4.000665,1.444553 -5.527689,0 -1.527024,-1.444551 -1.527024,-3.784593 0,-5.229146 l 12.672226,-11.987819 -34.741523,0 C 41.755041,29.699621 40,28.039367 40,26 z"
id="path3" /><rect
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6"
id="rect4278"
width="28"
height="28"
x="12"
y="12"
ry="3.2307694" /></g><g
style="display:none"
inkscape:groupmode="layer"
id="g4801"
inkscape:label="Contents expand"><rect
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6"
id="rect4803"
width="80"
height="80"
x="8"
y="8"
ry="8.780488" /><rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6"
id="rect4805"
width="24"
height="24"
x="24"
y="48"
ry="2.5714285" /><path
style="fill:none;stroke:#000000;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 49,47 C 71,25 71,25 71,25"
id="path4807"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 73,41 C 73,23 73,23 73,23 L 55,23"
id="path4809"
inkscape:connector-curvature="0" /></g><g
inkscape:label="Contents shrink"
id="g5202"
inkscape:groupmode="layer"
style="display:none"><rect
ry="8.780488"
y="8"
x="8"
height="80"
width="80"
id="rect5204"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6" /><rect
ry="2.5714285"
y="48"
x="24"
height="24"
width="24"
id="rect5206"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6" /><path
inkscape:connector-curvature="0"
id="path5208"
d="M 73.000001,23 C 53,43 53,43 53,43"
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path5210"
d="m 51,27 c 0,18 0,18 0,18 l 18,0"
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
inkscape:label="Contents expand alt"
id="g4186"
inkscape:groupmode="layer"
style="display:none"><rect
ry="8.780488"
y="8"
x="8"
height="80"
width="80"
id="rect4188"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6" /><rect
ry="3"
y="44"
x="24"
height="28"
width="28"
id="rect4190"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6" /><path
inkscape:connector-curvature="0"
id="path4192"
d="M 50.999999,44.999999 C 70.999999,25 70.999999,25 70.999999,25"
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /><path
inkscape:connector-curvature="0"
id="path4194"
d="M 73,41 C 73,23 73,23 73,23 L 55,23"
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
style="display:none"
inkscape:groupmode="layer"
id="g5353"
inkscape:label="Contents shrink alt"><rect
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6"
id="rect5355"
width="80"
height="80"
x="8"
y="8"
ry="8.780488" /><rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6"
id="rect5357"
width="28"
height="28"
x="24"
y="44"
ry="3" /><path
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 73.000001,23 c -18,17.999999 -18,17.999999 -18,17.999999"
id="path5359"
inkscape:connector-curvature="0" /><path
style="fill:none;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 55,23 c 0,18 0,18 0,18 l 18,0"
id="path5361"
inkscape:connector-curvature="0" /></g><g
inkscape:groupmode="layer"
id="g8714"
inkscape:label="Screenshot"
style="display:none"><rect
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline"
id="rect9869"
width="42"
height="14"
x="27"
y="19"
rx="4.5778842"
ry="3.5097113" /><rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:6.00000048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline"
id="rect9845"
width="82"
height="46"
x="7"
y="31"
ry="5.0138731"
rx="5.0138731" /><path
sodipodi:type="arc"
style="fill:none;stroke:#ffffff;stroke-width:3.72409511;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
id="path9681"
sodipodi:cx="14.5"
sodipodi:cy="13"
sodipodi:rx="8.5"
sodipodi:ry="8"
d="M 23,13 A 8.5,8 0 1 1 6,13 8.5,8 0 1 1 23,13 z"
transform="matrix(1.8235294,0,0,1.9375002,21.558824,28.812497)" /></g><g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Screenshots folder"
style="display:none"><rect
style="fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="rect4421"
width="88"
height="8"
x="4"
y="34"
rx="2.8039217"
ry="0.66666657" /><path
inkscape:connector-curvature="0"
id="path5-0"
d="M 4,73.937501 C 4,77.166667 7.4925,80 11.755,80 H 84.9875 C 89.25,80 92,77.166667 92,73.937501 V 40 H 4 v 33.937501 z" /><path
inkscape:connector-curvature="0"
id="path7-3"
d="m 92,31 c 0,-3.875 -2.75,-7 -7.0125,-7 0,0 -42.5425,0 -45.925,0 C 37.77,24 36.12,21.525 33.975,19.25 32.0225,17.175 29.9325,16 28.3375,16 26.275,16 11.755,16 11.755,16 7.4925,16 4,18.225 4,22.075 L 4,36 92,36 z"
sodipodi:nodetypes="scscscsccs" /></g><g
style="display:none"
inkscape:label="Screenshot alt"
id="g11448"
inkscape:groupmode="layer"><rect
ry="4"
rx="4"
y="19"
x="27"
height="12"
width="42"
id="rect11450"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.6;display:inline" /><path
id="path11454"
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.72409511;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
d="M 48,34.867185 C 37.712468,34.867185 29.080883,43.36797 29.080883,54 29.080883,64.63203 37.712468,73.132814 48,73.132814 58.287533,73.132814 66.919118,64.63203 66.919118,54 66.919118,43.36797 58.287533,34.867185 48,34.867185 z m 0,7.265626 c 6.833294,0 12.080883,5.378389 12.080883,11.867189 0,6.488798 -5.247589,11.867188 -12.080883,11.867188 -6.833295,0 -12.080882,-5.37839 -12.080882,-11.867188 0,-6.4888 5.247587,-11.867189 12.080882,-11.867189 z M 13,28 83,28 c 4.986,0 9,4.014 9,9 l 0,34 c 0,4.986 -4.014,9 -9,9 L 13,80 C 8.0140005,80 4.0000005,75.986 4.0000005,71 l 0,-34 c 0,-4.986 4.014,-9 8.9999995,-9 z"
inkscape:connector-curvature="0" /></g><g
inkscape:label="View only"
id="g5470"
inkscape:groupmode="layer"
style="display:none"><path
transform="translate(-2.0000003,-10.000001)"
d="m 66,46 a 28,28 0 1 1 -56,0 28,28 0 1 1 56,0 z"
sodipodi:ry="28"
sodipodi:rx="28"
sodipodi:cy="46"
sodipodi:cx="38"
id="path5472"
style="fill:none;stroke:#000000;stroke-width:7.99999952;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path5474"
d="M 88,87.999999 C 56,55.39701 56,56.000768 56,56.000768"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
style="display:none"
inkscape:label="View only alt"
id="g5588"
inkscape:groupmode="layer"><path
transform="matrix(0.92857143,0,0,0.92857143,-1.2857144,-8.714286)"
d="m 66,46 a 28,28 0 1 1 -56,0 28,28 0 1 1 56,0 z"
sodipodi:ry="28"
sodipodi:rx="28"
sodipodi:cy="46"
sodipodi:cx="38"
id="path5590"
style="fill:none;stroke:#000000;stroke-width:8.6153841;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
sodipodi:type="arc" /><path
inkscape:connector-curvature="0"
id="path5592"
d="M 88,87.999999 C 54,53.359323 54,54.000816 54,54.000816"
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="Expand"
style="display:none"><path
d="M 80,4 H 68 c -2.209,0 -4,1.791 -4,4 0,2.209 1.791,4 4,4 h 12 c 2.21,0 4,1.79 4,4 v 12 c 0,2.209 1.791,4 4,4 2.209,0 4,-1.791 4,-4 V 16 C 92,9.373 86.627,4 80,4 z"
id="path3-7"
inkscape:connector-curvature="0" /><path
d="M 28,4 H 16 C 9.373,4 4,9.373 4,16 v 12 c 0,2.209 1.791,4 4,4 2.209,0 4,-1.791 4,-4 V 16 c 0,-2.21 1.79,-4 4,-4 h 12 c 2.209,0 4,-1.791 4,-4 0,-2.209 -1.791,-4 -4,-4 z"
id="path5"
inkscape:connector-curvature="0" /><path
d="m 88,64 c -2.209,0 -4,1.791 -4,4 v 12 c 0,2.21 -1.79,4 -4,4 H 68 c -2.209,0 -4,1.791 -4,4 0,2.209 1.791,4 4,4 h 12 c 6.627,0 12,-5.373 12,-12 V 68 c 0,-2.209 -1.791,-4 -4,-4 z"
id="path7"
inkscape:connector-curvature="0" /><path
d="M 28,84 H 16 c -2.21,0 -4,-1.79 -4,-4 V 68 c 0,-2.209 -1.791,-4 -4,-4 -2.209,0 -4,1.791 -4,4 v 12 c 0,6.627 5.373,12 12,12 h 12 c 2.209,0 4,-1.791 4,-4 0,-2.209 -1.791,-4 -4,-4 z"
id="path9"
inkscape:connector-curvature="0" /></g><g
style="display:inline"
inkscape:label="Scale"
id="g4423"
inkscape:groupmode="layer"><path
inkscape:connector-curvature="0"
id="path4425"
d="M 80,4 H 68 c -2.209,0 -4,1.791 -4,4 0,2.209 1.791,4 4,4 h 12 c 2.21,0 4,1.79 4,4 v 12 c 0,2.209 1.791,4 4,4 2.209,0 4,-1.791 4,-4 V 16 C 92,9.373 86.627,4 80,4 z" /><path
inkscape:connector-curvature="0"
id="path4427"
d="M 28,4 H 16 C 9.373,4 4,9.373 4,16 v 12 c 0,2.209 1.791,4 4,4 2.209,0 4,-1.791 4,-4 V 16 c 0,-2.21 1.79,-4 4,-4 h 12 c 2.209,0 4,-1.791 4,-4 0,-2.209 -1.791,-4 -4,-4 z" /><path
inkscape:connector-curvature="0"
id="path4429"
d="m 88,64 c -2.209,0 -4,1.791 -4,4 v 12 c 0,2.21 -1.79,4 -4,4 H 68 c -2.209,0 -4,1.791 -4,4 0,2.209 1.791,4 4,4 h 12 c 6.627,0 12,-5.373 12,-12 V 68 c 0,-2.209 -1.791,-4 -4,-4 z" /><path
inkscape:connector-curvature="0"
id="path4431"
d="M 28,84 H 16 c -2.21,0 -4,-1.79 -4,-4 V 68 c 0,-2.209 -1.791,-4 -4,-4 -2.209,0 -4,1.791 -4,4 v 12 c 0,6.627 5.373,12 12,12 h 12 c 2.209,0 4,-1.791 4,-4 0,-2.209 -1.791,-4 -4,-4 z" /><rect
ry="10"
y="32"
x="24"
height="32"
width="48"
id="rect4433"
style="fill:#000000;fill-opacity:1;stroke:none" /></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
enable-background="new 0 0 50 50"
height="16"
id="Layer_1"
version="1.1"
viewBox="0 0 16 16"
width="16"
xml:space="preserve"
inkscape:version="0.48.4 r9939"
sodipodi:docname="handset.svg"><metadata
id="metadata11"><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></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs9" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1782"
inkscape:window-height="1130"
id="namedview7"
showgrid="true"
inkscape:zoom="57.5"
inkscape:cx="8"
inkscape:cy="8"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"><inkscape:grid
type="xygrid"
id="grid3756"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" /></sodipodi:namedview><rect
height="50"
width="50"
id="rect3"
x="0"
y="-34"
style="fill:none" /><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Handset"><path
d="m 5.5396163,9.252177 c 0,0 0.5563174,0.971756 0.6899968,1.226155 0.1341593,0.253679 0.1679992,0.516477 0.07056,0.676077 -0.1478397,0.241679 -1.4978334,2.271109 -1.6173529,2.441988 -0.1197594,0.17088 -0.4617578,0.253198 -0.7967963,0.01824 C 3.5507455,13.380878 2.2259516,12.301363 2.2401116,11.843925 2.2549916,11.385768 2.2967516,9.480896 5.8897746,5.888833 9.481838,2.29725 11.387189,2.25477 11.845107,2.24013 c 0.457919,-0.01464 1.537914,1.310154 1.772152,1.645192 0.233999,0.335519 0.14592,0.677997 -0.01872,0.797037 -0.18744,0.135359 -2.210391,1.469753 -2.44247,1.616632 -0.158159,0.10056 -0.422398,0.0636 -0.676316,-0.07056 C 10.225114,6.094992 9.2535981,5.538675 9.2535981,5.538675 c 0,0 -0.6688769,0.392158 -1.9948708,1.718392 -1.3262339,1.326474 -1.719112,1.99511 -1.719112,1.99511 z"
stroke-miterlimit="10"
id="path5"
inkscape:connector-curvature="0"
style="fill:#333333;fill-opacity:1;stroke:#222222;stroke-width:0.47999778;stroke-miterlimit:10;stroke-opacity:1" /></g></svg>
\ No newline at end of file
<?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="svg6921"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="screen.svg">
<defs
id="defs6923" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="53.9375"
inkscape:cx="8"
inkscape:cy="8"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:grid-bbox="true"
inkscape:document-units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1667"
inkscape:window-height="1073"
inkscape:window-x="83"
inkscape:window-y="22"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid6929"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata6926">
<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
id="layer1"
inkscape:label="Screen"
inkscape:groupmode="layer">
<rect
style="fill:#545454;fill-opacity:1;stroke:#333333;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="rect7463"
width="11"
height="7"
x="2.5"
y="2.5" />
<rect
style="fill:none;stroke:#333333;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect7465"
width="7"
height="0.19999999"
x="4.5"
y="13.3" />
<rect
style="fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:0.81199998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect7465-2"
width="4.1880002"
height="1.188"
x="-13.594"
y="7.4060001"
transform="matrix(0,-1,1,0,0,0)" />
</g>
</svg>
......@@ -2136,6 +2136,83 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="screen_sharing_page">
<layout class="QVBoxLayout" name="screen_sharing_layout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="screen_sharing_group_box">
<property name="title">
<string>Screen Sharing Settings</string>
</property>
<layout class="QGridLayout" name="screen_sharing_box_layout">
<property name="horizontalSpacing">
<number>3</number>
</property>
<item row="0" column="1">
<widget class="QLineEdit" name="screenshots_directory_editor">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="screenshots_directory_browse_button">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="screenshots_directory_label">
<property name="text">
<string>Screenshots Directory:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<spacer name="screen_sharing_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1" colspan="2">
<widget class="QCheckBox" name="screen_sharing_fullscreen_button">
<property name="text">
<string>Open the viewer window in fullscreen mode</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QCheckBox" name="screen_sharing_viewonly_button">
<property name="text">
<string>Open the viewer window in view only mode</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QCheckBox" name="screen_sharing_scale_button">
<property name="text">
<string>Scale the remote screen to fit the viewer window</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="file_transfers_page">
<layout class="QVBoxLayout" name="file_transfer_layout">
<property name="margin">
......@@ -2747,6 +2824,7 @@
<addaction name="separator"/>
<addaction name="audio_action"/>
<addaction name="chat_action"/>
<addaction name="screen_sharing_action"/>
<addaction name="file_transfer_action"/>
<addaction name="logging_action"/>
<addaction name="advanced_action"/>
......@@ -2826,6 +2904,18 @@
<string>Advanced</string>
</property>
</action>
<action name="screen_sharing_action">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>icons/screensharing.png</normaloff>icons/screensharing.png</iconset>
</property>
<property name="text">
<string>Screen Sharing</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
......@@ -2833,16 +2923,16 @@
<extends>QWidget</extends>
<header>QtWebKit/QWebView</header>
</customwidget>
<customwidget>
<class>ChatWebView</class>
<extends>QWebView</extends>
<header>blink.chatwindow</header>
</customwidget>
<customwidget>
<class>LineEdit</class>
<extends>QLineEdit</extends>
<header>blink.widgets.lineedit</header>
</customwidget>
<customwidget>
<class>ChatWebView</class>
<extends>QWebView</extends>
<header>blink.chatwindow</header>
</customwidget>
<customwidget>
<class>SIPPortEditor</class>
<extends>QSpinBox</extends>
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>screensharing_dialog</class>
<widget class="QDialog" name="screensharing_dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>190</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>190</height>
</size>
</property>
<property name="windowTitle">
<string>Screen sharing authentication</string>
</property>
<layout class="QGridLayout" name="dialog_layout">
<item row="3" column="0">
<widget class="QLabel" name="password_label">
<property name="text">
<string>Password:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLabel" name="message_label">
<property name="minimumSize">
<size>
<width>0</width>
<height>64</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>64</height>
</size>
</property>
<property name="text">
<string>Screen sharing requires authentication</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="icon_label">
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="pixmap">
<pixmap>icons/screensharing.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<layout class="QHBoxLayout" name="button_layout">
<item>
<spacer name="button_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="reject_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="accept_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Ok</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="3">
<spacer name="footer_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" colspan="3">
<spacer name="header_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="1" colspan="2">
<widget class="QLineEdit" name="password_editor">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="username_editor"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="username_label">
<property name="text">
<string>Username:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>username_editor</tabstop>
<tabstop>password_editor</tabstop>
<tabstop>accept_button</tabstop>
<tabstop>reject_button</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>reject_button</sender>
<signal>clicked()</signal>
<receiver>screensharing_dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>207</x>
<y>176</y>
</hint>
<hint type="destinationlabel">
<x>126</x>
<y>189</y>
</hint>
</hints>
</connection>
<connection>
<sender>accept_button</sender>
<signal>clicked()</signal>
<receiver>screensharing_dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>176</y>
</hint>
<hint type="destinationlabel">
<x>98</x>
<y>147</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>fullscreen_toolbox</class>
<widget class="QWidget" name="fullscreen_toolbox">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>385</width>
<height>37</height>
</rect>
</property>
<property name="windowTitle">
<string>Screensharing toolbox</string>
</property>
<property name="styleSheet">
<string notr="true">QWidget#fullscreen_toolbox {
background-color: white;
border: 1px outset #222222;
border-top: 0px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
</string>
</property>
<layout class="QHBoxLayout" name="toolbox_layout">
<property name="spacing">
<number>1</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="color_depth_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Higher color depths provide better quality but also require more bandwidth. Lowering it may help if screen sharing feels sluggish or it is lagging.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<item>
<property name="text">
<string>Default Color Depth</string>
</property>
</item>
<item>
<property name="text">
<string>True Color (24 bits)</string>
</property>
</item>
<item>
<property name="text">
<string>High Color (16 bits)</string>
</property>
</item>
<item>
<property name="text">
<string>Low Color (8 bits)</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="spacer_left">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="scale_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Scale</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/scale.svg</normaloff>icons/scale.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="viewonly_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>View Only</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/viewonly.svg</normaloff>icons/viewonly.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="screenshot_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Screenshot</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/screenshot.svg</normaloff>icons/screenshot.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="fullscreen_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Fullscreen</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/fullscreen.svg</normaloff>
<normalon>icons/fullscreen-exit.svg</normalon>
<disabledon>icons/fullscreen-exit.svg</disabledon>
<activeon>icons/fullscreen-exit.svg</activeon>
<selectedon>icons/fullscreen-exit.svg</selectedon>icons/fullscreen.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="spacer_right">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="minimize_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Minimize</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/minimize.svg</normaloff>
<activeoff>icons/minimize-active.svg</activeoff>icons/minimize.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="close_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/close.svg</normaloff>
<activeoff>icons/close-active.svg</activeoff>icons/close.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
<action name="scale_action">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>icons/scale.svg</normaloff>icons/scale.svg</iconset>
</property>
<property name="text">
<string>Scale</string>
</property>
<property name="toolTip">
<string>Scale remote screen to fit window size</string>
</property>
</action>
<action name="viewonly_action">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>icons/viewonly.svg</normaloff>icons/viewonly.svg</iconset>
</property>
<property name="text">
<string>View Only</string>
</property>
<property name="toolTip">
<string>View only</string>
</property>
</action>
<action name="screenshot_action">
<property name="icon">
<iconset>
<normaloff>icons/screenshot.svg</normaloff>icons/screenshot.svg</iconset>
</property>
<property name="text">
<string>Screenshot</string>
</property>
<property name="toolTip">
<string>Take a screenshot</string>
</property>
</action>
<action name="fullscreen_action">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>icons/fullscreen.svg</normaloff>
<normalon>icons/fullscreen-exit.svg</normalon>
<disabledon>icons/fullscreen-exit.svg</disabledon>
<activeon>icons/fullscreen-exit.svg</activeon>
<selectedon>icons/fullscreen-exit.svg</selectedon>icons/fullscreen.svg</iconset>
</property>
<property name="text">
<string>Fullscreen</string>
</property>
</action>
<action name="minimize_action">
<property name="icon">
<iconset>
<normaloff>icons/minimize.svg</normaloff>
<activeoff>icons/minimize-active.svg</activeoff>icons/minimize.svg</iconset>
</property>
<property name="text">
<string>Minimize</string>
</property>
<property name="toolTip">
<string>Minimize</string>
</property>
</action>
<action name="close_action">
<property name="icon">
<iconset>
<normaloff>icons/close.svg</normaloff>
<activeoff>icons/close-active.svg</activeoff>icons/close.svg</iconset>
</property>
<property name="text">
<string>Close</string>
</property>
<property name="toolTip">
<string>End screen sharing and close the window</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>screensharing_window</class>
<widget class="QMainWindow" name="screensharing_window">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1006</width>
<height>663</height>
</rect>
</property>
<property name="windowTitle">
<string>Blink Screen Sharing</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>icons/blink48.png</normaloff>icons/blink48.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="window_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="toolbar" native="true">
<layout class="QHBoxLayout" name="toolbar_layout">
<property name="spacing">
<number>1</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="color_depth_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Higher color depths provide better quality but also require more bandwidth. Lowering it may help if screen sharing feels sluggish or it is lagging.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<item>
<property name="text">
<string>Default Color Depth</string>
</property>
</item>
<item>
<property name="text">
<string>True Color (24 bits)</string>
</property>
</item>
<item>
<property name="text">
<string>High Color (16 bits)</string>
</property>
</item>
<item>
<property name="text">
<string>Low Color (8 bits)</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="toolbar_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>15</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="scale_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Scale</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/scale.svg</normaloff>icons/scale.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="viewonly_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>View only</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/viewonly.svg</normaloff>icons/viewonly.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="screenshot_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="text">
<string>Screenshot</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/screenshot.svg</normaloff>icons/screenshot.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="fullscreen_button">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Fullscreen</string>
</property>
<property name="icon">
<iconset>
<normaloff>icons/fullscreen.svg</normaloff>
<normalon>icons/fullscreen-exit.svg</normalon>
<disabledon>icons/fullscreen-exit.svg</disabledon>
<activeon>icons/fullscreen-exit.svg</activeon>
<selectedon>icons/fullscreen-exit.svg</selectedon>icons/fullscreen.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scroll_area">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<widget class="QWidget" name="vncviewer">
<property name="geometry">
<rect>
<x>7</x>
<y>7</y>
<width>987</width>
<height>611</height>
</rect>
</property>
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="focusPolicy">
<enum>Qt::WheelFocus</enum>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
#!/usr/bin/env python
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
from itertools import chain
import glob
import os
......@@ -32,6 +35,7 @@ setup(name = "blink",
"Programming Language :: Python"
],
packages = find_packages('blink'),
ext_modules = cythonize([Extension(name="blink.screensharing._rfb", sources=["blink/screensharing/_rfb.pyx"], libraries=["vncclient"])]),
data_files = list_resources('resources', destination_directory='share/blink'),
scripts = ['bin/blink']
)
......
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