import time from datetime import datetime import logging import numbers import Qt from Qt import QtWidgets, QtGui, QtCore from avalon.lib import HeroVersionType from openpype.style import get_objected_colors from .models import ( AssetModel, TreeModel ) from . import lib if Qt.__binding__ == "PySide": from PySide.QtGui import QStyleOptionViewItemV4 elif Qt.__binding__ == "PyQt4": from PyQt4.QtGui import QStyleOptionViewItemV4 log = logging.getLogger(__name__) class AssetDelegate(QtWidgets.QItemDelegate): bar_height = 3 def __init__(self, *args, **kwargs): super(AssetDelegate, self).__init__(*args, **kwargs) asset_view_colors = get_objected_colors()["loader"]["asset-view"] self._selected_color = ( asset_view_colors["selected"].get_qcolor() ) self._hover_color = ( asset_view_colors["hover"].get_qcolor() ) self._selected_hover_color = ( asset_view_colors["selected-hover"].get_qcolor() ) def sizeHint(self, option, index): result = super(AssetDelegate, self).sizeHint(option, index) height = result.height() result.setHeight(height + self.bar_height) return result def paint(self, painter, option, index): # Qt4 compat if Qt.__binding__ in ("PySide", "PyQt4"): option = QStyleOptionViewItemV4(option) painter.save() item_rect = QtCore.QRect(option.rect) item_rect.setHeight(option.rect.height() - self.bar_height) subset_colors = index.data(AssetModel.subsetColorsRole) subset_colors_width = 0 if subset_colors: subset_colors_width = option.rect.width() / len(subset_colors) subset_rects = [] counter = 0 for subset_c in subset_colors: new_color = None new_rect = None if subset_c: new_color = QtGui.QColor(*subset_c) new_rect = QtCore.QRect( option.rect.left() + (counter * subset_colors_width), option.rect.top() + ( option.rect.height() - self.bar_height ), subset_colors_width, self.bar_height ) subset_rects.append((new_color, new_rect)) counter += 1 # Background if option.state & QtWidgets.QStyle.State_Selected: if len(subset_colors) == 0: item_rect.setTop(item_rect.top() + (self.bar_height / 2)) if option.state & QtWidgets.QStyle.State_MouseOver: bg_color = self._selected_hover_color else: bg_color = self._selected_color else: item_rect.setTop(item_rect.top() + (self.bar_height / 2)) if option.state & QtWidgets.QStyle.State_MouseOver: bg_color = self._hover_color else: bg_color = QtGui.QColor() bg_color.setAlpha(0) # When not needed to do a rounded corners (easier and without # painter restore): # painter.fillRect( # item_rect, # QtGui.QBrush(bg_color) # ) pen = painter.pen() pen.setStyle(QtCore.Qt.NoPen) pen.setWidth(0) painter.setPen(pen) painter.setBrush(QtGui.QBrush(bg_color)) painter.drawRoundedRect(option.rect, 3, 3) if option.state & QtWidgets.QStyle.State_Selected: for color, subset_rect in subset_rects: if not color or not subset_rect: continue painter.fillRect(subset_rect, QtGui.QBrush(color)) painter.restore() painter.save() # Icon icon_index = index.model().index( index.row(), index.column(), index.parent() ) # - Default icon_rect if not icon icon_rect = QtCore.QRect( item_rect.left(), item_rect.top(), # To make sure it's same size all the time option.rect.height() - self.bar_height, option.rect.height() - self.bar_height ) icon = index.model().data(icon_index, QtCore.Qt.DecorationRole) if icon: mode = QtGui.QIcon.Normal if not (option.state & QtWidgets.QStyle.State_Enabled): mode = QtGui.QIcon.Disabled elif option.state & QtWidgets.QStyle.State_Selected: mode = QtGui.QIcon.Selected if isinstance(icon, QtGui.QPixmap): icon = QtGui.QIcon(icon) option.decorationSize = icon.size() / icon.devicePixelRatio() elif isinstance(icon, QtGui.QColor): pixmap = QtGui.QPixmap(option.decorationSize) pixmap.fill(icon) icon = QtGui.QIcon(pixmap) elif isinstance(icon, QtGui.QImage): icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon)) option.decorationSize = icon.size() / icon.devicePixelRatio() elif isinstance(icon, QtGui.QIcon): state = QtGui.QIcon.Off if option.state & QtWidgets.QStyle.State_Open: state = QtGui.QIcon.On actualSize = option.icon.actualSize( option.decorationSize, mode, state ) option.decorationSize = QtCore.QSize( min(option.decorationSize.width(), actualSize.width()), min(option.decorationSize.height(), actualSize.height()) ) state = QtGui.QIcon.Off if option.state & QtWidgets.QStyle.State_Open: state = QtGui.QIcon.On icon.paint( painter, icon_rect, QtCore.Qt.AlignLeft, mode, state ) # Text text_rect = QtCore.QRect( icon_rect.left() + icon_rect.width() + 2, item_rect.top(), item_rect.width(), item_rect.height() ) painter.drawText( text_rect, QtCore.Qt.AlignVCenter, index.data(QtCore.Qt.DisplayRole) ) painter.restore() class VersionDelegate(QtWidgets.QStyledItemDelegate): """A delegate that display version integer formatted as version string.""" version_changed = QtCore.Signal() first_run = False lock = False def __init__(self, dbcon, *args, **kwargs): self.dbcon = dbcon super(VersionDelegate, self).__init__(*args, **kwargs) def displayText(self, value, locale): if isinstance(value, HeroVersionType): return lib.format_version(value, True) assert isinstance(value, numbers.Integral), ( "Version is not integer. \"{}\" {}".format(value, str(type(value))) ) return lib.format_version(value) def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) if fg_color: if isinstance(fg_color, QtGui.QBrush): fg_color = fg_color.color() elif isinstance(fg_color, QtGui.QColor): pass else: fg_color = None if not fg_color: return super(VersionDelegate, self).paint(painter, option, index) if option.widget: style = option.widget.style() else: style = QtWidgets.QApplication.style() style.drawControl( style.CE_ItemViewItem, option, painter, option.widget ) painter.save() text = self.displayText( index.data(QtCore.Qt.DisplayRole), option.locale ) pen = painter.pen() pen.setColor(fg_color) painter.setPen(pen) text_rect = style.subElementRect(style.SE_ItemViewItemText, option) text_margin = style.proxy().pixelMetric( style.PM_FocusFrameHMargin, option, option.widget ) + 1 painter.drawText( text_rect.adjusted(text_margin, 0, - text_margin, 0), option.displayAlignment, text ) painter.restore() def createEditor(self, parent, option, index): item = index.data(TreeModel.ItemRole) if item.get("isGroup") or item.get("isMerged"): return editor = QtWidgets.QComboBox(parent) def commit_data(): if not self.first_run: self.commitData.emit(editor) # Update model data self.version_changed.emit() # Display model data editor.currentIndexChanged.connect(commit_data) self.first_run = True self.lock = False return editor def setEditorData(self, editor, index): if self.lock: # Only set editor data once per delegation return editor.clear() # Current value of the index item = index.data(TreeModel.ItemRole) value = index.data(QtCore.Qt.DisplayRole) if item["version_document"]["type"] != "hero_version": assert isinstance(value, numbers.Integral), ( "Version is not integer" ) # Add all available versions to the editor parent_id = item["version_document"]["parent"] version_docs = list(self.dbcon.find( { "type": "version", "parent": parent_id }, sort=[("name", 1)] )) hero_version_doc = self.dbcon.find_one( { "type": "hero_version", "parent": parent_id }, { "name": 1, "data.tags": 1, "version_id": 1 } ) doc_for_hero_version = None selected = None items = [] for version_doc in version_docs: version_tags = version_doc["data"].get("tags") or [] if "deleted" in version_tags: continue if ( hero_version_doc and doc_for_hero_version is None and hero_version_doc["version_id"] == version_doc["_id"] ): doc_for_hero_version = version_doc label = lib.format_version(version_doc["name"]) item = QtGui.QStandardItem(label) item.setData(version_doc, QtCore.Qt.UserRole) items.append(item) if version_doc["name"] == value: selected = item if hero_version_doc and doc_for_hero_version: version_name = doc_for_hero_version["name"] label = lib.format_version(version_name, True) if isinstance(value, HeroVersionType): index = len(version_docs) hero_version_doc["name"] = HeroVersionType(version_name) item = QtGui.QStandardItem(label) item.setData(hero_version_doc, QtCore.Qt.UserRole) items.append(item) # Reverse items so latest versions be upper items = list(reversed(items)) for item in items: editor.model().appendRow(item) index = 0 if selected: index = selected.row() # Will trigger index-change signal editor.setCurrentIndex(index) self.first_run = False self.lock = True def setModelData(self, editor, model, index): """Apply the integer version back in the model""" version = editor.itemData(editor.currentIndex()) model.setData(index, version["name"]) def pretty_date(t, now=None, strftime="%b %d %Y %H:%M"): """Parse datetime to readable timestamp Within first ten seconds: - "just now", Within first minute ago: - "%S seconds ago" Within one hour ago: - "%M minutes ago". Within one day ago: - "%H:%M hours ago" Else: "%Y-%m-%d %H:%M:%S" """ assert isinstance(t, datetime) if now is None: now = datetime.now() assert isinstance(now, datetime) diff = now - t second_diff = diff.seconds day_diff = diff.days # future (consider as just now) if day_diff < 0: return "just now" # history if day_diff == 0: if second_diff < 10: return "just now" if second_diff < 60: return str(second_diff) + " seconds ago" if second_diff < 120: return "a minute ago" if second_diff < 3600: return str(second_diff // 60) + " minutes ago" if second_diff < 86400: minutes = (second_diff % 3600) // 60 hours = second_diff // 3600 return "{0}:{1:02d} hours ago".format(hours, minutes) return t.strftime(strftime) def pretty_timestamp(t, now=None): """Parse timestamp to user readable format >>> pretty_timestamp("20170614T151122Z", now="20170614T151123Z") 'just now' >>> pretty_timestamp("20170614T151122Z", now="20170614T171222Z") '2:01 hours ago' Args: t (str): The time string to parse. now (str, optional) Returns: str: human readable "recent" date. """ if now is not None: try: now = time.strptime(now, "%Y%m%dT%H%M%SZ") now = datetime.fromtimestamp(time.mktime(now)) except ValueError as e: log.warning("Can't parse 'now' time format: {0} {1}".format(t, e)) return None if isinstance(t, float): dt = datetime.fromtimestamp(t) else: # Parse the time format as if it is `str` result from # `pyblish.lib.time()` which usually is stored in Avalon database. try: t = time.strptime(t, "%Y%m%dT%H%M%SZ") except ValueError as e: log.warning("Can't parse time format: {0} {1}".format(t, e)) return None dt = datetime.fromtimestamp(time.mktime(t)) # prettify return pretty_date(dt, now=now) class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate): """A delegate that displays a timestamp as a pretty date. This displays dates like `pretty_date`. """ def displayText(self, value, locale): if value is None: # Ignore None value return return pretty_timestamp(value)