import os import platform from PyQt5 import uic from PyQt5.QtCore import Qt, QEasingCurve, QEvent, QPoint, QPropertyAnimation, QRect, QTimer, QUrl from PyQt5.QtGui import QDesktopServices, QIcon, QImage, QPainter, QTransform, qRgb from PyQt5.QtWidgets import QApplication, QFrame, QMenu, QStyle, QStyleOption, QStylePainter, QWidget 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 from blink.util import translate __all__ = ['ScreensharingWindow', 'VNCViewer'] 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 and event.angleDelta(): wheel_delta = event.angleDelta() wheel_button_mask = 0 if wheel_delta.y() > 0: wheel_button_mask |= self.button_mask_map.vnc.WheelUp elif wheel_delta.y() < 0: wheel_button_mask |= self.button_mask_map.vnc.WheelDown if wheel_delta.x() > 0: wheel_button_mask |= self.button_mask_map.vnc.WheelLeft elif wheel_delta.x() < 0: wheel_button_mask |= 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(translate('vnc_viewer', '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(translate('vnc_viewer', '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, b'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(translate('vnc_viewer', '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.initFrom(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.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(translate('vnc_viewer', '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.screenshot_button_menu.addAction(translate('vnc_viewer', 'Open screenshots folder'), self._SH_ScreenshotsFolderActionTriggered) 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): settings = BlinkSettings() QDesktopServices.openUrl(QUrl.fromLocalFile(settings.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.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)