from PyQt5.QtCore import Qt, QCoreApplication, QLineF, QPointF, QRectF, QSize, QTimer, pyqtSignal, QT_TRANSLATE_NOOP from PyQt5.QtGui import QBrush, QColor, QLinearGradient, QIcon, QPainter, QPainterPath, QPalette, QPen, QPixmap, QPolygonF from PyQt5.QtWidgets import QAction, QCommonStyle, QMenu, QPushButton, QStyle, QStyleOptionToolButton, QStylePainter, QToolButton from blink.resources import Resources from blink.widgets.color import ColorScheme, ColorUtils, ColorHelperMixin __all__ = ['ToolButton', 'ConferenceButton', 'StreamButton', 'SegmentButton', 'SingleSegment', 'LeftSegment', 'MiddleSegment', 'RightSegment', 'RecordButton', 'SwitchViewButton', 'StateButton', 'AccountState'] translate = QCoreApplication.translate class ToolButton(QToolButton): """A custom QToolButton that doesn't show a menu indicator arrow""" def paintEvent(self, event): painter = QStylePainter(self) option = QStyleOptionToolButton() self.initStyleOption(option) option.features &= ~QStyleOptionToolButton.HasMenu painter.drawComplexControl(QStyle.CC_ToolButton, option) class ConferenceButton(ToolButton): makeConference = pyqtSignal() breakConference = pyqtSignal() def __init__(self, parent=None): super(ConferenceButton, self).__init__(parent) self.make_conference_action = QAction(translate('conference_button', 'Conference all single sessions'), self, triggered=self.makeConference.emit) self.break_conference_action = QAction(translate('conference_button', 'Break selected conference'), self, triggered=self.breakConference.emit) self.toggled.connect(self._SH_Toggled) self.addAction(self.make_conference_action) def _SH_Toggled(self, checked): if checked: self.removeAction(self.make_conference_action) self.addAction(self.break_conference_action) else: self.removeAction(self.break_conference_action) self.addAction(self.make_conference_action) class StreamButton(QToolButton): hidden = pyqtSignal() shown = pyqtSignal() def __init__(self, parent=None): super(StreamButton, self).__init__(parent) self.default_icon = QIcon() self.alternate_icon = QIcon() self.clicked.connect(self._clicked) def _clicked(self): super(StreamButton, self).setIcon(self.default_icon) def _get_accepted(self): return not self.isChecked() def _set_accepted(self, accepted): super(StreamButton, self).setIcon(self.alternate_icon) self.setChecked(not accepted) accepted = property(_get_accepted, _set_accepted) del _get_accepted, _set_accepted def _get_active(self): return self.isEnabled() def _set_active(self, active): self.setEnabled(bool(active)) active = property(_get_active, _set_active) del _get_active, _set_active @property def in_use(self): return self.isVisibleTo(self.parent()) def setVisible(self, visible): super(StreamButton, self).setVisible(visible) signal = self.shown if visible else self.hidden signal.emit() def setIcon(self, icon): self.default_icon = icon self.alternate_icon = QIcon(icon) normal_sizes = icon.availableSizes(QIcon.Normal, QIcon.On) selected_sizes = icon.availableSizes(QIcon.Selected, QIcon.On) selected_additional_sizes = [size for size in selected_sizes if size not in normal_sizes] for size in normal_sizes + selected_additional_sizes: pixmap = icon.pixmap(size, QIcon.Selected, QIcon.On) self.alternate_icon.addPixmap(pixmap, QIcon.Normal, QIcon.On) disabled_sizes = icon.availableSizes(QIcon.Disabled, QIcon.On) selected_additional_sizes = [size for size in selected_sizes if size not in disabled_sizes] for size in disabled_sizes + selected_additional_sizes: pixmap = icon.pixmap(size, QIcon.Selected, QIcon.On) self.alternate_icon.addPixmap(pixmap, QIcon.Disabled, QIcon.On) super(StreamButton, self).setIcon(icon) class SegmentTypeMeta(type): def __repr__(cls): return cls.__name__ class SegmentType(object, metaclass=SegmentTypeMeta): style_sheet = '' class SingleSegment(SegmentType): style_sheet = """ QToolButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #fafafa, stop:1 #bababa); border-color: #545454; border-radius: 4px; border-width: 1px; border-style: solid; } QToolButton:pressed { background: qradialgradient(cx:0.5, cy:0.5, radius:1, fx:0.5, fy:0.5, stop:0 #dddddd, stop:1 #777777); } """ class LeftSegment(SegmentType): style_sheet = """ QToolButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #fafafa, stop:1 #bababa); border-color: #545454; border-top-left-radius: 4px; border-bottom-left-radius: 4px; border-width: 1px; border-style: solid; } QToolButton:pressed { background: qradialgradient(cx:0.5, cy:0.5, radius:1, fx:0.5, fy:0.5, stop:0 #dddddd, stop:1 #777777); } """ class MiddleSegment(SegmentType): style_sheet = """ QToolButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #fafafa, stop:1 #bababa); border-color: #545454; border-width: 1px; border-left-width: 0px; border-style: solid; } QToolButton:pressed { background: qradialgradient(cx:0.5, cy:0.5, radius:1, fx:0.5, fy:0.5, stop:0 #dddddd, stop:1 #777777); } """ class RightSegment(SegmentType): style_sheet = """ QToolButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #fafafa, stop:1 #bababa); border-color: #545454; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-width: 1px; border-left-width: 0px; border-style: solid; } QToolButton:pressed { background: qradialgradient(cx:0.5, cy:0.5, radius:1, fx:0.5, fy:0.5, stop:0 #dddddd, stop:1 #777777); } """ class SegmentButton(QToolButton): SingleSegment = SingleSegment LeftSegment = LeftSegment MiddleSegment = MiddleSegment RightSegment = RightSegment hidden = pyqtSignal() shown = pyqtSignal() def __init__(self, parent=None): super(SegmentButton, self).__init__(parent) self.type = SingleSegment def _get_type(self): return self.__dict__['type'] def _set_type(self, value): if not issubclass(value, SegmentType): raise ValueError("Invalid type: %r" % value) self.__dict__['type'] = value self.setStyleSheet(value.style_sheet) type = property(_get_type, _set_type) del _get_type, _set_type def setVisible(self, visible): super(SegmentButton, self).setVisible(visible) signal = self.shown if visible else self.hidden signal.emit() class RecordButton(SegmentButton): def __init__(self, parent=None): super(RecordButton, self).__init__(parent) self.timer_id = None self.toggled.connect(self._SH_Toggled) self.animation_icons = [] self.animation_icon_index = 0 def _get_animation_icon_index(self): return self.__dict__['animation_icon_index'] def _set_animation_icon_index(self, index): self.__dict__['animation_icon_index'] = index self.update() animation_icon_index = property(_get_animation_icon_index, _set_animation_icon_index) del _get_animation_icon_index, _set_animation_icon_index def setIcon(self, icon): super(RecordButton, self).setIcon(icon) on_icon = QIcon(icon) off_icon = QIcon(icon) for size in off_icon.availableSizes(QIcon.Normal, QIcon.On): pixmap = off_icon.pixmap(size, QIcon.Normal, QIcon.Off) off_icon.addPixmap(pixmap, QIcon.Normal, QIcon.On) self.animation_icons = [on_icon, off_icon] def paintEvent(self, event): painter = QStylePainter(self) option = QStyleOptionToolButton() self.initStyleOption(option) option.icon = self.animation_icons[self.animation_icon_index] painter.drawComplexControl(QStyle.CC_ToolButton, option) def timerEvent(self, event): self.animation_icon_index = (self.animation_icon_index+1) % len(self.animation_icons) def _SH_Toggled(self, checked): if checked: self.timer_id = self.startTimer(1000) self.animation_icon_index = 0 else: self.killTimer(self.timer_id) self.timer_id = None class SwitchViewButton(QPushButton): ContactView = 0 SessionView = 1 viewChanged = pyqtSignal(int) button_text = {ContactView: QT_TRANSLATE_NOOP('switch_view_button', 'Switch to Calls'), SessionView: QT_TRANSLATE_NOOP('switch_view_button', 'Switch to Contacts')} button_dnd_text = {ContactView: QT_TRANSLATE_NOOP('switch_view_button', 'Drag here to add to a conference'), SessionView: QT_TRANSLATE_NOOP('switch_view_button', 'Drag here to go back to contacts')} dnd_style_sheet1 = """ QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #d3ffdc); border-color: #237523; border-radius: 4px; border-width: 2px; border-style: solid; } """ dnd_style_sheet2 = """ QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #c2ffce); border-color: #dc3169; border-radius: 4px; border-width: 2px; border-style: solid; } """ def __init__(self, parent=None): super(SwitchViewButton, self).__init__(parent) self.setAcceptDrops(True) self.__dict__['dnd_active'] = False self.view = self.ContactView self.original_height = 20 # used to restore button size after DND self.dnd_timer = QTimer(self) self.dnd_timer.setInterval(100) self.dnd_timer.timeout.connect(self._update_dnd) self.dnd_timer.phase = 0 self.clicked.connect(self._change_view) def _get_view(self): return self.__dict__['view'] def _set_view(self, value): if self.__dict__.get('view', None) == value: return if value not in (self.ContactView, self.SessionView): raise ValueError("invalid view value: %r" % value) self.__dict__['view'] = value if self.dnd_active: text = self.button_dnd_text[value] else: text = self.button_text[value] self.setText(translate('switch_view_button', text)) self.viewChanged.emit(value) view = property(_get_view, _set_view) del _get_view, _set_view def _get_dnd_active(self): return self.__dict__['dnd_active'] def _set_dnd_active(self, value): if self.__dict__.get('dnd_active', None) == value: return self.__dict__['dnd_active'] = value if value is True: self.dnd_timer.phase = 0 self.original_height = self.height() self.setStyleSheet(self.dnd_style_sheet1) self.setText(translate('switch_view_button', self.button_dnd_text[self.view])) self.setFixedHeight(40) else: self.setStyleSheet('') self.setText(translate('switch_view_button', self.button_text[self.view])) self.setFixedHeight(self.original_height) dnd_active = property(_get_dnd_active, _set_dnd_active) del _get_dnd_active, _set_dnd_active def _change_view(self): self.view = self.ContactView if self.view is self.SessionView else self.SessionView def _update_dnd(self): self.dnd_timer.phase += 1 if self.dnd_timer.phase == 11: self.dnd_timer.stop() self.click() self.setStyleSheet(self.dnd_style_sheet1) else: style_sheet = (self.dnd_style_sheet1, self.dnd_style_sheet2)[self.dnd_timer.phase % 2] self.setStyleSheet(style_sheet) def dragEnterEvent(self, event): if self.dnd_active: event.accept() self._update_dnd() self.dnd_timer.start() else: event.ignore() def dragLeaveEvent(self, event): if self.dnd_active: self.dnd_timer.stop() self.dnd_timer.phase = 0 self.setStyleSheet(self.dnd_style_sheet1) super(SwitchViewButton, self).dragLeaveEvent(event) def dropEvent(self, event): if self.dnd_active: self.dnd_timer.stop() self.dnd_timer.phase = 0 self.setStyleSheet(self.dnd_style_sheet1) event.ignore() class StateButtonStyle(QCommonStyle, ColorHelperMixin): _pixel_metrics = {QStyle.PM_MenuButtonIndicator: 11, QStyle.PM_DefaultFrameWidth: 3, QStyle.PM_ButtonMargin: 1, QStyle.PM_ButtonShiftHorizontal: 0, QStyle.PM_ButtonShiftVertical: 0, QStyle.PM_ButtonIconSize: 32} def polish(self, widget): widget.setAttribute(Qt.WA_Hover) super(StateButtonStyle, self).polish(widget) def pixelMetric(self, metric, option=None, widget=None): return self._pixel_metrics[metric] def sizeFromContents(self, element, option, size, widget=None): if element == QStyle.CT_ToolButton: return self.toolButtonSizeFromContents(option, size, widget) else: return super(StateButtonStyle, self).sizeFromContents(element, option, size, widget) def toolButtonSizeFromContents(self, option, size, widget): # Make width >= height to avoid super-skinny buttons margin = 2 * (self._pixel_metrics[QStyle.PM_DefaultFrameWidth] + self._pixel_metrics[QStyle.PM_ButtonMargin]) if option.features & QStyleOptionToolButton.MenuButtonPopup: margin_size = QSize(margin+1, margin) menu_width = self._pixel_metrics[QStyle.PM_MenuButtonIndicator] else: margin_size = QSize(margin, margin) menu_width = 0 if size.width() - menu_width < size.height(): size.setWidth(size.height() + menu_width) return size + margin_size def drawComplexControl(self, control, option, painter, widget=None): if control == QStyle.CC_ToolButton: painter.save() self.drawToolButtonComplexControl(option, painter, widget) painter.restore() else: super(StateButtonStyle, self).drawComplexControl(control, option, painter, widget) def drawToolButtonComplexControl(self, option, painter, widget): button_color = option.palette.color(QPalette.Button) if option.state & (QStyle.State_On|QStyle.State_Sunken): self.drawToolButtonSunkenBezel(painter, QRectF(option.rect).adjusted(1, 1, -1, -1), button_color) else: enabled = bool(option.state & QStyle.State_Enabled) hoover = enabled and bool(option.state & QStyle.State_MouseOver) has_focus = enabled and bool(option.state & QStyle.State_HasFocus) self.drawToolButtonBezel(painter, QRectF(option.rect), button_color, hoover=hoover, has_focus=has_focus) if option.features & QStyleOptionToolButton.MenuButtonPopup: self.drawToolButtonMenuIndicator(option, painter, widget) self.drawToolButtonContent(option, painter, widget) def drawToolButtonBezel(self, painter, rect, color, hoover=False, has_focus=False): painter.setRenderHint(QPainter.Antialiasing, True) painter.setPen(Qt.NoPen) glow_rect = rect shadow_rect = rect.adjusted(1, 1, -1, -1) border_rect = rect.adjusted(2, 2, -2, -2) content_rect = rect.adjusted(3, 3, -3, -3) focus_color = QColor('#3aa7dd') hoover_color = QColor('#6ed6ff') shadow_color = ColorScheme.shade(self.background_bottom_color(color), ColorScheme.ShadowShade, 0.0) border_color_top = ColorScheme.shade(self.background_top_color(color), ColorScheme.LightShade, 0.0) border_color_bottom = ColorScheme.shade(self.background_bottom_color(color), ColorScheme.MidlightShade, 0.5) # glow painter.setCompositionMode(QPainter.CompositionMode_SourceOver) blend = QLinearGradient(glow_rect.topLeft(), glow_rect.bottomLeft()) if hoover: blend.setColorAt(0.0, self.color_with_alpha(hoover_color, 0x45)) blend.setColorAt(0.9, self.color_with_alpha(hoover_color, 0x45)) blend.setColorAt(1.0, self.color_with_alpha(ColorUtils.mix(hoover_color, shadow_color, 0.4), 0x55)) elif has_focus: blend.setColorAt(0.0, self.color_with_alpha(focus_color, 0x45)) blend.setColorAt(0.9, self.color_with_alpha(focus_color, 0x45)) blend.setColorAt(1.0, self.color_with_alpha(ColorUtils.mix(focus_color, shadow_color, 0.4), 0x55)) else: blend.setColorAt(0.0, Qt.transparent) # or @0.5 blend.setColorAt(0.9, self.color_with_alpha(shadow_color, 0x10)) # blend.setColorAt(1-4.0/glow_rect.height(), self.color_with_alpha(shadow_color, 0x10)) # this is for exactly 4 pixels from bottom blend.setColorAt(1.0, self.color_with_alpha(shadow_color, 0x30)) # 0x25, 0x30 or 0x35 painter.setBrush(blend) painter.drawRoundedRect(glow_rect, 5, 5) # 5 or 6 # shadow painter.setCompositionMode(QPainter.CompositionMode_SourceOver) if hoover: painter.setBrush(hoover_color) elif has_focus: painter.setBrush(focus_color) else: blend = QLinearGradient(shadow_rect.topLeft(), shadow_rect.bottomLeft()) blend.setColorAt(0.00, self.color_with_alpha(shadow_color, 0x10)) blend.setColorAt(1.00, self.color_with_alpha(shadow_color, 0x80)) painter.setBrush(blend) painter.drawRoundedRect(shadow_rect, 4, 4) # 4 or 5 # border painter.setCompositionMode(QPainter.CompositionMode_Source) blend = QLinearGradient(border_rect.topLeft(), border_rect.bottomLeft()) blend.setColorAt(0.0, border_color_top) blend.setColorAt(1.0, border_color_bottom) painter.setBrush(blend) painter.drawRoundedRect(border_rect, 4, 4) # content painter.setCompositionMode(QPainter.CompositionMode_Source) grad = QLinearGradient(content_rect.topLeft(), content_rect.bottomLeft()) grad.setColorAt(0.0, self.background_top_color(color)) grad.setColorAt(1.0, self.background_bottom_color(color)) painter.setBrush(QBrush(grad)) painter.drawRoundedRect(content_rect, 4, 4) def drawToolButtonSunkenBezel(self, painter, rect, color): painter.setRenderHint(QPainter.Antialiasing, True) painter.setPen(Qt.NoPen) hole_rect = rect.adjusted(1, 1, -1, -1) shadow_rect = rect.adjusted(2, 2, -2, -2) content_rect = rect.adjusted(3, 3, -3, -3) shade_color = ColorScheme.shade(self.background_bottom_color(color), ColorScheme.MidlightShade, 0.5) shadow_color = ColorScheme.shade(self.background_bottom_color(color), ColorScheme.ShadowShade, 0.0) if self.calc_shadow_color(color).value() > color.value(): content_grad = QLinearGradient(0, content_rect.top(), 0, content_rect.bottom()+content_rect.height()*0.2) content_grad.setColorAt(0.0, self.background_bottom_color(color)) content_grad.setColorAt(1.0, self.background_top_color(color)) else: content_grad = QLinearGradient(0, content_rect.top()-content_rect.height()*0.2, 0, content_rect.bottom()) content_grad.setColorAt(0.0, self.background_top_color(color)) content_grad.setColorAt(1.0, self.background_bottom_color(color)) # hole edge painter.setCompositionMode(QPainter.CompositionMode_SourceOver) blend = QLinearGradient(hole_rect.topLeft(), hole_rect.bottomLeft()) blend.setColorAt(0.0, self.color_with_alpha(shadow_color, 0x80)) blend.setColorAt(1.0, self.color_with_alpha(shadow_color, 0x20)) painter.setBrush(blend) painter.drawRoundedRect(hole_rect, 4, 4) # 4 or 5 # shadow painter.setCompositionMode(QPainter.CompositionMode_Source) painter.setBrush(content_grad) painter.drawRoundedRect(shadow_rect, 4, 4) # 5 or 6 painter.setCompositionMode(QPainter.CompositionMode_SourceOver) blend = QLinearGradient(shadow_rect.topLeft(), shadow_rect.bottomLeft()) blend.setColorAt(0.0, self.color_with_alpha(shadow_color, 0x40)) blend.setColorAt(0.1, self.color_with_alpha(shadow_color, 0x07)) blend.setColorAt(0.9, self.color_with_alpha(shadow_color, 0x07)) blend.setColorAt(1.0, shade_color) painter.setBrush(blend) painter.drawRoundedRect(shadow_rect, 4, 4) # 5 or 6 # content painter.setCompositionMode(QPainter.CompositionMode_Source) painter.setBrush(content_grad) painter.drawRoundedRect(content_rect, 4, 4) def drawToolButtonMenuIndicator(self, option, painter, widget=None): arrow_rect = self.proxy().subControlRect(QStyle.CC_ToolButton, option, QStyle.SC_ToolButtonMenu, widget) text_color = option.palette.color(QPalette.WindowText if option.state & QStyle.State_AutoRaise else QPalette.ButtonText) button_color = option.palette.color(QPalette.Button) background_color = self.background_color(button_color, 0.5) painter.save() # draw separating vertical line if option.state & (QStyle.State_On|QStyle.State_Sunken): top_offset, bottom_offset = 4, 3 else: top_offset, bottom_offset = 2, 2 if option.direction == Qt.LeftToRight: separator_line = QLineF(arrow_rect.x()-3, arrow_rect.top()+top_offset, arrow_rect.x()-3, arrow_rect.bottom()-bottom_offset) else: separator_line = QLineF(arrow_rect.right()+3, arrow_rect.top()+top_offset, arrow_rect.right()+3, arrow_rect.bottom()-bottom_offset) light_gradient = QLinearGradient(separator_line.p1(), separator_line.p2()) light_gradient.setColorAt(0.0, ColorScheme.shade(self.background_top_color(button_color), ColorScheme.LightShade, 0.0)) light_gradient.setColorAt(1.0, ColorScheme.shade(self.background_bottom_color(button_color), ColorScheme.MidlightShade, 0.5)) separator_color = ColorScheme.shade(self.background_bottom_color(button_color), ColorScheme.MidShade, 0.0) painter.setRenderHint(QPainter.Antialiasing, False) painter.setPen(QPen(light_gradient, 1)) painter.drawLine(separator_line.translated(-1, 0)) painter.drawLine(separator_line.translated(+1, 0)) painter.setPen(QPen(separator_color, 1)) painter.drawLine(separator_line) # draw arrow arrow = QPolygonF([QPointF(-3, -1.5), QPointF(0.5, 2.5), QPointF(4, -1.5)]) if option.direction == Qt.LeftToRight: arrow.translate(-2, 1) else: arrow.translate(+2, 1) pen_thickness = 1.6 painter.setRenderHint(QPainter.Antialiasing, True) painter.translate(arrow_rect.center()) painter.translate(0, +1) painter.setPen(QPen(self.calc_light_color(background_color), pen_thickness, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawPolyline(arrow) painter.translate(0, -1) painter.setPen(QPen(self.deco_color(background_color, text_color), pen_thickness, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawPolyline(arrow) painter.restore() def drawToolButtonContent(self, option, painter, widget): if option.state & QStyle.State_Enabled: pixmap = widget.pixmap(QIcon.Normal) else: pixmap = widget.pixmap(QIcon.Disabled) if not pixmap.isNull(): margin = self._pixel_metrics[QStyle.PM_DefaultFrameWidth] + self._pixel_metrics[QStyle.PM_ButtonMargin] if option.features & QStyleOptionToolButton.MenuButtonPopup and option.direction == Qt.LeftToRight: right_offset = 1 else: right_offset = 0 content_rect = QRectF(self.proxy().subControlRect(QStyle.CC_ToolButton, option, QStyle.SC_ToolButton, widget)).adjusted(margin, margin, -margin-right_offset, -margin) pixmap_rect = QRectF(pixmap.rect()) pixmap_rect.moveCenter(content_rect.center()) painter.setRenderHint(QPainter.Antialiasing, True) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) painter.drawPixmap(pixmap_rect.topLeft(), pixmap) class StateButton(QToolButton): default_color = QColor('#efedeb') def __init__(self, parent=None): super(StateButton, self).__init__(parent) self.setPopupMode(QToolButton.MenuButtonPopup) self.setToolButtonStyle(Qt.ToolButtonIconOnly) palette = self.palette() palette.setColor(QPalette.Button, self.default_color) self.setPalette(palette) self.setStyle(StateButtonStyle()) def pixmap(self, mode=QIcon.Normal, state=QIcon.Off): pixmap = self.icon().pixmap(self.iconSize(), mode, state) if pixmap.isNull(): return pixmap size = max(pixmap.width(), pixmap.height()) offset_x = int((size - pixmap.width())/2) offset_y = int((size - pixmap.height())/2) new_pixmap = QPixmap(size, size) new_pixmap.fill(Qt.transparent) path = QPainterPath() path.addRoundedRect(0, 0, size, size, 3.7, 3.7) painter = QPainter(new_pixmap) painter.setRenderHint(QPainter.Antialiasing, True) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) painter.setClipPath(path) painter.drawPixmap(offset_x, offset_y, pixmap) painter.end() return new_pixmap class PresenceState(object): def __init__(self, name, color, icon, internal=False): self.name = name self.color = color self.icon = icon self.internal = internal def __eq__(self, other): if isinstance(other, PresenceState): return self.name == other.name return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __repr__(self): return self.name @property def Internal(self): return PresenceState(self.name, self.color, self.icon, True) class AccountState(StateButton): Invisible = PresenceState(QT_TRANSLATE_NOOP('presence_state', 'Invisible'), '#efedeb', Resources.get('icons/state-invisible.svg')) Available = PresenceState(QT_TRANSLATE_NOOP('presence_state', 'Available'), '#00ff00', Resources.get('icons/state-available.svg')) Away = PresenceState(QT_TRANSLATE_NOOP('presence_state', 'Away'), '#ffff00', Resources.get('icons/state-away.svg')) Busy = PresenceState(QT_TRANSLATE_NOOP('presence_state', 'Busy'), '#ff0000', Resources.get('icons/state-busy.svg')) stateChanged = pyqtSignal() history_size = 7 def __init__(self, parent=None): super(AccountState, self).__init__(parent) menu = QMenu(self) for state in (self.Available, self.Away, self.Busy, self.Invisible): action = menu.addAction(QIcon(state.icon), translate('presence_state', state.name)) action.state = state action.note = None menu.addSeparator() menu.triggered.connect(self._SH_MenuTriggered) self.setMenu(menu) self.state = self.Invisible self.note = None def _get_history(self): return [(action.state.name, action.note) for action in self.menu().actions()[5:]] def _set_history(self, values): menu = self.menu() for action in menu.actions()[5:]: menu.removeAction(action) for state_name, note in values: try: state = getattr(self, state_name) except AttributeError: continue action = menu.addAction(QIcon(state.icon), note) action.state = state action.note = note history = property(_get_history, _set_history) del _get_history, _set_history def _SH_MenuTriggered(self, action): if hasattr(action, 'state'): self.setState(action.state, action.note) def mousePressEvent_no(self, event): if event.button() == Qt.LeftButton and self.popupMode() == QToolButton.MenuButtonPopup: option = QStyleOptionToolButton() self.initStyleOption(option) position = self.style().subControlRect(QStyle.CC_ToolButton, option, QStyle.SC_ToolButtonMenu, self).center() event = event.__class__(event.type(), position, self.mapToGlobal(position), event.button(), event.buttons(), event.modifiers()) return super(AccountState, self).mousePressEvent(event) def setState(self, state, note=None): if state == self.state and note == self.note: return self.state = state self.note = note palette = self.palette() palette.setColor(QPalette.Button, QColor(state.color)) self.setPalette(palette) if note and not state.internal: menu = self.menu() actions = menu.actions()[5:] try: action = next(action for action in actions if action.state is state and action.note == note) except StopIteration: action = QAction(QIcon(state.icon), note, menu) if len(actions) == 0: menu.addAction(action) else: if len(actions) >= self.history_size: menu.removeAction(actions[-1]) menu.insertAction(actions[0], action) action.state = state action.note = note else: if action is not actions[0]: menu.removeAction(action) menu.insertAction(actions[0], action) self.stateChanged.emit()