mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
449 lines
14 KiB
Python
449 lines
14 KiB
Python
import time
|
|
from datetime import datetime
|
|
import logging
|
|
import numbers
|
|
|
|
import Qt
|
|
from Qt import QtWidgets, QtGui, QtCore
|
|
|
|
from avalon.lib import HeroVersionType
|
|
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 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
|
|
bg_color = QtGui.QColor(60, 60, 60)
|
|
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.setRgb(70, 70, 70)
|
|
else:
|
|
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
|
if option.state & QtWidgets.QStyle.State_MouseOver:
|
|
bg_color.setAlpha(100)
|
|
else:
|
|
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)
|