class PythonObject(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self, None) self._called = "" self._arg1 = None self._arg2 = None def setCalled(self, v): self._called = v def setArg1(self, v): self._arg1 = v def setArg2(self, v): self._arg2 = v def getCalled(self): return self._called def getArg1(self): return self._arg1 def getArg2(self): return self._arg2 called = QtCore.Property(str, getCalled, setCalled) arg1 = QtCore.Property(int, getArg1, setArg1) arg2 = QtCore.Property('QVariant', getArg2, setArg2)
class DataFrameModel(QtCore.QAbstractTableModel): DtypeRole = QtCore.Qt.UserRole + 1000 ValueRole = QtCore.Qt.UserRole + 1001 def __init__(self, df=pd.DataFrame(), parent=None): super(DataFrameModel, self).__init__(parent) self._dataframe = df def setDataFrame(self, dataframe): self.beginResetModel() self._dataframe = dataframe.copy() self.endResetModel() def dataFrame(self): return self._dataframe dataFrame = QtCore.Property(pd.DataFrame, fget=dataFrame, fset=setDataFrame) @QtCore.Slot(int, QtCore.Qt.Orientation, result=str) def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole): if role == QtCore.Qt.DisplayRole: if orientation == QtCore.Qt.Horizontal: return self._dataframe.columns[section] else: return str(self._dataframe.index[section]) return None def rowCount(self, parent=QtCore.QModelIndex()): if parent.isValid(): return 0 return len(self._dataframe.index) def columnCount(self, parent=QtCore.QModelIndex()): if parent.isValid(): return 0 return 3 #self._dataframe.columns.size def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid() or not (0 <= index.row() < self.rowCount() and 0 <= index.column() < self.columnCount()): return None row = self._dataframe.index[index.row()] col = self._dataframe.columns[index.column()] dt = self._dataframe[col].dtype val = self._dataframe.loc[row, col] if role == QtCore.Qt.DisplayRole: return str(val) elif role == DataFrameModel.ValueRole: return val if role == DataFrameModel.DtypeRole: return dt return None def roleNames(self): roles = { QtCore.Qt.DisplayRole: b'display', DataFrameModel.DtypeRole: b'dtype', DataFrameModel.ValueRole: b'value' } return roles
class BlueprintButton(QtWidgets.QPushButton): def __init__(self, parent=None, index=0): super(BlueprintButton, self).__init__(parent) self.setFixedHeight(17) self.index = index color1 = QtGui.QColor(255, 255, 255) color2 = QtGui.QColor(255, 0, 0) self.backColorAnim = QtCore.QPropertyAnimation(self, "backColor") self.backColorAnim.setDuration(250) self.backColorAnim.setLoopCount(4) self.backColorAnim.setStartValue(color1) self.backColorAnim.setKeyValueAt(0.5, color2) self.backColorAnim.setEndValue(color1) def getBackColor(self): return self.palette().text() def setBackColor(self, color): pal = self.palette() pal.setColor(self.foregroundRole(), color) self.setPalette(pal) backColor = QtCore.Property(QtGui.QColor, getBackColor, setBackColor)
class MaterialIcon(QtWidgets.QWidget): opacityChanged = QtCore.Signal() def __init__(self, parent, address): super(MaterialIcon, self).__init__(parent) self.icon = QtGui.QPixmap(address) self._opacity = 0.0 def opacity(self): return self._opacity def setOpacity(self, o): if o != self._opacity: self._opacity = o self.opacityChanged.emit() self.update() opacity = QtCore.Property(float, fget=opacity, fset=setOpacity, notify=opacityChanged) def paintEvent(self, event): painter = QtGui.QPainter(self) painter.setOpacity(self.opacity) mask = QtGui.QPainter(self.icon) mask.setCompositionMode(QtGui.QPainter.CompositionMode_SourceIn) mask.fillRect(self.icon.rect(), QtGui.QColor(0, 158, 227)) mask.end() painter.drawPixmap(0, 0, self.icon)
class Squircle( QtQuick.QQuickFramebufferObject ): tChanged = QtCore.Signal(float) def __init__( self, parent=None ): logging.debug(threading.get_ident()) logging.debug('Squircle.__init__') super().__init__( parent ) self.renderer = None def gett( self ): if self.renderer == None: return 0.0 else: return self.renderer.squircle.t def sett( self, value ): if self.renderer == None or self.renderer.squircle.t == value: return self.renderer.squircle.setT( value ) self.tChanged.emit(value) if self.window(): self.window().update() t = QtCore.Property(float, fget=gett, fset=sett, notify=tChanged) def createRenderer( self ): logging.debug(threading.get_ident()) logging.debug('Squircle.createRenderer') self.renderer = SquircleInFboRenderer() logging.debug(threading.get_ident()) logging.debug('set window inside Squircle.createRenderer') self.renderer.squircle.setWindow( self.window() ) return self.renderer
class CustomLineEdit(QLineEdit): mousePressed = QtCore.Property(QMouseEvent) tagSelected = QtCore.Signal(str) def __init__(self, parent): super(self.__class__, self).__init__() self.model = QStringListModel() self.setCompleter(QCompleter()) self.completer().setModel(self.model) self.completer().setCompletionMode(QCompleter.PopupCompletion) self.completer().activated.connect(self.selected) self.textEdited.connect(self.slot_text_edited) self.parent = parent self.setPlaceholderText("Type tags here") def slot_text_edited(self, text): self.completer().setCompletionMode( QCompleter.UnfilteredPopupCompletion if text == '' else QCompleter.PopupCompletion) def selected(self, txt): self.tagSelected.emit(txt) def set_list(self, qsl): self.model.setStringList(qsl) def mousePressEvent(self, e): self.completer().complete() def focusInEvent(self, e): self.completer().complete() def focusOutEvent(self, e): pass
class DotLabel(QtWidgets.QFrame): def __init__(self, name, height=20, parent=None): super(DotLabel, self).__init__(parent) self._dot_color = QtGui.QColor(0, 0, 0) self._name = name self._height = height self.setFixedHeight(height) def get_dot_color(self): return self._dot_color def set_dot_color(self, color): self._dot_color = color def paintEvent(self, e): qp = QtGui.QPainter(self) qp.setRenderHint(QtGui.QPainter.Antialiasing) rect = QtCore.QRect(self._height, 0, self.width(), self._height) qp.drawText(rect, QtCore.Qt.AlignVCenter, self._name) qp.setBrush(self.get_dot_color()) qp.setPen(QtCore.Qt.NoPen) qp.drawEllipse(QtCore.QPoint(self._height * .5, self._height * .5), self._height * .20, self._height * .20) qp.end() dotColor = QtCore.Property(QtGui.QColor, get_dot_color, set_dot_color)
class BlinkBackgroundWidget(QWidget): # Fake bg_color property to use in the animation def get_bg_color(self): return self.palette().background().color() def set_bg_color(self, color): palette = self.palette() palette.setColor(self.backgroundRole(), color) self.setPalette(palette) bg_color = QtCore.Property(QtGui.QColor, get_bg_color, set_bg_color) def change_bg_color_with_blink(self, color: QColor): self.blink_bg_color(self.background_color, color) self.background_color = color def blink_bg_color(self, start_color, end_color): # make sure we apply any possible pending color changes self.blink_animation.stop() self.set_bg_color(self.background_color) self.blink_animation.setDuration(800) self.blink_animation.setLoopCount(1) self.blink_animation.setStartValue(start_color) self.blink_animation.setEndValue(end_color) self.blink_animation.setKeyValueAt(0.33, end_color) self.blink_animation.setKeyValueAt(0.66, start_color) self.blink_animation.start() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setAutoFillBackground(True) self.background_color = self.get_bg_color() self.blink_animation = QtCore.QPropertyAnimation(self, b"bg_color")
class Ball(QtCore.QObject): def __init__(self, parent=None): super(Ball, self).__init__(parent) self.pixmap_item = QtWidgets.QGraphicsPixmapItem( QtGui.QPixmap("ball.png")) def _set_pos(self, pos): self.pixmap_item.setPos(pos) pos = QtCore.Property(QtCore.QPointF, fset=_set_pos)
class TimerLabel(qw.QLabel): def __init__(self, parent: t.Optional[qw.QWidget] = None, f: qq.WindowFlags = qq.WindowFlags()): super().__init__(parent, f) timer = qc.QTimer() timer.timeout.connect(self._on_timeout) self._timer = timer self._elapsed = qc.QTime(0, 0) self._work = False self.setText(self._elapsed.toString(_FORMAT)) def get_interval(self): return self._timer.interval() def set_interval(self, msec: int): self._timer.setInterval(msec) def is_work(self) -> bool: return self._work def set_work(self, enabled: bool): self._work = enabled if enabled: self._timer.start() else: self._timer.stop() interval = qc.Property(int, get_interval, set_interval) work = qc.Property(bool, is_work, set_work) def _on_timeout(self): self._elapsed = self._elapsed.addMSecs(self._timer.interval()) self.setText(self._elapsed.toString(_FORMAT))
class Pixmap(QtCore.QObject): def __init__(self, pix): super(Pixmap, self).__init__() self.pixmap_item = QtGui.QGraphicsPixmapItem(pix) self.pixmap_item.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) def set_pos(self, pos): self.pixmap_item.setPos(pos) def get_pos(self): return self.pixmap_item.pos() pos = QtCore.Property(QtCore.QPointF, get_pos, set_pos)
class SettingsViewModel(QtCore.QObject): todos_reference_changed = QtCore.Signal() todo_count_changed = QtCore.Signal() def __init__(self): QtCore.QObject.__init__(self) self.__todos_reference = None def get_todo_count(self): if self.__todos_reference: return len(self.__todos_reference.todos) return 0 def get_todos_reference(self): return self.__todos_reference def set_todos_reference(self, value): # Only change the view model if there is a new one (don't reload when closing the app) if value: self.__todos_reference = value self.todos_reference_changed.emit() # Connect signal in TodosViewModel self.__todos_reference.todo_count_changed.connect( lambda: self.todo_count_changed.emit()) @QtCore.Slot() def clearTodos(self): self.__todos_reference.clear_todos() todosReference = QtCore.Property(TodosViewModel, get_todos_reference, set_todos_reference, notify=todos_reference_changed) todoCount = QtCore.Property(int, get_todo_count, notify=todo_count_changed)
class RotateValue(QtCore.QObject): def __init__(self): super(RotateValue, self).__init__() @QtCore.Slot(result=int) def val(self): return 100 def setRotation(self, v): self._rotation = v def getRotation(self): return self._rotation rotation = QtCore.Property(int, getRotation, setRotation)
class LabelEditWidget(QtWidgets.QWidget): def __init__(self, parent=None, label=''): super(LabelEditWidget, self).__init__(parent) self.labelName = label self.label = None self.setupUI() def setupUI(self): mainLayout = VertBox() self.layout = QtWidgets.QHBoxLayout() self.layout.setContentsMargins(1, 1, 1, 1) if self.labelName != '': self.label = QtWidgets.QLabel(self.labelName) self.layout.addWidget(self.label) self.edit = QtWidgets.QLineEdit() self.edit.setValidator(CharNameValidator()) color1 = self.edit.palette().color(self.edit.backgroundRole()) color2 = QtGui.QColor(255, 0, 0) self.backColorAnim = QtCore.QPropertyAnimation(self, "backColor") self.backColorAnim.setDuration(250) self.backColorAnim.setLoopCount(4) self.backColorAnim.setStartValue(color1) self.backColorAnim.setKeyValueAt(0.5, color2) self.backColorAnim.setEndValue(color1) self.layout.addWidget(self.edit) mainLayout.addLayout(self.layout) self.setLayout(mainLayout) def getBackColor(self): return self.edit.palette().color(QtGui.QPalette.Background) def setBackColor(self, color): self.edit.setStyleSheet("QLineEdit { background: rgb(" + str(color.red()) + "," + str(color.green()) + "," + str(color.blue()) + "); }") backColor = QtCore.Property(QtGui.QColor, getBackColor, setBackColor)
class ColorButton(qtw.QPushButton): """Button with color and backgroundColor properties for animation""" def _color(self): return self.palette().color(qtg.QPalette.ButtonText) def _setColor(self, qcolor): palette = self.palette() palette.setColor(qtg.QPalette.ButtonText, qcolor) self.setPalette(palette) color = qtc.Property(qtg.QColor, _color, _setColor) @qtc.Property(qtg.QColor) def backgroundColor(self): return self.palette().color(qtg.QPalette.Button) @backgroundColor.setter def backgroundColor(self, qcolor): palette = self.palette() palette.setColor(qtg.QPalette.Button, qcolor) self.setPalette(palette)
class BackgroundWithColor(QtWidgets.QPushButton): ''' Simple Background Color Class with color property that can be animated. :param int height: Height :param QColor color: Background color :param bool roundedCorner: Rounded/Square corner ''' def __init__(self, parent=None, height=32, color=GRAY, roundedCorner=True): super(BackgroundWithColor, self).__init__(parent) self.border_radius = int(height * 0.5) if roundedCorner else 0 self._color = color self.setColor(self._color) def getColor(self): return self._color def setColor(self, value): self._color = value css = 'background-color: rgb({0},{1},{2});'.format(value.red(), value.green(), value.blue()) css += 'border-radius: {0}px;'.format(self.border_radius) self.setStyleSheet(css) color = QtCore.Property(QtGui.QColor, getColor, setColor)
class DataFrameModel(QtCore.QAbstractTableModel): DtypeRole = QtCore.Qt.UserRole + 1000 ValueRole = QtCore.Qt.UserRole + 1001 def __init__(self, df=pd.DataFrame(), parent=None): super(DataFrameModel, self).__init__(parent) self._dataframe = df def setDataFrame(self, dataframe): self.beginResetModel() self._dataframe = dataframe.copy() self.endResetModel() def dataFrame(self): return self._dataframe dataFrame = QtCore.Property(pd.DataFrame, fget=dataFrame, fset=setDataFrame) @QtCore.Slot(int, QtCore.Qt.Orientation, result=str) def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole): if role == QtCore.Qt.DisplayRole: if orientation == QtCore.Qt.Horizontal: return self._dataframe.columns[section] else: return str(self._dataframe.index[section]) return None def rowCount(self, parent=QtCore.QModelIndex()): if parent.isValid(): return 0 return len(self._dataframe.index) def columnCount(self, parent=QtCore.QModelIndex()): if parent.isValid(): return 0 return self._dataframe.columns.size def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid() or not (0 <= index.row() < self.rowCount() and 0 <= index.column() < self.columnCount()): return None row = self._dataframe.index[index.row()] col = self._dataframe.columns[index.column()] dt = self._dataframe[col].dtype val = self._dataframe.loc[row][col] if role == QtCore.Qt.DisplayRole: return str(val) elif role == DataFrameModel.ValueRole: return val if role == DataFrameModel.DtypeRole: return dt return None def roleNames(self): roles = { QtCore.Qt.DisplayRole: b'display', DataFrameModel.DtypeRole: b'dtype', DataFrameModel.ValueRole: b'value' } return roles def sort(self, column, order): self.layoutAboutToBeChanged.emit() if order == 0: self._dataframe = self._dataframe.reindex(index=order_by_index( self._dataframe.index, index_natsorted( eval('self._dataframe["%s"]' % (list(self._dataframe.columns)[column]))))) else: self._dataframe = self._dataframe.reindex(index=order_by_index( self._dataframe.index, reversed( index_natsorted( eval('self._dataframe["%s"]' % (list(self._dataframe.columns)[column])))))) self._dataframe.reset_index(inplace=True, drop=True) self.setDataFrame(self._dataframe) self.layoutChanged.emit()
class RamDiagram(QtWidgets.QWidget): percentChanged = QtCore.Signal(float) def __init__(self, parent=None): super().__init__(parent) self.setFixedSize(150, 150) # color constants self.dark = "#3B3A44" self.light = "#4A4953" self.color = "#75ECB5" # text constants self.module_name = "RAM" self.postfix = "average" # timer with an interval of 1 sec self.timer = QtCore.QTimer() self.timer.setInterval(2000) self.timer.timeout.connect(self.onTimeout) self.timer.start() # animation initialization self._percent = 0 self._animation = QtCore.QPropertyAnimation(self, b"percent", duration=400) self._animation.setEasingCurve(QtCore.QEasingCurve.OutExpo) self.percentChanged.connect(self.update) self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.X11BypassWindowManagerHint ) @QtCore.Slot() def onTimeout(self): start_value = self.percent end_value = virtual_memory().percent self._animation.setStartValue(start_value) self._animation.setEndValue(end_value) self._animation.setDuration(1000) self._animation.start() def get_percent(self): return self._percent def set_percent(self, p): if self._percent != p: self._percent = p self.percentChanged.emit(p) percent = QtCore.Property( float, fget=get_percent, fset=set_percent, notify=percentChanged ) def paintEvent(self, event: QtGui.QPaintEvent): painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing) # draw base basic_rect = self.rect().adjusted(5, 5, -5, -5) painter.setBrush(QtGui.QBrush(QtGui.QColor(self.dark))) painter.drawEllipse(basic_rect) # draw arc pen = QtGui.QPen(QtGui.QColor(self.light)) pen.setWidth(8) pen.setCapStyle(QtCore.Qt.RoundCap) painter.setPen(pen) arc_rect = basic_rect.adjusted(13, 13, -13, -13) painter.drawEllipse(arc_rect) # draw active arc pen.setColor(QtGui.QColor(self.color)) start_angle = 90 span_angle = self.percent_to_angle(self.percent) painter.setPen(pen) painter.drawArc(arc_rect, start_angle * 16, span_angle * 16) # draw text # draw module name painter.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.white))) font = QtGui.QFont() font.setPixelSize(30) painter.setFont(font) arc_rect.moveTop(5) painter.drawText(arc_rect, QtCore.Qt.AlignCenter, self.module_name) # draw postfix font = QtGui.QFont() font.setPixelSize(15) painter.setFont(font) arc_rect.moveTop(-19) painter.drawText( arc_rect, QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom, self.postfix ) # draw percents font = QtGui.QFont() font.setPixelSize(14) painter.setFont(font) painter.setPen(QtGui.QPen(self.color)) arc_rect.moveTop(5) painter.drawText( arc_rect, QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom, f"{self.percent:.2f} %", ) def percent_to_angle(self, percent): return -percent / 100 * 360
class Test(QtWidgets.QWidget): def __init__(self, parent=None): super(Test, self).__init__(parent) self.angle = 150 self.setBackgroundRole(QtGui.QPalette.Dark) self.setAutoFillBackground(True) self.value = 0 self.maxValue = 7000 def sizeHint(self): return QtCore.QSize(400, 400) @QtCore.Slot(int) def setDialValue(self, _val): self.value = _val self.angle = 150 + (210 * self.value) // self.maxValue self.update() @QtCore.Slot(int) def setRotationAngle(self, _angle): self.angle = _angle self.update() def paintEvent(self, event): painter = QtGui.QPainter(self) painter.setWindow(-200, -200, 400, 400) painter.setRenderHint(QtGui.QPainter.Antialiasing, True) painter.drawRect(-195, -195, 390, 390) # Test painter.drawLine(-200, 0, 200, 0) painter.drawLine(0, -200, 0, 200) radius = 90 painter.save() # painter.rotate(360 - self.angle) painter.rotate(self.angle) palette = QtGui.QPalette() needleBaseWidth = 5 needlePath = QtGui.QPainterPath() needlePath.moveTo(0, needleBaseWidth) needlePath.lineTo(radius, 0) needlePath.lineTo(0, -needleBaseWidth) needlePath.lineTo(0, needleBaseWidth) # needlePath.closeSubpath() painter.fillPath(needlePath, QtGui.QBrush(QtGui.QColor("lime"))) painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) painter.setBrush(palette.brush(QtGui.QPalette.Active, QtGui.QPalette.Light)) painter.drawPath(needlePath) # painter.setPen(QtGui.QPen(QtCore.Qt.red, 3)) # painter.drawPoint(0, 0) # painter.drawPoint(radius, 0) painter.restore() origin = QtCore.QPoint(-90, -90) width = 180 height = 180 penWidth = 20 painter.setPen(QtGui.QPen(QtGui.QColor("lime"), penWidth)) painter.drawArc(origin.x(), origin.y(), width, height, 16 * 210, -16 * (210 - 60)) painter.setPen(QtGui.QPen(QtGui.QColor("yellow"), penWidth)) painter.drawArc(origin.x(), origin.y(), width, height, 16 * 61, -16 * (60 - 20)) painter.setPen(QtGui.QPen(QtGui.QColor("red"), penWidth)) painter.drawArc(origin.x(), origin.y(), width, height, 0, 16 * 19) # Write value text font = painter.font() font.setPixelSize(24) painter.setFont(font) rect = QtCore.QRect(1, 10, 95, 25) painter.setBrush(QtCore.Qt.black) painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) painter.drawRect(1, 10, 95, 25) painter.setPen(QtGui.QPen(QtGui.QColor("lime"), 2)) painter.drawText(rect, QtCore.Qt.AlignRight, f"{self.value}") # painter.drawText() rotationAngle = QtCore.Property(int, fset=setRotationAngle)
class MaterialCheckBox(QtWidgets.QWidget): clicked = QtCore.Signal() toggled = QtCore.Signal(bool) def __init__(self, parent=None): super(MaterialCheckBox, self).__init__(parent) self._is_checked = False style = QtWidgets.QApplication.style() icon = style.standardIcon(QtWidgets.QStyle.SP_DialogApplyButton) checkedIcon = MaterialIcon(self, icon.pixmap(24, 24)) icon = style.standardIcon(QtWidgets.QStyle.SP_DialogCancelButton) uncheckedIcon = MaterialIcon(self, icon.pixmap(24, 24)) stateMachine = QtCore.QStateMachine(self) checkedState = QtCore.QState() checkedState.assignProperty(self, b"checked", True) checkedState.assignProperty(checkedIcon, b"opacity", 1.0) checkedState.assignProperty(uncheckedIcon, b"opacity", 0.0) uncheckedState = QtCore.QState() uncheckedState.assignProperty(self, b"checked", False) uncheckedState.assignProperty(checkedIcon, b"opacity", 0.0) uncheckedState.assignProperty(uncheckedIcon, b"opacity", 1.0) stateMachine.addState(checkedState) stateMachine.addState(uncheckedState) stateMachine.setInitialState(uncheckedState) duration = 2000 transition1 = checkedState.addTransition(self.clicked, uncheckedState) animation1 = QtCore.QPropertyAnimation(checkedIcon, b"opacity", self, duration=duration) transition1.addAnimation(animation1) animation2 = QtCore.QPropertyAnimation(uncheckedIcon, b"opacity", self, duration=duration) transition1.addAnimation(animation2) transition2 = uncheckedState.addTransition(self.clicked, checkedState) animation3 = QtCore.QPropertyAnimation(checkedIcon, b"opacity", self, duration=duration) transition2.addAnimation(animation3) animation4 = QtCore.QPropertyAnimation(uncheckedIcon, b"opacity", self, duration=duration) transition2.addAnimation(animation4) stateMachine.start() def sizeHint(self): return QtCore.QSize(24, 24) def isChecked(self): return self._is_checked def setChecked(self, value): if self._is_checked != value: self._is_checked = value self.toggled.emit(self._is_checked) checked = QtCore.Property(bool, fget=isChecked, fset=setChecked, notify=toggled) def mousePressEvent(self, event): self.clicked.emit() self.update() super(MaterialCheckBox, self).mousePressEvent(event)
class HistoryListModel(QtCore.QAbstractListModel): # Store role constants that are used as object keys in JS __TRACK_TITLE_ROLE = QtCore.Qt.UserRole # UserRole means custom role __ARTIST_NAME_ROLE = QtCore.Qt.UserRole + 1 __TIMESTAMP_ROLE = QtCore.Qt.UserRole + 2 __LASTFM_IS_LOVED_ROLE = QtCore.Qt.UserRole + 3 __ALBUM_IMAGE_URL_ROLE = QtCore.Qt.UserRole + 4 __HAS_LASTFM_DATA = QtCore.Qt.UserRole + 5 def __init__( self, parent=None): # parent=None because it isn't within another list QtCore.QAbstractListModel.__init__(self, parent) # Store reference to application view model self.__history_reference = None def __scrobble_album_image_changed(self, row): '''Tell Qt that the scrobble album image has changed''' # Create a QModelIndex from the row index index = self.createIndex(row, 0) # Use list model dataChanged signal to indicate that UI needs to be updated at index self.dataChanged.emit( index, index, [self.__ALBUM_IMAGE_URL_ROLE, self.__HAS_LASTFM_DATA ]) # index twice because start and end range def __scrobble_lastfm_is_loved_changed(self, row): '''Tell the Qt that the track loved status has changed''' index = self.createIndex(row, 0) self.dataChanged.emit(index, index, [self.__LASTFM_IS_LOVED_ROLE]) # --- Qt Property Getters and Setters --- def get_history_reference(self): return self.__history_reference def set_history_reference(self, new_reference): # Only change the view model reference if there is a new one (don't reload when closing the app) if new_reference: # Tell the list model that the entirety of the list will be replaced (not just change one item) self.beginResetModel() self.__history_reference: HistoryViewModel = new_reference self.endResetModel() # Tell Qt that a new row at the top of the list will be added self.__history_reference.pre_append_scrobble.connect( lambda: self.beginInsertRows(QtCore.QModelIndex(), 0, 0) ) # 0 and 0 are start and end indices # Tell Qt that a row has been added self.__history_reference.post_append_scrobble.connect( lambda: self.endInsertRows()) # Tell Qt that we are beginning and ending a full refresh of the model self.__history_reference.begin_refresh_history.connect( lambda: self.beginResetModel()) self.__history_reference.end_refresh_history.connect( lambda: self.endResetModel()) # Connect row data changed signals self.__history_reference.scrobble_album_image_changed.connect( self.__scrobble_album_image_changed) self.__history_reference.scrobble_lastfm_is_loved_changed.connect( self.__scrobble_lastfm_is_loved_changed) # --- QAbstractListModel Implementation --- def roleNames(self): '''Create a mapping of our enum ints to their JS object key names''' # Only the track title, artist name, and timestamp attributes of a scrobble will be displayed in scrobble history item views # Use binary strings because C++ requires it return { self.__TRACK_TITLE_ROLE: b'trackTitle', self.__ARTIST_NAME_ROLE: b'artistName', self.__ALBUM_IMAGE_URL_ROLE: b'albumImageUrl', self.__LASTFM_IS_LOVED_ROLE: b'lastfmIsLoved', self.__TIMESTAMP_ROLE: b'timestamp', self.__HAS_LASTFM_DATA: b'hasLastfmData' } def rowCount(self, parent=QtCore.QModelIndex()): '''Return the number of rows in the scrobble history list''' # Prevent value from being returned if the list has a parent (is inside another list) if self.__history_reference and not parent.isValid(): return len(self.__history_reference.scrobble_history) return 0 def data( self, index, role=QtCore.Qt.DisplayRole ): # DisplayRole is a default role that returns the fallback value for the data function '''Provide data about items to each delegate view in the list''' if (not self.__history_reference # Prevent checking for value if it's outside the current range of the list or not index.isValid()): return scrobble = self.__history_reference.scrobble_history[index.row()] if role == self.__TRACK_TITLE_ROLE: return scrobble.track_title elif role == self.__ARTIST_NAME_ROLE: return scrobble.artist_name elif role == self.__ALBUM_IMAGE_URL_ROLE: return scrobble.image_set.small_url if scrobble.image_set else '' elif role == self.__LASTFM_IS_LOVED_ROLE: return scrobble.lastfm_track.is_loved if scrobble.lastfm_track else False elif role == self.__TIMESTAMP_ROLE: return scrobble.timestamp.strftime('%-m/%-d/%y %-I:%M:%S %p') elif role == self.__HAS_LASTFM_DATA: return scrobble.lastfm_track is not None # Return no data if we don't have a reference to the scrobble history view model return None # --- Qt Properties --- # Allow the __history_reference to be set in the view historyReference = QtCore.Property(HistoryViewModel, get_history_reference, set_history_reference)
class OnboardingViewModel(QtCore.QObject): # Qt Property signals has_error_changed = QtCore.Signal() auth_url_changed = QtCore.Signal() current_page_index_changed = QtCore.Signal() selected_media_player_changed = QtCore.Signal() # Signals handled by QML openUrl = QtCore.Signal(str) def __init__(self) -> None: QtCore.QObject.__init__(self) self.__application_reference: ApplicationViewModel = None self.reset_state() def reset_state(self) -> None: # Keep track of errors, True if Last.fm can't get a session key (auth token wasn't authorized by user) self.__has_error = None # Keep track of which onboarding page is showing self.__current_page_index = 0 # Store media player preference, will be written to preferences table in db self.__selected_media_player = '' # Store Last.fm authorization url and auth token to share between methods self.__auth_url = None self.__auth_token = None # Store Last.fm session until we're ready to submit it to the ApplicationViewModel self.__session = None # --- Qt Property Getters and Setters --- def set_application_reference(self, new_reference: ApplicationViewModel) -> None: if not new_reference: return self.__application_reference = new_reference self.__application_reference.openOnboarding.connect(self.__handle_open) def set_has_error(self, has_error): self.__has_error = has_error self.has_error_changed.emit() def set_current_page(self, page): self.__current_page_index = page if page == 0: # Welcome page # Reset error state if you go back to the welcome page self.set_has_error(False) if page == 1: # Connecting page # Immediately fetch and open the authorization url self.openNewAuthorizationUrl() self.current_page_index_changed.emit() def set_selected_media_player(self, media_player_name): self.__selected_media_player = media_player_name self.selected_media_player_changed.emit() # --- Slots --- @QtCore.Slot() def openNewAuthorizationUrl(self) -> None: '''Get an auth token, then generate and open the Last.fm user authorization url''' self.set_has_error(False) # Save the auth token for later use getting a Last.fm session self.__auth_token = self.__application_reference.lastfm.get_auth_token( ) # Save the authorization url so that it can be displayed in the UI self.__auth_url = self.__application_reference.lastfm.generate_authorization_url( self.__auth_token) self.auth_url_changed.emit() # Open the authorization url by calling a QML signal that launches the user's web browser self.openUrl.emit(self.__auth_url) @QtCore.Slot() def handleTryAuthenticating(self) -> None: '''Try getting a Last.fm session after the user authorizes the auth token in their browser''' try: # Get and store a new session from Last.fm self.__session = self.__application_reference.lastfm.get_session( self.__auth_token) except: # The auth token wasn't authorized by the user self.__auth_url = None self.auth_url_changed.emit() self.set_has_error(True) return # Continue to choose media player page if there wasn't an error self.set_current_page(2) @QtCore.Slot() def handleFinish(self) -> None: '''Tell ApplicationViewModel to log in''' self.__application_reference.log_in_after_onboarding( self.__session, self.__selected_media_player) # --- Private Methods --- def __handle_open(self) -> None: '''Reset state when onboarding opens and call signals to update UI''' self.reset_state() self.has_error_changed.emit() self.auth_url_changed.emit() self.current_page_index_changed.emit() # --- Qt Properties --- applicationReference = QtCore.Property( type=ApplicationViewModel, fget=lambda self: self.__application_reference, fset=set_application_reference) hasError = QtCore.Property(type=bool, fget=lambda self: self.__has_error, fset=set_has_error, notify=has_error_changed) authUrl = QtCore.Property(type=str, fget=lambda self: self.__auth_url, notify=auth_url_changed) currentPageIndex = QtCore.Property( type=int, fget=lambda self: self.__current_page_index, fset=set_current_page, notify=current_page_index_changed) selectedMediaPlayer = QtCore.Property( type=str, fget=lambda self: self.__selected_media_player, fset=set_selected_media_player, notify=selected_media_player_changed)
class TodosViewModel(QtCore.QObject): is_valid_changed = QtCore.Signal() new_todo_name_changed = QtCore.Signal() todo_count_changed = QtCore.Signal() pre_insert_todo = QtCore.Signal(int) post_insert_todo = QtCore.Signal() pre_clear_todos = QtCore.Signal() post_clear_todos = QtCore.Signal() todo_is_checked_changed = QtCore.Signal(int) def __init__(self): QtCore.QObject.__init__(self) self.__new_todo_name = '' self.todos = [] def get_new_todo_name(self): return self.__new_todo_name def set_new_todo_name(self, value): self.__new_todo_name = value self.is_valid_changed.emit() self.new_todo_name_changed.emit() def get_is_valid(self): return len(self.__new_todo_name) > 0 def clear_todos(self): # Tell the ListView that we are going to clear all todos self.pre_clear_todos.emit() self.todos = [] self.todo_count_changed.emit() # Tell the ListView that we are finished clearing all todos self.post_clear_todos.emit() # A slot is a function we can use in JS in QML @QtCore.Slot() def toggleFirstTodo( self): # Camel case because it will be used as a js function # Make sure there is a first todo if len(self.todos) > 0: todo = self.todos[0] todo.is_checked = not todo.is_checked # Toggle the is_checked property self.todo_is_checked_changed.emit( 0) # 0 is the index of the changed todo @QtCore.Slot() def addTodo(self): # Only add a todo if the field validation passes if self.get_is_valid(): # Tell the ListView that we are going to add a todo self.pre_insert_todo.emit( len(self.todos )) # Index of new todo is the current length of the list # Create a Todo instance with the current value of the input field todo = Todo(self.__new_todo_name) self.todos.append(todo) self.todo_count_changed.emit() # Clear the input field self.set_new_todo_name('') # Tell the ListView that we finished adding a todo self.post_insert_todo.emit() # Provide an interface through the Qt Property to communicate with the QML components newTodoName = QtCore.Property(str, get_new_todo_name, set_new_todo_name, notify=new_todo_name_changed) isValid = QtCore.Property(bool, get_is_valid, notify=is_valid_changed)
class ApplicationViewModel(QtCore.QObject): # Qt Property changed signals is_logged_in_changed = QtCore.Signal() is_offline_changed = QtCore.Signal() # JS event signals openOnboarding = QtCore.Signal() closeOnboarding = QtCore.Signal() # Signals handled from QML isInMiniModeChanged = QtCore.Signal() showNotification = QtCore.Signal(str, str) def __init__(self) -> None: QtCore.QObject.__init__(self) # Initialize helper classes self.lastfm = LastfmApiWrapper() self.spotify_api = SpotifyApiWrapper() self.art_provider = ArtProvider(self.lastfm, self.spotify_api) self.is_logged_in = False self.is_offline = False # Store whether the app is in mini mode self.__is_in_mini_mode: bool = None # Create network request manager and expose it to all NetworkImage instances self.network_manager = QtNetwork.QNetworkAccessManager() NetworkImage.NETWORK_MANAGER = self.network_manager # Connect to SQLite db_helper.connect() def log_in_after_onboarding(self, session: LastfmSession, media_player_preference: str) -> None: '''Save new login details to db, log in, and close onboarding''' self.lastfm.log_in_with_session(session) # Save Last.fm details and app preferences to the database db_helper.save_lastfm_session_to_database(session) db_helper.save_default_preferences_to_database(media_player_preference) # Close onboarding and start app self.__set_is_logged_in(True) self.closeOnboarding.emit() def update_is_offline(self) -> None: try: requests.get('https://1.1.1.1') self.__set_is_offline(False) except requests.exceptions.ConnectionError: self.__set_is_offline(True) # --- Slots --- @QtCore.Slot() def attemptLogin(self) -> None: '''Try to log in from database and open onboarding if they don't exist''' # Try to get session key and username from database session = db_helper.get_lastfm_session() if session: # Set Last.fm wrapper session key and username from database self.lastfm.log_in_with_session(session) self.__set_is_logged_in(True) logging.info(f'Logged in as {session.username}') else: self.openOnboarding.emit() @QtCore.Slot() def toggleMiniMode(self) -> None: self.__is_in_mini_mode = not self.__is_in_mini_mode self.isInMiniModeChanged.emit() db_helper.set_preference('is_in_mini_mode', self.__is_in_mini_mode) # --- Private Methods --- def __set_is_logged_in(self, is_logged_in: bool) -> None: if is_logged_in: # Load mini moce preference from database self.__is_in_mini_mode = db_helper.get_preference('is_in_mini_mode') self.isInMiniModeChanged.emit() self.is_logged_in = is_logged_in self.is_logged_in_changed.emit() def __set_is_offline(self, is_offline: bool) -> None: self.is_offline = is_offline self.is_offline_changed.emit() # --- Qt Properties --- isInMiniMode = QtCore.Property( type=bool, fget=lambda self: self.__is_in_mini_mode, notify=isInMiniModeChanged ) isLoggedIn = QtCore.Property( type=bool, fget=lambda self: self.is_logged_in, notify=is_logged_in_changed )
class DetailsViewModel(QtCore.QObject): # Qt Property changed signals scrobble_changed = QtCore.Signal() is_player_paused_changed = QtCore.Signal() media_player_name_changed = QtCore.Signal() is_offline_changed = QtCore.Signal() def __init__(self) -> None: QtCore.QObject.__init__(self) # Store a reference to the scrobble history view model instance that provides data self.__history_reference: HistoryViewModel = None # Store a reference to the application view model self.__application_reference: ApplicationViewModel = None # --- Qt Property Getters and Setters --- def set_history_reference(self, new_reference: HistoryViewModel) -> None: if not new_reference: return self.__history_reference = new_reference # Pass through signals from history view model self.__history_reference.selected_scrobble_changed.connect( lambda: self.scrobble_changed.emit()) self.__history_reference.is_player_paused_changed.connect( lambda: self.is_player_paused_changed.emit()) self.__history_reference.media_player_name_changed.connect( lambda: self.media_player_name_changed.emit()) # Update details view immediately after connecting self.scrobble_changed.emit() self.media_player_name_changed.emit() def set_application_reference(self, new_reference: ApplicationViewModel) -> None: if not new_reference: return self.__application_reference = new_reference # Pass through signal from application view model self.__application_reference.is_offline_changed.connect( lambda: self.is_offline_changed.emit()) # --- Qt Properties --- applicationReference = QtCore.Property( type=ApplicationViewModel, fget=lambda self: self.__application_reference, fset=set_application_reference) historyReference = QtCore.Property( type=HistoryViewModel, fget=lambda self: self.__history_reference, fset=set_history_reference) scrobble = QtCore.Property( type='QVariant', fget=lambda self: (asdict(self.__history_reference.selected_scrobble) if self.__history_reference.selected_scrobble else None) if self.__history_reference else None, notify=scrobble_changed) isCurrentScrobble = QtCore.Property( type=bool, fget=lambda self: (self.__history_reference.get_selected_scrobble_index() == -1 if self.__history_reference else None), notify=scrobble_changed) isOffline = QtCore.Property( type=bool, fget=(lambda self: self.__application_reference.is_offline if self.__application_reference else None), notify=is_offline_changed) isPlayerPaused = QtCore.Property(type=bool, fget=lambda self: (self.__history_reference.is_player_paused if self.__history_reference else None), notify=is_player_paused_changed) mediaPlayerName = QtCore.Property(type=str, fget=lambda self: (self.__history_reference.mediaPlayerName if self.__history_reference else None), notify=media_player_name_changed)
class TodosListModel(QtCore.QAbstractListModel): NAME_ROLE = QtCore.Qt.UserRole # UserRole means custom role which means custom object key in JS IS_CHECKED_ROLE = QtCore.Qt.UserRole + 1 # Add one because it needs a unique enum value def __init__(self, parent=None): QtCore.QAbstractListModel.__init__(self, parent) self.__todos_reference = None def get_todo_count(self): if self.__todos_reference: return len(self.__todos_reference.todos) return 0 def get_todos_reference(self): return self.__todos_reference def todo_is_checked_changed_callback(self, row): # Create a QModelIndex from the row number # createIndex is a ListModel class method index = self.createIndex(row, 0) # Column is zero # Use the ListModel dataChanged signal to indicate that the UI needs to be updated at this index self.dataChanged.emit(index, index, [ self.IS_CHECKED_ROLE ]) # Tell the ListModel which row range and roles are being updated def set_todos_reference(self, value): # Only change the view model if there is a new one (don't reload when closing the app) if value: # Tell the list model that we are going to replace the entirety of the list (not just change one item) # beginResetModel and endResetModel are class methods from the parent self.beginResetModel() self.__todos_reference = value self.endResetModel() # Connect signals in TodosViewModel # QtCore.QModelIndex() is because we don't have a parent list for the ListModel and we have index twice because we have a start and end index for insertion self.__todos_reference.pre_insert_todo.connect( lambda index: self.beginInsertRows(QtCore.QModelIndex(), index, index)) self.__todos_reference.post_insert_todo.connect( lambda: self.endInsertRows()) # QtCore.QModelIndex() is because we don't have a parent list for the ListModel self.__todos_reference.pre_clear_todos.connect( lambda: self.beginRemoveRows( QtCore.QModelIndex(), 0, len(self.__todos_reference.todos) - 1) ) # Remove all todos from 0 to end self.__todos_reference.post_clear_todos.connect( lambda: self.endRemoveRows()) # Tell the ListModel which row range and roles are being updated self.__todos_reference.todo_is_checked_changed.connect( self.todo_is_checked_changed_callback) # Override the required roleNames method from the parent ListModel class # Camel cased because it's a wrapper for a C++ function def roleNames(self): # Create a mapping of our enum ints to their JS object key names # In QML we can use them as model.name or model.isChecked where model is the ListModel return {self.NAME_ROLE: b'name', self.IS_CHECKED_ROLE: b'isChecked'} # Override the required rowCount method from the parent ListModel class def rowCount(self, parent=QtCore.QModelIndex()): if self.__todos_reference: # Prevents value from being returned if the list has a parent (is inside another list) if not parent.isValid(): return len(self.__todos_reference.todos) # Override the required data method from the parent ListModel class def data( self, index, role=QtCore.Qt.DisplayRole ): # DisplayRole is a default role (object key in QML) that returns the fallback value for the data function # Make sure the view model is connected (don't load in data before the UI loads) if self.__todos_reference: # Make sure the index is within the range of the row count (in the list) if index.isValid(): # Get the data at the index for the ListModel to display todo = self.__todos_reference.todos[index.row( )] # index is an object with row and column methods # Return the type of data that was requested based on the role enum values if role == self.NAME_ROLE: return todo.name elif role == self.IS_CHECKED_ROLE: return todo.is_checked # This is for DisplayRole or if the todos reference doesn't exist return None # Override the required setData method from the parent ListModel class def setData(self, index, value, role): # Make sure there is a reference to the TodosViewModel # Only allow changes to the checked value, we can't rename todos from within the todo item UI if self.__todos_reference and role == self.IS_CHECKED_ROLE: todo = self.__todos_reference.todos[ index.row()] # index is an object with row and column methods todo.is_checked = value # Use the ListModel dataChanged signal to indicate that the UI needs to be updated at this index # dataChanged requires two QModelIndex arguments and because we are overriding a default class method, they are already in the right format self.dataChanged.emit( index, index, [self.IS_CHECKED_ROLE] ) # Tell the ListModel which row range and roles are being updated return True return False # A slot is a function we can use in JS in QML @QtCore.Slot() def clearTodos(self): # Run the clear_todos function from the TodosViewModel in Python self.__todos_reference.clear_todos() todosReference = QtCore.Property(TodosViewModel, get_todos_reference, set_todos_reference)
class NetworkImage(QtQuick.QQuickItem): has_image_changed = QtCore.Signal() should_blank_on_new_source_changed = QtCore.Signal() source_changed = QtCore.Signal() NETWORK_MANAGER = None RAM_IMAGE_CACHE = {} def __init__(self, parent=None): QtQuick.QQuickItem.__init__(self, parent) self.__image = None self.__reply = None # Store Qt Scene Graph node that will display texture self.__node = None # This is set after _image is replaced with another source, and causes _image to be reconverted to a texture on the next updatePaintNode call self.__should_refresh_node_texture = False # Internal variables for Qt Properties self.__has_image = False # Trying to check _image for None causes unintended behavior because of Python to C++ translation, so a separate variable is needed to check whether an image exists self.__should_blank_on_new_source = False self.__source: str = None # Tell Qt that this component should render onscreen self.setFlag(QtQuick.QQuickItem.ItemHasContents, True) def updatePaintNode(self, old_node, data): if self.__has_image: if self.__node is None: self.__node = QtQuick.QSGNode() new_texture_node = QtQuick.QSGSimpleTextureNode() self.__node.appendChildNode(new_texture_node) texture_node = self.__node.firstChild() if self.__should_refresh_node_texture: new_texture = self.window().createTextureFromImage( self.__image) texture_node.setFiltering(QtQuick.QSGTexture.Linear) texture_node.setTexture(new_texture) self.__should_refresh_node_texture = False # Get size values for aspect ratio calculation bounding_rect = self.boundingRect() texture_size = texture_node.texture().textureSize() # Make the texture node (which is a child of the main node) fill the full component bounds texture_node.setRect(bounding_rect) if ( texture_size.width() > texture_size.height() ): # Account for Spotify images that are wider than their container # Calculate portion of texture to use to correct for container's aspect ratio slice_width = (bounding_rect.width() / bounding_rect.height()) * texture_size.height() slice_x = (texture_size.width() / 2) - (slice_width / 2) # Use calculated slice as area of texture to apply to the texture node texture_node.setSourceRect(slice_x, 0, slice_width, texture_size.height()) else: # Account for album images being applied to a wider container slice_height = (bounding_rect.height() / bounding_rect.width()) * texture_size.width() slice_y = (texture_size.height() / 2) - (slice_height / 2) texture_node.setSourceRect(0, slice_y, texture_size.width(), slice_height) return self.__node def update_image(self, image): '''Refresh the paint node with new image''' self.__image = image self.__has_image = True self.__should_refresh_node_texture = True self.has_image_changed.emit() # Request update of paint node self.update() def handle_reply(self): '''Convert recieved network data into QImage and update''' if self.__reply: if self.__reply.error() == QtNetwork.QNetworkReply.NoError: image = QtGui.QImage.fromData(self.__reply.readAll()) # Add image to cache if not in cache if self.__source not in NetworkImage.RAM_IMAGE_CACHE: NetworkImage.RAM_IMAGE_CACHE[self.__source] = image self.update_image(image) # Delete reply as it's not needed anymore self.__reply = None def set_should_blank_on_new_source(self, value): self.__should_blank_on_new_source = value self.should_blank_on_new_source_changed.emit() def set_source(self, value): # Don't do anything if source is changed to the same value if not NetworkImage.NETWORK_MANAGER or value == self.__source: return if not value: return self.__source = value if self.__should_blank_on_new_source: self.__has_image = False self.has_image_changed.emit() # Cancel previous ongoing request if exists if self.__reply: self.__reply.abort() if self.__source in NetworkImage.RAM_IMAGE_CACHE: # Immediately set image to cached version if exists self.update_image(NetworkImage.RAM_IMAGE_CACHE[self.__source]) else: # If cached image doesn't exist, tell network manager to request from source self.__reply = NetworkImage.NETWORK_MANAGER.get( QtNetwork.QNetworkRequest(self.__source)) self.__reply.finished.connect(self.handle_reply) # Qt Properties hasImage = QtCore.Property(bool, lambda self: self.__has_image, notify=has_image_changed) shouldBlankOnNewSource = QtCore.Property( bool, lambda self: self.__should_blank_on_new_source, set_should_blank_on_new_source, notify=should_blank_on_new_source_changed ) # Controls whether the view should immediately blank or keep showing cached content when a new URL is set. Should be true when the view needs to swap between entirely different images. (e.g. album art view in track details) Needs additional view to cover image view like in Picture component. # Set the source of the view. QUrl doesn't allow blank string - only None/undefined. source = QtCore.Property('QUrl', lambda self: self.__source, set_source, notify=source_changed)
class SkribblWidget(FramelessWindowMixin, QtWidgets.QWidget): guess_made = QtCore.Signal(str) choice_made = QtCore.Signal(int) message_sent = QtCore.Signal(str) start_requested = QtCore.Signal(object) def __init__(self, client, parent=None): """ :param bin.app.Skribble client: the client linked to this instance :param QtWidgets.QObject parent: """ super(SkribblWidget, self).__init__(parent=parent) self.client = client self._progress = 0 self.players_wid = None self.paint_wid = None self.chat_wid = None self.choice_dial = None self.progress_animation = None self.setup_ui() self.make_connections() def get_progress(self): return self._progress def set_progress(self, value): self._progress = value self.time_progressbar.setValue(value) progress = QtCore.Property(int, get_progress, set_progress) def setup_ui(self): super(SkribblWidget, self).setup_ui() self.setWindowTitle("BETTER SKRIBBL v0.0.1") self.setObjectName("SkribblWidget") self.setProperty("elevation", "lowest") # Title self.title_lbl = QtWidgets.QLabel(self) self.title_lbl.setStyleSheet("margin: 0; padding:0;") self.title_lbl.setPixmap( QtGui.QPixmap(os.path.join(RESSOURCES_DIR, "game_header.png"))) self.lyt.addWidget(self.title_lbl) # Main widget self.main_wid = QtWidgets.QWidget(self) self.main_wid.setObjectName("main_wid") self.main_wid.setProperty("elevation", "low") self.main_wid_lyt = QtWidgets.QVBoxLayout(self.main_wid) self.main_wid_lyt.setSpacing(16) self.main_wid_lyt.setObjectName("main_wid_lyt") self.main_wid_lyt.setContentsMargins(16, 16, 16, 16) # Progress bar self.time_progress_wid = QtWidgets.QWidget(self.main_wid) self.time_progress_wid.setObjectName("time_progress_wid") self.time_progress_wid_lyt = QtWidgets.QHBoxLayout( self.time_progress_wid) self.time_progress_wid_lyt.setObjectName("time_progress_wid_lyt") self.time_progress_wid_lyt.setContentsMargins(0, 0, 0, 0) self.time_progressbar = QtWidgets.QProgressBar(self.time_progress_wid) self.time_progressbar.setObjectName("time_progressbar") self.time_progressbar.setStyleSheet("color: white;") self.time_progressbar.setFont( QtGui.QFont("Arial", 16, QtGui.QFont.Bold)) self.time_progressbar.setFixedHeight(36) self.time_progressbar.setMaximum(10000) self.time_progressbar.setAlignment(QtCore.Qt.AlignCenter) self.time_progress_wid_lyt.addWidget(self.time_progressbar) self.main_wid_lyt.addWidget(self.time_progress_wid) # Game widget self.game_wid = QtWidgets.QWidget(self.main_wid) self.game_wid.setObjectName("game_wid") self.game_wid_lyt = QtWidgets.QHBoxLayout(self.game_wid) self.game_wid_lyt.setSpacing(16) self.game_wid_lyt.setObjectName("game_wid_lyt") self.game_wid_lyt.setContentsMargins(0, 0, 0, 0) # Players widget self.players_wid_cont = QtWidgets.QWidget(self.game_wid) self.players_wid_cont.setObjectName("players_wid_container") self.players_wid_cont.setProperty("elevation", "medium") self.players_wid_cont.setLayout(QtWidgets.QVBoxLayout()) self.game_wid_lyt.addWidget(self.players_wid_cont) self.players_wid = plywid.PlayersWidget(self.players_wid_cont) self.players_wid_cont.layout().addWidget(self.players_wid) # Paint Widget self.paint_and_guess_wid = QtWidgets.QWidget(self.game_wid) self.paint_and_guess_wid.setObjectName("paint_and_guess_wid") self.paint_and_guess_wid.setProperty("elevation", "medium") self.paint_and_guess_wid_lyt = QtWidgets.QVBoxLayout( self.paint_and_guess_wid) self.paint_and_guess_wid_lyt.setObjectName("paint_and_guess_wid_lyt") self.paint_and_guess_wid_lyt.setContentsMargins(16, 16, 16, 16) self.paint_wid = pntwid.PaintWidget(self.paint_and_guess_wid) self.paint_wid.setObjectName("paint_wid") self.paint_and_guess_wid_lyt.addWidget(self.paint_wid) # Paint overlay widgets self.paint_wid_lyt = QtWidgets.QVBoxLayout(self.paint_wid.paint_view) self.paint_wid_lyt.setContentsMargins(0, 0, 0, 0) self.paint_overlay_wid = QtWidgets.QFrame(self.paint_wid) self.paint_overlay_wid.setObjectName("paint_overlay_widget") self.paint_overlay_wid.setStyleSheet( "QFrame#paint_overlay_widget{" " background-color: rgba(0, 0, 0, 180)}") self.paint_overlay_wid.setSizePolicy( QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)) self.paint_overlay_wid.hide() self.paint_wid_lyt.addWidget(self.paint_overlay_wid) self.paint_overlay_lyt = QtWidgets.QVBoxLayout(self.paint_overlay_wid) self.config_wid = configWidget.ConfigWidget(DEFAULT_CONFIG, self) self.config_wid.hide() self.paint_overlay_lyt.addWidget(self.config_wid, 0, QtCore.Qt.AlignCenter) self.player_has_found_wid = playerHasFoundWidget.PlayerHasFoundWidget( self) self.player_has_found_wid.hide() self.paint_overlay_lyt.addWidget(self.player_has_found_wid, 0, QtCore.Qt.AlignCenter) self.score_board = scoreBoard.ScoreBoard(self.paint_wid) self.score_board.hide() self.paint_overlay_lyt.addWidget(self.score_board, 0, QtCore.Qt.AlignCenter) self.guess_lyt = QtWidgets.QHBoxLayout() self.guess_lyt.setObjectName("guess_lyt") self.guess_lbl = QtWidgets.QLabel(self.paint_and_guess_wid) self.guess_lbl.setFont(QtGui.QFont("Arial", 12, QtGui.QFont.Black)) self.guess_lbl.setObjectName("guess_lbl") self.guess_lbl.setText("Make a guess !") self.guess_lyt.addWidget(self.guess_lbl) self.guess_lne = textbar.HistoryTextBar(self.paint_and_guess_wid) self.guess_lne.setObjectName("guess_lne") self.guess_lne.setFont(QtGui.QFont("SansSerif", 11)) self.guess_lne.setPlaceholderText( "Ex: Pierre qui roule n'amasse pas mousse") self.guess_lyt.addWidget(self.guess_lne) self.paint_and_guess_wid_lyt.addLayout(self.guess_lyt) self.game_wid_lyt.addWidget(self.paint_and_guess_wid) self.chat_wid = chtwid.ChatWidget(self.game_wid) self.chat_wid.setObjectName("chat_wid") self.chat_wid.setProperty("elevation", "medium") self.game_wid_lyt.addWidget(self.chat_wid) self.main_wid_lyt.addWidget(self.game_wid) self.lyt.addWidget(self.main_wid) def make_connections(self): super(SkribblWidget, self).make_connections() self.config_wid.start_btn.clicked.connect(self.request_start) self.guess_lne.text_sent.connect(self.make_guess) self.player_has_found_wid.dismiss.connect( self.dismiss_player_has_found_widget) def request_start(self): self.start_requested.emit(self.config_wid.config) def on_close(self): self.client.close() def update_players_from_game(self, game): self.players_wid.update_from_game(game) def clear_choice_dialog(self): if self.choice_dial: self.choice_dial.hide() self.choice_dial.deleteLater() self.choice_dial = None def start_config(self, config_dict): self.config_wid.config = config_dict.copy() self.config_wid.update_from_config() self.paint_overlay_wid.show() self.config_wid.show() def start_choosing(self, choices, time): # Conforming the window state self.choice_dial = chcdial.ChoiceWidget(self) rect = QtCore.QRect(0, 64, self.width(), self.height() - 64) self.choice_dial.setGeometry(rect) self.choice_dial.load_choices(choices) self.choice_dial.choice_made.connect(self.choice_made.emit) self.choice_dial.show() self.start_progress_timer(time) def wait_for_choice(self, time): # Conforming the window state self.paint_wid.lock() self.start_progress_timer(time) def start_drawing(self, word, time): # Conforming the window state self.clear_choice_dialog() self.paint_wid.paint_view.clear() self.paint_wid.unlock() self.set_current_word(word) self.start_progress_timer(time) def start_guessing(self, preview, time): # Conforming the window state self.paint_wid.lock() self.paint_wid.paint_view.clear() self.set_current_word(preview) self.start_progress_timer(time) def set_current_word(self, word): self.time_progressbar.setFormat(word) def start_game(self): self.paint_overlay_wid.hide() self.config_wid.hide() def start_round(self): self.paint_overlay_wid.hide() self.score_board.hide() def make_guess(self): self.guess_made.emit(self.guess_lne.text()) self.guess_lne.clear() def guess_was_right(self, rank): self.player_has_found_wid.set_rank(rank) self.paint_overlay_wid.show() self.player_has_found_wid.show() def dismiss_player_has_found_widget(self): self.paint_overlay_wid.hide() self.player_has_found_wid.hide() def set_player_has_found(self, player_id): self.players_wid.set_player_has_found(player_id) def set_drawing_player(self, player_id): self.players_wid.set_drawing_player(player_id) def start_progress_timer(self, time): if self.progress_animation: self.progress_animation.stop() self.progress_animation = QtCore.QPropertyAnimation(self, b"progress") self.progress_animation.setStartValue(0) self.progress_animation.setEndValue(10000) self.progress_animation.setDuration(time) self.progress_animation.start() def end_round(self, game, word): self.player_has_found_wid.hide() self.paint_overlay_wid.show() self.score_board.update_from_game(game) self.score_board.show() self.set_current_word(word) self.start_progress_timer(game.config["score_time"])
class QObjectListModel(QtCore.QAbstractListModel): """ QObjectListModel provides a more powerful, but still easy to use, alternative to using QObjectList lists as models for QML views. As a QAbstractListModel, it has the ability to automatically notify the view of specific changes to the list, such as adding or removing items. At the same time it provides QList-like convenience functions such as append, at, and removeAt for easily working with the model from Python. """ ObjectRole = QtCore.Qt.UserRole def __init__(self, keyAttrName='', parent=None): """ Constructs an object list model with the given parent. """ super(QObjectListModel, self).__init__(parent) self._objects = list() # Internal list of objects self._keyAttrName = keyAttrName self._objectByKey = {} self.roles = QtCore.QAbstractListModel.roleNames(self) self.roles[self.ObjectRole] = b"object" self.requestDeletion.connect(self.onRequestDeletion, QtCore.Qt.QueuedConnection) def roleNames(self): return self.roles def __iter__(self): """ Enables iteration over the list of objects. """ return iter(self._objects) def keys(self): return self._objectByKey.keys() def items(self): return self._objectByKey.items() def __len__(self): return self.size() def __bool__(self): return self.size() > 0 def __getitem__(self, index): """ Enables the [] operator """ return self._objects[index] def data(self, index, role): """ Returns data for the specified role, from the item with the given index. The only valid role is ObjectRole. If the view requests an invalid index or role, an invalid variant is returned. """ if index.row() < 0 or index.row() >= len(self._objects): return None return self._objects[index.row()] def rowCount(self, parent): """ Returns the number of rows in the model. This value corresponds to the number of items in the model's internal object list. """ return self.size() def objectList(self): """ Returns the object list used by the model to store data. """ return self._objects def values(self): return self._objects def setObjectList(self, objects): """ Sets the model's internal objects list to objects. The model will notify any attached views that its underlying data has changed. """ oldSize = self.size() self.beginResetModel() for obj in self._objects: self._dereferenceItem(obj) self._objects = objects for obj in self._objects: self._referenceItem(obj) self.endResetModel() self.dataChanged.emit(self.index(0), self.index(self.size() - 1), []) if self.size() != oldSize: self.countChanged.emit() # ###### # BaseModel API # ###### @property def objects(self): return self._objectByKey @QtCore.Slot(str, result=QtCore.QObject) def get(self, key): """ Raises a KeyError if key is not in the map. :param key: :return: """ return self._objectByKey[key] def add(self, obj): self.append(obj) def pop(self, key): obj = self.get(key) self.remove(obj) return obj ############ # List API # ############ def append(self, obj): """ Insert object at the end of the model. """ self.extend([obj]) def extend(self, iterable): """ Insert objects at the end of the model. """ self.beginInsertRows(QtCore.QModelIndex(), self.size(), self.size() + len(iterable) - 1) [self._referenceItem(obj) for obj in iterable] self._objects.extend(iterable) self.endInsertRows() self.countChanged.emit() def insert(self, i, toInsert): """ Inserts object(s) at index position i in the model and notifies any views. If i is 0, the object is prepended to the model. If i is size(), the object is appended to the list. Accepts both QObject and list of QObjects. """ if not isinstance(toInsert, list): toInsert = [toInsert] self.beginInsertRows(QtCore.QModelIndex(), i, i + len(toInsert) - 1) for obj in reversed(toInsert): self._referenceItem(obj) self._objects.insert(i, obj) self.endInsertRows() self.countChanged.emit() @QtCore.Slot(int, result=QtCore.QObject) def at(self, i): """ Return the object at index i. """ return self._objects[i] def replace(self, i, obj): """ Replaces the item at index position i with object and notifies any views. i must be a valid index position in the list (i.e., 0 <= i < size()). """ self._dereferenceItem(self._objects[i]) self._referenceItem(obj) self._objects[i] = obj self.dataChanged.emit(self.index(i), self.index(i), []) def move(self, fromIndex, toIndex): """ Moves the item at index position from to index position to and notifies any views. This function assumes that both from and to are at least 0 but less than size(). To avoid failure, test that both from and to are at least 0 and less than size(). """ value = toIndex if toIndex > fromIndex: value += 1 if not self.beginMoveRows(QtCore.QModelIndex(), fromIndex, fromIndex, QtCore.QModelIndex(), value): return self._objects.insert(toIndex, self._objects.pop(fromIndex)) self.endMoveRows() def removeAt(self, i, count=1): """ Removes count number of items from index position i and notifies any views. i must be a valid index position in the model (i.e., 0 <= i < size()), as must as i + count - 1. """ self.beginRemoveRows(QtCore.QModelIndex(), i, i + count - 1) for cpt in range(count): obj = self._objects.pop(i) self._dereferenceItem(obj) self.endRemoveRows() self.countChanged.emit() def remove(self, obj): """ Removes the first occurrence of the given object. Raises a ValueError if not in list. """ if not self.contains(obj): raise ValueError("QObjectListModel.remove(obj) : obj not in list") self.removeAt(self.indexOf(obj)) def takeAt(self, i): """ Removes the item at index position i (notifying any views) and returns it. i must be a valid index position in the model (i.e., 0 <= i < size()). """ self.beginRemoveRows(QtCore.QModelIndex(), i, i) obj = self._objects.pop(i) self._dereferenceItem(obj) self.endRemoveRows() self.countChanged.emit() return obj def clear(self): """ Removes all items from the model and notifies any views. """ if not self._objects: return self.beginResetModel() for obj in self._objects: self._dereferenceItem(obj) self._objects = [] self.endResetModel() self.countChanged.emit() def update(self, objects): self.extend(objects) def reset(self, objects): self.setObjectList(objects) @QtCore.Slot(QtCore.QObject, result=bool) def contains(self, obj): """ Returns true if the list contains an occurrence of object; otherwise returns false. """ return obj in self._objects @QtCore.Slot(QtCore.QObject, result=int) def indexOf(self, matchObj, fromIndex=0, positive=True): """ Returns the index position of the first occurrence of object in the model, searching forward from index position from. If positive is True, will always return a positive index. """ index = self._objects[fromIndex:].index(matchObj) + fromIndex if positive and index < 0: index += self.size() return index def lastIndexOf(self, matchObj, fromIndex=-1, positive=True): """ Returns the index position of the last occurrence of object in the list, searching backward from index position from. If from is -1 (the default), the search starts at the last item. If positive is True, will always return a positive index. """ r = list(self._objects) r.reverse() index = -r[-fromIndex - 1:].index(matchObj) + fromIndex if positive and index < 0: index += self.size() return index def size(self): """ Returns the number of items in the model. """ return len(self._objects) @QtCore.Slot(result=bool) def isEmpty(self): """ Returns true if the model contains no items; otherwise returns false. """ return len(self._objects) == 0 def _referenceItem(self, item): if not item.parent(): # Take ownership of the object if not already parented item.setParent(self) if not self._keyAttrName: return key = getattr(item, self._keyAttrName, None) if key is None: return if key in self._objectByKey: raise ValueError("Object key {}:{} is not unique".format( self._keyAttrName, key)) self._objectByKey[key] = item def _dereferenceItem(self, item): # Ask for object deletion if parented to the model if shiboken2.isValid(item) and item.parent() == self: # delay deletion until the next event loop # This avoids warnings when the QML engine tries to evaluate (but should not) # an object that has already been deleted self.requestDeletion.emit(item) if not self._keyAttrName: return key = getattr(item, self._keyAttrName, None) if key is None: return assert key in self._objectByKey del self._objectByKey[key] def onRequestDeletion(self, item): item.deleteLater() countChanged = QtCore.Signal() count = QtCore.Property(int, size, notify=countChanged) requestDeletion = QtCore.Signal(QtCore.QObject)
class CellWidget(QtWidgets.QPushButton): """ A `QPushButton` representing a single cell of chess board. CellWidget can be either a chess piece or an empty cell of the board. It can be marked with different colors. `CellWidget` by default represents an empty cell. """ designated = QtCore.Signal(bool) """ Indicates that the setter of `marked` property has been called. """ def __init__(self, parent=None): super().__init__(parent=parent) self._piece = None self._isInCheck = False self._isHighlighted = False self._isMarked = False self._justMoved = False self.setMouseTracking(True) self.setObjectName("cell_plain") self.setCheckable(False) def getPiece(self) -> Optional[chess.Piece]: return self._piece def isPiece(self) -> bool: """ Indicates the type of the cell. It is True if the cell is a chess piece and False if it is an empty cell. """ return self._piece is not None def setPiece(self, piece: Optional[chess.Piece]) -> None: """ Sets the content of the cell. Parameters ---------- piece : Optional[chess.Piece] The piece that will occupy this cell. The cell is emptied if the piece is None otherwise its object name is set to ``cell_`` + the name of the piece or ``plain``. For example the object name of an empty cell will be ``cell_plain`` and the object name of a cell occupied by a white pawn will be ``cell_white_pawn`` Cells containing a piece are checkable, whereas those empty ones are not. """ self._piece = piece if self._piece: self.setObjectName( f"cell_{chess.COLOR_NAMES[piece.color]}_{chess.PIECE_NAMES[piece.piece_type]}" ) self.setCheckable(True) else: self.setObjectName("cell_plain") self.setCheckable(False) self.style().unpolish(self) self.style().polish(self) def isPlain(self) -> bool: """ A convenience property indicating if the cell is empty or not. """ return not self._piece def toPlain(self) -> None: """ Empties the cell. """ self.setPiece(None) @staticmethod def makePiece(piece: chess.Piece) -> "CellWidget": """ A static method that creates a `CellWidget` from the given piece. Parameters ---------- piece : `chess.Piece` The piece that will occupy the cell. Note that the type of the piece cannot be NoneType as in the definition of the method `setPiece`, because by default cells are created empty. """ assert isinstance(piece, chess.Piece) w = CellWidget() w.setPiece(piece) return w def isInCheck(self) -> bool: """ Indicates if the cell contains a king in check. Warnings -------- Cells that aren't occupied by a king cannot be in check. Raises ------- `NotAKingError` It is raised when the property is set to True for a cell not being occupied by a king. """ return self._isInCheck def setInCheck(self, ck: bool) -> None: if self._piece and self._piece.piece_type == chess.KING: self._isInCheck = ck self.style().unpolish(self) self.style().polish(self) else: raise NotAKingError( "Trying to (un)check a cell that does not hold a king.") def check(self) -> None: """ A convenience method that sets the property `isInCheck` to True. """ self.setInCheck(True) def uncheck(self) -> None: """ A convenience method that sets the property `isInCheck` to False. """ self.setInCheck(False) def isHighlighted(self) -> bool: """ Indicates if the cell is highlighted. Highlighted cells are special cells that are used to indicate legal moves on the board. Highlighted cells are not checkable. """ return self._isHighlighted def setHighlighted(self, highlighted: bool) -> None: if self._isHighlighted != highlighted: self._isHighlighted = highlighted if self._isHighlighted: self.setCheckable(False) else: self.setCheckable(bool(self._piece)) self.style().unpolish(self) self.style().polish(self) def highlight(self) -> None: """ A convenience method that sets the property `highlighted` to True. """ self.setHighlighted(True) def unhighlight(self) -> None: """ A convenience method that sets the property `highlighted` to False. """ self.setHighlighted(False) def isMarked(self) -> bool: """ Indicates if a cell is marked. Marked cells can have a different stylesheet which will visually distinguish them from other cells. The `designated` signal is emitted when this property's setter is called. """ return self._isMarked def setMarked(self, marked: bool) -> None: self._isMarked = marked self.designated.emit(self._isMarked) self.style().unpolish(self) self.style().polish(self) def mark(self): """ A convenience method that sets the property `marked` to True. """ self.setMarked(True) def unmark(self): """ A convenience method that sets the property `marked` to False. """ self.setMarked(False) def justMoved(self) -> bool: """ Indicates if the piece occupying this cell was just moved from/to this cell. """ return self._justMoved def setJustMoved(self, jm: bool): self._justMoved = jm self.style().unpolish(self) self.style().polish(self) def mouseMoveEvent(self, e): e.ignore() piece = QtCore.Property(bool, isPiece, setPiece) plain = QtCore.Property(bool, isPlain) inCheck = QtCore.Property(bool, isInCheck, setInCheck) highlighted = QtCore.Property(bool, isHighlighted, setHighlighted) marked = QtCore.Property(bool, isMarked, setMarked, notify=designated) justMoved = QtCore.Property(bool, justMoved, setJustMoved)