From 396bdfde8a05d37be283f4ba411debc500ee4328 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:16:32 +0100 Subject: [PATCH 01/14] added few new attribute definitions and their widgets --- openpype/pipeline/lib/__init__.py | 16 +- .../pipeline/lib/attribute_definitions.py | 125 +++- openpype/tools/resources/__init__.py | 45 ++ openpype/tools/resources/images/delete.png | Bin 0 -> 12343 bytes openpype/tools/resources/images/file.png | Bin 0 -> 2801 bytes openpype/tools/resources/images/files.png | Bin 0 -> 3094 bytes openpype/tools/resources/images/folder.png | Bin 0 -> 2627 bytes .../widgets/attribute_defs/files_widget.py | 645 ++++++++++++++++++ openpype/widgets/attribute_defs/widgets.py | 95 ++- 9 files changed, 919 insertions(+), 7 deletions(-) create mode 100644 openpype/tools/resources/__init__.py create mode 100644 openpype/tools/resources/images/delete.png create mode 100644 openpype/tools/resources/images/file.png create mode 100644 openpype/tools/resources/images/files.png create mode 100644 openpype/tools/resources/images/folder.png create mode 100644 openpype/widgets/attribute_defs/files_widget.py diff --git a/openpype/pipeline/lib/__init__.py b/openpype/pipeline/lib/__init__.py index 1bb65be79b..f762c4205d 100644 --- a/openpype/pipeline/lib/__init__.py +++ b/openpype/pipeline/lib/__init__.py @@ -1,18 +1,30 @@ from .attribute_definitions import ( AbtractAttrDef, + + UIDef, + UISeparatorDef, + UILabelDef, + UnknownDef, NumberDef, TextDef, EnumDef, - BoolDef + BoolDef, + FileDef, ) __all__ = ( "AbtractAttrDef", + + "UIDef", + "UISeparatorDef", + "UILabelDef", + "UnknownDef", "NumberDef", "TextDef", "EnumDef", - "BoolDef" + "BoolDef", + "FileDef", ) diff --git a/openpype/pipeline/lib/attribute_definitions.py b/openpype/pipeline/lib/attribute_definitions.py index 2b34e15bc4..111eb44429 100644 --- a/openpype/pipeline/lib/attribute_definitions.py +++ b/openpype/pipeline/lib/attribute_definitions.py @@ -38,13 +38,19 @@ class AbtractAttrDef: key(str): Under which key will be attribute value stored. label(str): Attribute label. tooltip(str): Attribute tooltip. + is_label_horizontal(bool): UI specific argument. Specify if label is + next to value input or ahead. """ + is_value_def = True - def __init__(self, key, default, label=None, tooltip=None): + def __init__( + self, key, default, label=None, tooltip=None, is_label_horizontal=None + ): self.key = key self.label = label self.tooltip = tooltip self.default = default + self.is_label_horizontal = is_label_horizontal self._id = uuid.uuid4() self.__init__class__ = AbtractAttrDef @@ -68,8 +74,39 @@ class AbtractAttrDef: pass +# ----------------------------------------- +# UI attribute definitoins won't hold value +# ----------------------------------------- + +class UIDef(AbtractAttrDef): + is_value_def = False + + def __init__(self, key=None, default=None, *args, **kwargs): + super(UIDef, self).__init__(key, default, *args, **kwargs) + + def convert_value(self, value): + return value + + +class UISeparatorDef(UIDef): + pass + + +class UILabelDef(UIDef): + def __init__(self, label): + super(UILabelDef, self).__init__(label=label) + + +# --------------------------------------- +# Attribute defintioins should hold value +# --------------------------------------- + class UnknownDef(AbtractAttrDef): - """Definition is not known because definition is not available.""" + """Definition is not known because definition is not available. + + This attribute can be used to keep existing data unchanged but does not + have known definition of type. + """ def __init__(self, key, default=None, **kwargs): kwargs["default"] = default super(UnknownDef, self).__init__(key, **kwargs) @@ -261,3 +298,87 @@ class BoolDef(AbtractAttrDef): if isinstance(value, bool): return value return self.default + + +class FileDef(AbtractAttrDef): + """File definition. + It is possible to define filters of allowed file extensions and if supports + folders. + Args: + multipath(bool): Allow multiple path. + folders(bool): Allow folder paths. + extensions(list): Allow files with extensions. Empty list will + allow all extensions and None will disable files completely. + default(str, list): Defautl value. + """ + + def __init__( + self, key, multipath=False, folders=None, extensions=None, + default=None, **kwargs + ): + if folders is None and extensions is None: + folders = True + extensions = [] + + if default is None: + if multipath: + default = [] + else: + default = "" + else: + if multipath: + if not isinstance(default, (tuple, list, set)): + raise TypeError(( + "'default' argument must be 'list', 'tuple' or 'set'" + ", not '{}'" + ).format(type(default))) + + else: + if not isinstance(default, six.string_types): + raise TypeError(( + "'default' argument must be 'str' not '{}'" + ).format(type(default))) + default = default.strip() + + # Change horizontal label + is_label_horizontal = kwargs.get("is_label_horizontal") + if is_label_horizontal is None and multipath: + kwargs["is_label_horizontal"] = False + + self.multipath = multipath + self.folders = folders + self.extensions = extensions + super(FileDef, self).__init__(key, default=default, **kwargs) + + def __eq__(self, other): + if not super(FileDef, self).__eq__(other): + return False + + return ( + self.multipath == other.multipath + and self.folders == other.folders + and self.extensions == other.extensions + ) + + def convert_value(self, value): + if isinstance(value, six.string_types): + if self.multipath: + value = [value.strip()] + else: + value = value.strip() + return value + + if isinstance(value, (tuple, list, set)): + _value = [] + for item in value: + if isinstance(item, six.string_types): + _value.append(item.strip()) + + if self.multipath: + return _value + + if not _value: + return self.default + return _value[0].strip() + + return str(value).strip() diff --git a/openpype/tools/resources/__init__.py b/openpype/tools/resources/__init__.py new file mode 100644 index 0000000000..fd5c45f901 --- /dev/null +++ b/openpype/tools/resources/__init__.py @@ -0,0 +1,45 @@ +import os + +from Qt import QtGui + + +def get_icon_path(icon_name=None, filename=None): + """Path to image in './images' folder.""" + if icon_name is None and filename is None: + return None + + if filename is None: + filename = "{}.png".format(icon_name) + + path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "images", + filename + ) + if os.path.exists(path): + return path + return None + + +def get_image(icon_name=None, filename=None): + """Load image from './images' as QImage.""" + path = get_icon_path(icon_name, filename) + if path: + return QtGui.QImage(path) + return None + + +def get_pixmap(icon_name=None, filename=None): + """Load image from './images' as QPixmap.""" + path = get_icon_path(icon_name, filename) + if path: + return QtGui.QPixmap(path) + return None + + +def get_icon(icon_name=None, filename=None): + """Load image from './images' as QICon.""" + pix = get_pixmap(icon_name, filename) + if pix: + return QtGui.QIcon(pix) + return None diff --git a/openpype/tools/resources/images/delete.png b/openpype/tools/resources/images/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..ab02768ba31460baedaaa4d965c6879e9781517e GIT binary patch literal 12343 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 literal 0 HcmV?d00001 diff --git a/openpype/tools/resources/images/file.png b/openpype/tools/resources/images/file.png new file mode 100644 index 0000000000000000000000000000000000000000..4b05a0665f3f86f3adf98c65d87f14bb446b64c5 GIT binary patch literal 2801 zcmbtVX;@QN8a_9hm&kHuGg2rwaYsWI79m3*3sGQ*AfSM)NFWz-2S`E^ATXjK2De%o z85~Ee5YbwfRw+87)Psy6gjOh{qO>wp#`Y#y)KO=C%#Zt=@0|Dj_H(|w zK5^j!7rGA}0N@fE6P*MAi4aKuA`{EskIp|10Hp~}Ni(F$;|1B8frmK2cMs2p}11WN)3^7~JM3Dv};y z6c7dqFoOa$7UZk-0;7=Suqz;B`!R=wIw*!bAuCOuh)Oj&3=Ihk3FNXO>1c#bnIlMw zp8rgl*a=xGgF!3ca7s!_0!xAdHM(35FFZV)!{u}Md^W*g>r2%Jg^{h+PqmvEvk{Hy zvvs)EfNRvK-KHW-Q)CdbSOktfGmaVY=hoHwXXz7#;MfrkFObW5k#4|q-axk_uh6wB zjX|SVX|&@ByyRjWenpWe-|JP33hnF2a`}I!7ZkjdhTb46CJOPas4vCzn*)7HsTSiT zVR}uGE*p~-V`{@xM2#^0CX!@H_sU}~eOD3)rCS>s( zO7_|bVha?xSSqeGsGeJU4#Z>X+*c5LbooX0_AXZAL_T97IF!92*x zdm(VdC)X&6@PZuSJ%?hY(nOsm2hS%kebNF68Y`2A^1?$y+5EsU4@Bbxgf5;_8m%x8 zEIyadWApgzAZ`keF9_xfcp-DRJOP*Mu}T9Q8uP@I48`Fp?Xb@p+up>#JNP1g)DJS z4o;M_6eXSyo~y=mC@+x54Gex+sYHbl2H%_>=UH$Z`@{P!x(LoI@kclaFT7MjZ|sbi z0K_QajL#BcGd_(lH8Fy8#H<&UN|q$Fxj# zLEO%*?(+hVGAiFIxS&0J^2tX>OaFK~R(IeZ2WJ)bbWm48O&9)}6diZJChrdIWNpbi z1?Ouo>uWEK)DA`L81ijc6BM`P(2yu}ONSY%(?>RTq%S-M>rzVDrDO z_NJ^qEx*<(j!Ju{a>u@as>PpGUu$fAqaa(g_l zs!lXJY=>_J-73(tgst%G>*DXT_Hh6bd zzQ^t}rtDgJWY+bwpPrv~VuwUexuLe!@_VcPC2Ej_3JK`Z{<`~f9029^tSGvqJ*gKniGBLXZN{}k}fXy zI%LF_E~yy6vJPB~x-~fRT{=`oif{s-o02G*Hc{$?zLAeUC5^hqB~$0So>)>*OAfXq zt)fI&<_u;)Dmc~l$hj2ma69MIzSy*rcErne2y&Sa`G>ogk+^aXBP{!t;?wHil|)S= zM469dQt_rWiI!!%nKeJ8_BZ>`t_0bbD&Vve_WK&y&_2n95WKw2uz|wxvQ2NXfv?iR z+QgdbbXYcfYb8nwv((qT8v_Tli$x;=p6+IPa4y7HndCf| zvaO%AaV~{=tO4=tdcV>Jntq9VKvSQV7=}qrx6Q4)rkN^HXfDMmlNkdhib-j(dyB*h zyv>vbB-muJLDMKMsJ#+mat!PvlC%SNGtG)bgSAA`lu0ejb--0j(ZVc~6oGura2JvS z)(|OEyiCl!z*$Vqgad#y5n`C>Er=3SqU2eWDI#VYpo?jVFyADxLY`)48)5(!8yThe z0cIb7#S=uZ07%*)#7t!7F7lO-)=6VPA?$>@GNH4~X{Im>1zBqacM!gaA)&$2HlTo~ zpw3LPg{d@gEzZaqE7Xc4i999rb&?op5j-2EGpW(c-KJR60d6lSjpwfG_yt$%|c1DLTE6GWytGq{;8E!h>BQ;2o^;QtU-%xizEYjwm**5JcB zAh^Q=%}*+Z8C_>2;oM$R6Qk?${Eh*4@`@XK!63eCAmqN=-JZ==1A`A9LJ8-tl5LYN zEzfw{NV!t|y>sY;gx%RkT?fqIr`fJZ`+Y^}>YnfYs=v%*CJzKRl)CH*8@#s``Y^R( zL-z2g$*OBDKf<<~p#1H=(3U}vc?0yMi+s);Pfy6LxObr9!v%%kwi!~{t{@wM!f9qA6s19ld43E41dottCe{5v*@}~PsJV>iu^RJN?+AaV9 literal 0 HcmV?d00001 diff --git a/openpype/tools/resources/images/files.png b/openpype/tools/resources/images/files.png new file mode 100644 index 0000000000000000000000000000000000000000..667d6148ab75e01919bcee7dc9e59b2ed76d1fe1 GIT binary patch literal 3094 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 literal 0 HcmV?d00001 diff --git a/openpype/tools/resources/images/folder.png b/openpype/tools/resources/images/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..9c55e699278dee8f6f94cfc33dfd1db570f6e1f1 GIT binary patch literal 2627 zcmbtVd0Z3M7M@82C2U1SLHl$XvC5LnBp8*Hu$WK;35KvG7Ft3wkQ7KlCX|5Nh+3%n z>Q)7Xf@>}Cv*kUXOBG54tRgPBU_q#gf~b`WiYSWo-2@8y(D(j$`TZty&;7o0?z!K& z_smzK&_G+-6dD9Uwm}O8VhEyuAq9e|;C121$3H>Pn4_u)iC!WM;mS2BOqoIxk24J^ zT7W~4x357flPBSNBpy#xsd@BACo1WPO2MN?bA*^s%g2?fg_$}$JTo*xo|z=~QqX;U zXx;`c2#|v7Wr!gqSxs;aJi0k97mQ8QC>=3V^hrFrL?}Y|8Xb;sm>ecX_n{%)Iz<9k zEckfH8CX2JQm@x?Q8Yb0otf^*)aVjXmY0_oim_2Pn*kUMB15g085n9}hAG5g3;|Ba zbtyXh{(|5s2C?S zX*xL`@CB~c&oC=t(uFHh8SrF@K$U{437{^I&SA0t1C_oD_=WjlQ;!^sUfvEx6|V zgc=1%&(oaV2o%KUi*%X!Md&rU4D;0BQ4hyc z2dzOfS!F)s2x6`@S0*=!#G`9<8bz8MSC|(E;RaDclb}zR>2Uu19RRcvbhwuQ zW4IgPYS4mopx1O)ZQTVyaFz;GW>iHWX!ho`%@^GTVG~dLdh%;R_KGH5-Y1jigiV;b zeuoon{f@8|DG7yryYHUwoS3aH{=?9 zHzdhl6I=V)`!7!g2iJ|Ot4mG1Y|-A~b9d3h?uQ|g_ydNIxF2LaPi38)4KK67JPvLA zw7jQwom=0ln|)W>T%tEi_vie=4idL@=PcRuAoVM&OLy3pC!Qbpv$M3&sb_m>^DEKI zI43k#bQCU{S#7vEZ^K1u*>Ba-BU_4^SMQF#w8-&%h4lr_e6Jss|6t6I%|7xGrzD_q zX8tkGX0l_3_MHEdnR7h8S=xK(Vk<)s)n3YsTRQf5dpXfLqTWDqOVJ_ z_bfy+EN!HeMKV?I?rdj4tLq2C0RQ|J%IJ^Y0BdGWy`zkqMr@UK7h z*6ys^)bAXQt$h_tthz$|ew$T66lvdIuxhPUqbKIMitwF$EYMg(y^#O?)icx>cWvk@ zG1WfjMXmJ4qxJDM7PmPr_SJR*uZT8}M}Fm2A;u5MwbYsu3oksjb9o{4`+Rb7br!9O~l47!gCdnhy(^k5Qw-=?{O*>H(<8aHjYq4NF)EWER zp`f#mC*;Uyz*%iK?<|T$46T9vqG-y-n!;jbY^ia4%-lAQXVj{EMuwA}-`L#y(x}S( z8_v7ztKl`k_?foLo^QD;vCob4vGF1c+S!{Ju@=eBP9>x}wa9BLg>QT2RonFa?tXVm z+rtTWEQ&mrn;-3nJn6pQaGmCXBTwKz8Y18jTc;&(B%JJ6f6yqljB(7X^GkEO(OeI0 zz;|(N9xXpdc`6f~Cz;^Yc-qzaYPQUNfi;#3U!gU+VTUPlXKiHMZ0b{wdDIe{tJ$rN zr@Z~n=_Zs-f^TDO;EA!ybHhHf0o-kHIuF|pUq|`y>`2eD${Wa7^+Dra$LIa${b_ko z#^rx&4F9g(*!jD$!qLzJUqdNVhl~IJ4C*J;mk&Ij-Z7QD+PYfcx72u+`iDbV7x~Fb zGV|vbC;Is=y^Q^ZKMpXTO)XpjJvH>C?-&=CAnhYtXSvS5;F5|Y9adYBhu1M7kChI+ zU$e$MZ?M&h;~rSNXZNq*Ut6q^f}CwX?4PX7r6i%Gc3;$SJsP)_(u~F_Uh#d6XUT4; zz$VYmZ@vxT3;ychafiyMk=VKy(3-FoA98b#{j|85#(QumC1e^xt9l6UK|H1**8@A@ zJ 0 + self._files_view.setVisible(files_exists) + self._empty_widget.setVisible(not files_exists) + + +class SingleFileWidget(QtWidgets.QWidget): + value_changed = QtCore.Signal() + + def __init__(self, parent): + super(SingleFileWidget, self).__init__(parent) + + self.setAcceptDrops(True) + + filepath_input = QtWidgets.QLineEdit(self) + + browse_btn = QtWidgets.QPushButton("Browse", self) + browse_btn.setVisible(False) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(filepath_input, 1) + layout.addWidget(browse_btn, 0) + + browse_btn.clicked.connect(self._on_browse_clicked) + filepath_input.textChanged.connect(self._on_text_change) + + self._in_set_value = False + + self._filepath_input = filepath_input + self._folders_allowed = False + self._exts_filter = [] + + def set_value(self, value, multivalue): + self._in_set_value = True + + if multivalue: + set_value = set(value) + if len(set_value) == 1: + value = tuple(set_value)[0] + else: + value = "< Multiselection >" + self._filepath_input.setText(value) + + self._in_set_value = False + + def current_value(self): + return self._filepath_input.text() + + def set_filters(self, folders_allowed, exts_filter): + self._folders_allowed = folders_allowed + self._exts_filter = exts_filter + + def _on_text_change(self, text): + if not self._in_set_value: + self.value_changed.emit() + + def _on_browse_clicked(self): + # TODO implement file dialog logic in '_on_browse_clicked' + print("_on_browse_clicked") + + def dragEnterEvent(self, event): + mime_data = event.mimeData() + if not mime_data.hasUrls(): + return + + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + + # TODO add folder, extensions check + if len(filepaths) == 1: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + mime_data = event.mimeData() + if mime_data.hasUrls(): + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + # TODO filter check + if len(filepaths) == 1: + self.set_value(filepaths[0], False) + event.accept() diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 1cfed08363..2eb22209db 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -1,14 +1,20 @@ +import os import uuid + +from Qt import QtWidgets, QtCore + from openpype.pipeline.lib import ( AbtractAttrDef, UnknownDef, NumberDef, TextDef, EnumDef, - BoolDef + BoolDef, + FileDef, + UISeparatorDef, + UILabelDef ) from openpype.widgets.nice_checkbox import NiceCheckbox -from Qt import QtWidgets, QtCore def create_widget_for_attr_def(attr_def, parent=None): @@ -32,12 +38,22 @@ def create_widget_for_attr_def(attr_def, parent=None): if isinstance(attr_def, UnknownDef): return UnknownAttrWidget(attr_def, parent) + if isinstance(attr_def, FileDef): + return FileAttrWidget(attr_def, parent) + + if isinstance(attr_def, UISeparatorDef): + return SeparatorAttrWidget(attr_def, parent) + + if isinstance(attr_def, UILabelDef): + return LabelAttrWidget(attr_def, parent) + raise ValueError("Unknown attribute definition \"{}\"".format( str(type(attr_def)) )) class _BaseAttrDefWidget(QtWidgets.QWidget): + # Type 'object' may not work with older PySide versions value_changed = QtCore.Signal(object, uuid.UUID) def __init__(self, attr_def, parent): @@ -68,12 +84,36 @@ class _BaseAttrDefWidget(QtWidgets.QWidget): def set_value(self, value, multivalue=False): raise NotImplementedError( - "Method 'current_value' is not implemented. {}".format( + "Method 'set_value' is not implemented. {}".format( self.__class__.__name__ ) ) +class SeparatorAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = QtWidgets.QWidget(self) + input_widget.setObjectName("Separator") + input_widget.setMinimumHeight(2) + input_widget.setMaximumHeight(2) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + +class LabelAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = QtWidgets.QLabel(self) + label = self.attr_def.label + if label: + input_widget.setText(str(label)) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + class NumberAttrWidget(_BaseAttrDefWidget): def _ui_init(self): decimals = self.attr_def.decimals @@ -83,6 +123,9 @@ class NumberAttrWidget(_BaseAttrDefWidget): else: input_widget = QtWidgets.QSpinBox(self) + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + input_widget.setMinimum(self.attr_def.minimum) input_widget.setMaximum(self.attr_def.maximum) input_widget.setValue(self.attr_def.default) @@ -136,6 +179,9 @@ class TextAttrWidget(_BaseAttrDefWidget): ): input_widget.setPlaceholderText(self.attr_def.placeholder) + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + if self.attr_def.default: if self.multiline: input_widget.setPlainText(self.attr_def.default) @@ -184,6 +230,9 @@ class BoolAttrWidget(_BaseAttrDefWidget): input_widget = NiceCheckbox(parent=self) input_widget.setChecked(self.attr_def.default) + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + input_widget.stateChanged.connect(self._on_value_change) self._input_widget = input_widget @@ -220,6 +269,9 @@ class EnumAttrWidget(_BaseAttrDefWidget): combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) input_widget.setItemDelegate(combo_delegate) + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + items = self.attr_def.items for key, label in items.items(): input_widget.addItem(label, key) @@ -281,3 +333,40 @@ class UnknownAttrWidget(_BaseAttrDefWidget): if str_value != self._value: self._value = str_value self._input_widget.setText(str_value) + + +class FileAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + self.multipath = self.attr_def.multipath + if self.multipath: + from .files_widget import MultiFilesWidget + + input_widget = MultiFilesWidget(self) + + else: + from .files_widget import SingleFileWidget + + input_widget = SingleFileWidget(self) + + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + + input_widget.set_filters( + self.attr_def.folders, self.attr_def.extensions + ) + + input_widget.value_changed.connect(self._on_value_change) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + def _on_value_change(self): + new_value = self.current_value() + self.value_changed.emit(new_value, self.attr_def.id) + + def current_value(self): + return self._input_widget.current_value() + + def set_value(self, value, multivalue=False): + self._input_widget.set_value(value, multivalue) From fbdd1d8ab5cc43c5bf00ff6122e68c5aaac5f739 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:22:08 +0100 Subject: [PATCH 02/14] moved few widgets to tools/utils and modified asset/task widgets to easily change source model --- openpype/tools/publisher/widgets/__init__.py | 4 - .../publisher/widgets/card_view_widgets.py | 8 +- .../publisher/widgets/validations_widget.py | 6 +- openpype/tools/publisher/widgets/widgets.py | 109 +++--------------- openpype/tools/publisher/window.py | 7 +- openpype/tools/utils/__init__.py | 9 ++ openpype/tools/utils/assets_widget.py | 28 ++++- openpype/tools/utils/models.py | 30 +++-- openpype/tools/utils/tasks_widget.py | 8 ++ openpype/tools/utils/widgets.py | 59 ++++++++++ 10 files changed, 146 insertions(+), 122 deletions(-) diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 9b22a6cf25..55afc349ff 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -9,8 +9,6 @@ from .border_label_widget import ( from .widgets import ( SubsetAttributesWidget, - PixmapLabel, - StopBtn, ResetBtn, ValidateBtn, @@ -44,8 +42,6 @@ __all__ = ( "SubsetAttributesWidget", "BorderedLabelWidget", - "PixmapLabel", - "StopBtn", "ResetBtn", "ValidateBtn", diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 271d06e94c..5b59cccd25 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -27,12 +27,12 @@ from Qt import QtWidgets, QtCore from openpype.widgets.nice_checkbox import NiceCheckbox +from openpype.tools.utils import BaseClickableFrame from .widgets import ( AbstractInstanceView, ContextWarningLabel, - ClickableFrame, IconValuePixmapLabel, - TransparentPixmapLabel + PublishPixmapLabel ) from ..constants import ( CONTEXT_ID, @@ -140,7 +140,7 @@ class GroupWidget(QtWidgets.QWidget): widget_idx += 1 -class CardWidget(ClickableFrame): +class CardWidget(BaseClickableFrame): """Clickable card used as bigger button.""" selected = QtCore.Signal(str, str) # Group identifier of card @@ -184,7 +184,7 @@ class ContextCardWidget(CardWidget): self._id = CONTEXT_ID self._group_identifier = "" - icon_widget = TransparentPixmapLabel(self) + icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("FamilyIconLabel") label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index 09e56d64cc..4af098413a 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -6,8 +6,8 @@ except Exception: from Qt import QtWidgets, QtCore, QtGui +from openpype.tools.utils import BaseClickableFrame from .widgets import ( - ClickableFrame, IconValuePixmapLabel ) @@ -55,7 +55,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): self._error_info = error_info self._selected = False - title_frame = ClickableFrame(self) + title_frame = BaseClickableFrame(self) title_frame.setObjectName("ValidationErrorTitleFrame") title_frame._mouse_release_callback = self._mouse_release_callback @@ -168,7 +168,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) -class ActionButton(ClickableFrame): +class ActionButton(BaseClickableFrame): """Plugin's action callback button. Action may have label or icon or both. diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 2ebcf73d4e..073e5f4bc2 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -8,14 +8,20 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome from openpype.widgets.attribute_defs import create_widget_for_attr_def +from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm -from openpype.tools.utils import PlaceholderLineEdit -from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from .models import ( AssetsHierarchyModel, TasksModel, RecursiveSortFilterProxyModel, ) +from openpype.tools.utils import ( + PlaceholderLineEdit, + IconButton, + PixmapLabel, + BaseClickableFrame +) +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from .icons import ( get_pixmap, get_icon_path @@ -26,49 +32,14 @@ from ..constants import ( ) -class PixmapLabel(QtWidgets.QLabel): - """Label resizing image to height of font.""" - def __init__(self, pixmap, parent): - super(PixmapLabel, self).__init__(parent) - self._source_pixmap = pixmap - - def set_source_pixmap(self, pixmap): - """Change source image.""" - self._source_pixmap = pixmap - self._set_resized_pix() - - def _set_resized_pix(self): +class PublishPixmapLabel(PixmapLabel): + def _get_pix_size(self): size = self.fontMetrics().height() size += size % 2 - self.setPixmap( - self._source_pixmap.scaled( - size, - size, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - ) - - def resizeEvent(self, event): - self._set_resized_pix() - super(PixmapLabel, self).resizeEvent(event) + return size, size -class TransparentPixmapLabel(QtWidgets.QLabel): - """Transparent label resizing to width and height of font.""" - def __init__(self, *args, **kwargs): - super(TransparentPixmapLabel, self).__init__(*args, **kwargs) - - def resizeEvent(self, event): - size = self.fontMetrics().height() - size += size % 2 - pix = QtGui.QPixmap(size, size) - pix.fill(QtCore.Qt.transparent) - self.setPixmap(pix) - super(TransparentPixmapLabel, self).resizeEvent(event) - - -class IconValuePixmapLabel(PixmapLabel): +class IconValuePixmapLabel(PublishPixmapLabel): """Label resizing to width and height of font. Handle icon parsing from creators/instances. Using of QAwesome module @@ -125,7 +96,7 @@ class IconValuePixmapLabel(PixmapLabel): return self._default_pixmap() -class ContextWarningLabel(PixmapLabel): +class ContextWarningLabel(PublishPixmapLabel): """Pixmap label with warning icon.""" def __init__(self, parent): pix = get_pixmap("warning") @@ -138,29 +109,6 @@ class ContextWarningLabel(PixmapLabel): self.setObjectName("FamilyIconLabel") -class IconButton(QtWidgets.QPushButton): - """PushButton with icon and size of font. - - Using font metrics height as icon size reference. - """ - - def __init__(self, *args, **kwargs): - super(IconButton, self).__init__(*args, **kwargs) - self.setObjectName("IconButton") - - def sizeHint(self): - result = super(IconButton, self).sizeHint() - icon_h = self.iconSize().height() - font_height = self.fontMetrics().height() - text_set = bool(self.text()) - if not text_set and icon_h < font_height: - new_size = result.height() - icon_h + font_height - result.setHeight(new_size) - result.setWidth(new_size) - - return result - - class PublishIconBtn(IconButton): """Button using alpha of source image to redraw with different color. @@ -314,7 +262,7 @@ class ShowPublishReportBtn(PublishIconBtn): class RemoveInstanceBtn(PublishIconBtn): """Create remove button.""" def __init__(self, parent=None): - icon_path = get_icon_path("delete") + icon_path = resources.get_icon_path("delete") super(RemoveInstanceBtn, self).__init__(icon_path, parent) self.setToolTip("Remove selected instances") @@ -359,33 +307,6 @@ class AbstractInstanceView(QtWidgets.QWidget): ).format(self.__class__.__name__)) -class ClickableFrame(QtWidgets.QFrame): - """Widget that catch left mouse click and can trigger a callback. - - Callback is defined by overriding `_mouse_release_callback`. - """ - def __init__(self, parent): - super(ClickableFrame, self).__init__(parent) - - self._mouse_pressed = False - - def _mouse_release_callback(self): - pass - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - self._mouse_pressed = True - super(ClickableFrame, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - if self._mouse_pressed: - self._mouse_pressed = False - if self.rect().contains(event.pos()): - self._mouse_release_callback() - - super(ClickableFrame, self).mouseReleaseEvent(event) - - class AssetsDialog(QtWidgets.QDialog): """Dialog to select asset for a context of instance.""" def __init__(self, controller, parent): @@ -554,7 +475,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): event.accept() -class AssetsField(ClickableFrame): +class AssetsField(BaseClickableFrame): """Field where asset name of selected instance/s is showed. Click on the field will trigger `AssetsDialog`. diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index bb58813e55..b668888281 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -4,7 +4,10 @@ from openpype import ( resources, style ) -from openpype.tools.utils import PlaceholderLineEdit +from openpype.tools.utils import ( + PlaceholderLineEdit, + PixmapLabel +) from .control import PublisherController from .widgets import ( BorderedLabelWidget, @@ -14,8 +17,6 @@ from .widgets import ( InstanceListView, CreateDialog, - PixmapLabel, - StopBtn, ResetBtn, ValidateBtn, diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index eb0cb1eef5..ac93595682 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -3,6 +3,8 @@ from .widgets import ( BaseClickableFrame, ClickableFrame, ExpandBtn, + PixmapLabel, + IconButton, ) from .error_dialog import ErrorMessageBox @@ -11,15 +13,22 @@ from .lib import ( paint_image_with_color ) +from .models import ( + RecursiveSortFilterProxyModel, +) __all__ = ( "PlaceholderLineEdit", "BaseClickableFrame", "ClickableFrame", "ExpandBtn", + "PixmapLabel", + "IconButton", "ErrorMessageBox", "WrappedCallbackItem", "paint_image_with_color", + + "RecursiveSortFilterProxyModel", ) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 1495586b04..55e34285fc 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -635,9 +635,10 @@ class AssetsWidget(QtWidgets.QWidget): selection_model = view.selectionModel() selection_model.selectionChanged.connect(self._on_selection_change) refresh_btn.clicked.connect(self.refresh) - current_asset_btn.clicked.connect(self.set_current_session_asset) + current_asset_btn.clicked.connect(self._on_current_asset_click) view.doubleClicked.connect(self.double_clicked) + self._refresh_btn = refresh_btn self._current_asset_btn = current_asset_btn self._model = model self._proxy = proxy @@ -668,11 +669,30 @@ class AssetsWidget(QtWidgets.QWidget): def stop_refresh(self): self._model.stop_refresh() + def _get_current_session_asset(self): + return self.dbcon.Session.get("AVALON_ASSET") + + def _on_current_asset_click(self): + """Trigger change of asset to current context asset. + This separation gives ability to override this method and use it + in differnt way. + """ + self.set_current_session_asset() + def set_current_session_asset(self): - asset_name = self.dbcon.Session.get("AVALON_ASSET") + asset_name = self._get_current_session_asset() if asset_name: self.select_asset_by_name(asset_name) + def set_refresh_btn_visibility(self, visible=None): + """Hide set refresh button. + Some tools may have their global refresh button or do not support + refresh at all. + """ + if visible is None: + visible = not self._refresh_btn.isVisible() + self._refresh_btn.setVisible(visible) + def set_current_asset_btn_visibility(self, visible=None): """Hide set current asset button. @@ -727,6 +747,10 @@ class AssetsWidget(QtWidgets.QWidget): def _set_loading_state(self, loading, empty): self._view.set_loading_state(loading, empty) + def _clear_selection(self): + selection_model = self._view.selectionModel() + selection_model.clearSelection() + def _select_indexes(self, indexes): valid_indexes = [ index diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index df3eee41a2..2b5b156eeb 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -199,31 +199,37 @@ class Item(dict): class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): - """Filters to the regex if any of the children matches allow parent""" - def filterAcceptsRow(self, row, parent): + """Recursive proxy model. + Item is not filtered if any children match the filter. + Use case: Filtering by string - parent won't be filtered if does not match + the filter string but first checks if any children does. + """ + def filterAcceptsRow(self, row, parent_index): regex = self.filterRegExp() if not regex.isEmpty(): - pattern = regex.pattern() model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) + source_index = model.index( + row, self.filterKeyColumn(), parent_index + ) if source_index.isValid(): + pattern = regex.pattern() + # Check current index itself - key = model.data(source_index, self.filterRole()) - if re.search(pattern, key, re.IGNORECASE): + value = model.data(source_index, self.filterRole()) + if re.search(pattern, value, re.IGNORECASE): return True - # Check children rows = model.rowCount(source_index) - for i in range(rows): - if self.filterAcceptsRow(i, source_index): + for idx in range(rows): + if self.filterAcceptsRow(idx, source_index): return True # Otherwise filter it return False - return super( - RecursiveSortFilterProxyModel, self - ).filterAcceptsRow(row, parent) + return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow( + row, parent_index + ) class ProjectModel(QtGui.QStandardItemModel): diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 6e6cd17ffd..6c7787d06a 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -255,6 +255,10 @@ class TasksWidget(QtWidgets.QWidget): # Force a task changed emit. self.task_changed.emit() + def _clear_selection(self): + selection_model = self._tasks_view.selectionModel() + selection_model.clearSelection() + def select_task_name(self, task_name): """Select a task by name. @@ -285,6 +289,10 @@ class TasksWidget(QtWidgets.QWidget): self._tasks_view.setCurrentIndex(index) break + last_selected_task_name = self.get_selected_task_name() + if last_selected_task_name: + self._last_selected_task_name = last_selected_task_name + def get_selected_task_name(self): """Return name of task at current index (selected) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index c32eae043e..ea09968e40 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -148,6 +148,65 @@ class ImageButton(QtWidgets.QPushButton): return self.iconSize() +class IconButton(QtWidgets.QPushButton): + """PushButton with icon and size of font. + + Using font metrics height as icon size reference. + """ + + def __init__(self, *args, **kwargs): + super(IconButton, self).__init__(*args, **kwargs) + self.setObjectName("IconButton") + + def sizeHint(self): + result = super(IconButton, self).sizeHint() + icon_h = self.iconSize().height() + font_height = self.fontMetrics().height() + text_set = bool(self.text()) + if not text_set and icon_h < font_height: + new_size = result.height() - icon_h + font_height + result.setHeight(new_size) + result.setWidth(new_size) + + return result + + +class PixmapLabel(QtWidgets.QLabel): + """Label resizing image to height of font.""" + def __init__(self, pixmap, parent): + super(PixmapLabel, self).__init__(parent) + self._empty_pixmap = QtGui.QPixmap(0, 0) + self._source_pixmap = pixmap + + def set_source_pixmap(self, pixmap): + """Change source image.""" + self._source_pixmap = pixmap + self._set_resized_pix() + + def _get_pix_size(self): + size = self.fontMetrics().height() + size += size % 2 + return size, size + + def _set_resized_pix(self): + if self._source_pixmap is None: + self.setPixmap(self._empty_pixmap) + return + width, height = self._get_pix_size() + self.setPixmap( + self._source_pixmap.scaled( + width, + height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + ) + + def resizeEvent(self, event): + self._set_resized_pix() + super(PixmapLabel, self).resizeEvent(event) + + class OptionalMenu(QtWidgets.QMenu): """A subclass of `QtWidgets.QMenu` to work with `OptionalAction` From fffdef5030f74cb0c0428cfb657234465d2d3a09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:22:41 +0100 Subject: [PATCH 03/14] added publisher specific asset and task widgets --- .../tools/publisher/widgets/assets_widget.py | 275 ++++++++++++++++++ .../tools/publisher/widgets/tasks_widget.py | 169 +++++++++++ 2 files changed, 444 insertions(+) create mode 100644 openpype/tools/publisher/widgets/assets_widget.py create mode 100644 openpype/tools/publisher/widgets/tasks_widget.py diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py new file mode 100644 index 0000000000..5d5372cbce --- /dev/null +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -0,0 +1,275 @@ +import collections + +import avalon.api + +from Qt import QtWidgets, QtCore, QtGui +from openpype.tools.utils import ( + PlaceholderLineEdit, + RecursiveSortFilterProxyModel +) +from openpype.tools.utils.assets_widget import ( + SingleSelectAssetsWidget, + ASSET_ID_ROLE, + ASSET_NAME_ROLE +) + + +class CreateDialogAssetsWidget(SingleSelectAssetsWidget): + current_context_required = QtCore.Signal() + + def __init__(self, controller, parent): + self._controller = controller + super(CreateDialogAssetsWidget, self).__init__(None, parent) + + self.set_refresh_btn_visibility(False) + self.set_current_asset_btn_visibility(False) + + self._current_asset_name = None + self._last_selection = None + self._enabled = None + + def _on_current_asset_click(self): + self.current_context_required.emit() + + def set_enabled(self, enabled): + if self._enabled == enabled: + return + self._enabled = enabled + if not enabled: + self._last_selection = self.get_selected_asset_id() + self._clear_selection() + elif self._last_selection is not None: + self.select_asset(self._last_selection) + + def _select_indexes(self, *args, **kwargs): + super(CreateDialogAssetsWidget, self)._select_indexes(*args, **kwargs) + if self._enabled: + return + self._last_selection = self.get_selected_asset_id() + self._clear_selection() + + def set_current_asset_name(self, asset_name): + self._current_asset_name = asset_name + # Hide set current asset if there is no one + self.set_current_asset_btn_visibility(asset_name is not None) + + def _get_current_session_asset(self): + return self._current_asset_name + + def _create_source_model(self): + return AssetsHierarchyModel(self._controller) + + def _refresh_model(self): + self._model.reset() + self._on_model_refresh(self._model.rowCount() > 0) + + +class AssetsHierarchyModel(QtGui.QStandardItemModel): + """Assets hiearrchy model. + + For selecting asset for which should beinstance created. + + Uses controller to load asset hierarchy. All asset documents are stored by + their parents. + """ + def __init__(self, controller): + super(AssetsHierarchyModel, self).__init__() + self._controller = controller + + self._items_by_name = {} + self._items_by_asset_id = {} + + def reset(self): + self.clear() + + self._items_by_name = {} + self._items_by_asset_id = {} + assets_by_parent_id = self._controller.get_asset_hierarchy() + + items_by_name = {} + items_by_asset_id = {} + _queue = collections.deque() + _queue.append((self.invisibleRootItem(), None)) + while _queue: + parent_item, parent_id = _queue.popleft() + children = assets_by_parent_id.get(parent_id) + if not children: + continue + + children_by_name = { + child["name"]: child + for child in children + } + items = [] + for name in sorted(children_by_name.keys()): + child = children_by_name[name] + child_id = child["_id"] + item = QtGui.QStandardItem(name) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + ) + item.setData(child_id, ASSET_ID_ROLE) + item.setData(name, ASSET_NAME_ROLE) + + items_by_name[name] = item + items_by_asset_id[child_id] = item + items.append(item) + _queue.append((item, child_id)) + + parent_item.appendRows(items) + + self._items_by_name = items_by_name + self._items_by_asset_id = items_by_asset_id + + def get_index_by_asset_id(self, asset_id): + item = self._items_by_asset_id.get(asset_id) + if item is not None: + return item.index() + return QtCore.QModelIndex() + + def get_index_by_asset_name(self, asset_name): + item = self._items_by_name.get(asset_name) + if item is None: + return QtCore.QModelIndex() + return item.index() + + def name_is_valid(self, item_name): + return item_name in self._items_by_name + + +class AssetsDialog(QtWidgets.QDialog): + """Dialog to select asset for a context of instance.""" + def __init__(self, controller, parent): + super(AssetsDialog, self).__init__(parent) + self.setWindowTitle("Select asset") + + model = AssetsHierarchyModel(controller) + proxy_model = RecursiveSortFilterProxyModel() + proxy_model.setSourceModel(model) + proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + filter_input = PlaceholderLineEdit(self) + filter_input.setPlaceholderText("Filter assets..") + + asset_view = QtWidgets.QTreeView(self) + asset_view.setModel(proxy_model) + asset_view.setHeaderHidden(True) + asset_view.setFrameShape(QtWidgets.QFrame.NoFrame) + asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) + asset_view.setAlternatingRowColors(True) + asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows) + asset_view.setAllColumnsShowFocus(True) + + ok_btn = QtWidgets.QPushButton("OK", self) + cancel_btn = QtWidgets.QPushButton("Cancel", self) + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + btns_layout.addWidget(cancel_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(filter_input, 0) + layout.addWidget(asset_view, 1) + layout.addLayout(btns_layout, 0) + + filter_input.textChanged.connect(self._on_filter_change) + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self._filter_input = filter_input + self._ok_btn = ok_btn + self._cancel_btn = cancel_btn + + self._model = model + self._proxy_model = proxy_model + + self._asset_view = asset_view + + self._selected_asset = None + # Soft refresh is enabled + # - reset will happen at all cost if soft reset is enabled + # - adds ability to call reset on multiple places without repeating + self._soft_reset_enabled = True + + def showEvent(self, event): + """Refresh asset model on show.""" + super(AssetsDialog, self).showEvent(event) + # Refresh on show + self.reset(False) + + def reset(self, force=True): + """Reset asset model.""" + if not force and not self._soft_reset_enabled: + return + + if self._soft_reset_enabled: + self._soft_reset_enabled = False + + self._model.reset() + + def name_is_valid(self, name): + """Is asset name valid. + + Args: + name(str): Asset name that should be checked. + """ + # Make sure we're reset + self.reset(False) + # Valid the name by model + return self._model.name_is_valid(name) + + def _on_filter_change(self, text): + """Trigger change of filter of assets.""" + self._proxy_model.setFilterFixedString(text) + + def _on_cancel_clicked(self): + self.done(0) + + def _on_ok_clicked(self): + index = self._asset_view.currentIndex() + asset_name = None + if index.isValid(): + asset_name = index.data(QtCore.Qt.DisplayRole) + self._selected_asset = asset_name + self.done(1) + + def set_selected_assets(self, asset_names): + """Change preselected asset before showing the dialog. + + This also resets model and clean filter. + """ + self.reset(False) + self._asset_view.collapseAll() + self._filter_input.setText("") + + indexes = [] + for asset_name in asset_names: + index = self._model.get_index_by_asset_name(asset_name) + if index.isValid(): + indexes.append(index) + + if not indexes: + return + + index_deque = collections.deque() + for index in indexes: + index_deque.append(index) + + all_indexes = [] + while index_deque: + index = index_deque.popleft() + all_indexes.append(index) + + parent_index = index.parent() + if parent_index.isValid(): + index_deque.append(parent_index) + + for index in all_indexes: + proxy_index = self._proxy_model.mapFromSource(index) + self._asset_view.expand(proxy_index) + + def get_selected_asset(self): + """Get selected asset name.""" + return self._selected_asset diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py new file mode 100644 index 0000000000..a0b3a340ae --- /dev/null +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -0,0 +1,169 @@ +from Qt import QtCore, QtGui + +from openpype.tools.utils.tasks_widget import TasksWidget, TASK_NAME_ROLE + + +class TasksModel(QtGui.QStandardItemModel): + """Tasks model. + + Task model must have set context of asset documents. + + Items in model are based on 0-infinite asset documents. Always contain + an interserction of context asset tasks. When no assets are in context + them model is empty if 2 or more are in context assets that don't have + tasks with same names then model is empty too. + + Args: + controller (PublisherController): Controller which handles creation and + publishing. + """ + def __init__(self, controller): + super(TasksModel, self).__init__() + + self._controller = controller + self._items_by_name = {} + self._asset_names = [] + self._task_names_by_asset_name = {} + + def set_asset_names(self, asset_names): + """Set assets context.""" + self._asset_names = asset_names + self.reset() + + @staticmethod + def get_intersection_of_tasks(task_names_by_asset_name): + """Calculate intersection of task names from passed data. + + Example: + ``` + # Passed `task_names_by_asset_name` + { + "asset_1": ["compositing", "animation"], + "asset_2": ["compositing", "editorial"] + } + ``` + Result: + ``` + # Set + {"compositing"} + ``` + + Args: + task_names_by_asset_name (dict): Task names in iterable by parent. + """ + tasks = None + for task_names in task_names_by_asset_name.values(): + if tasks is None: + tasks = set(task_names) + else: + tasks &= set(task_names) + + if not tasks: + break + return tasks or set() + + def is_task_name_valid(self, asset_name, task_name): + """Is task name available for asset. + + Args: + asset_name (str): Name of asset where should look for task. + task_name (str): Name of task which should be available in asset's + tasks. + """ + task_names = self._task_names_by_asset_name.get(asset_name) + if task_names and task_name in task_names: + return True + return False + + def reset(self): + """Update model by current context.""" + if not self._asset_names: + self._items_by_name = {} + self._task_names_by_asset_name = {} + self.clear() + return + + task_names_by_asset_name = ( + self._controller.get_task_names_by_asset_names(self._asset_names) + ) + + self._task_names_by_asset_name = task_names_by_asset_name + + new_task_names = self.get_intersection_of_tasks( + task_names_by_asset_name + ) + old_task_names = set(self._items_by_name.keys()) + if new_task_names == old_task_names: + return + + root_item = self.invisibleRootItem() + for task_name in old_task_names: + if task_name not in new_task_names: + item = self._items_by_name.pop(task_name) + root_item.removeRow(item.row()) + + new_items = [] + for task_name in new_task_names: + if task_name in self._items_by_name: + continue + + item = QtGui.QStandardItem(task_name) + item.setData(task_name, TASK_NAME_ROLE) + self._items_by_name[task_name] = item + new_items.append(item) + root_item.appendRows(new_items) + + def headerData(self, section, orientation, role=None): + if role is None: + role = QtCore.Qt.EditRole + # Show nice labels in the header + if section == 0: + if ( + role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) + and orientation == QtCore.Qt.Horizontal + ): + return "Tasks" + + return super(TasksModel, self).headerData(section, orientation, role) + + +class CreateDialogTasksWidget(TasksWidget): + def __init__(self, controller, parent): + self._controller = controller + super(CreateDialogTasksWidget, self).__init__(None, parent) + + self._enabled = None + + def _create_source_model(self): + return TasksModel(self._controller) + + def set_asset_name(self, asset_name): + current = self.get_selected_task_name() + if current: + self._last_selected_task_name = current + + self._tasks_model.set_asset_names([asset_name]) + if self._last_selected_task_name and self._enabled: + self.select_task_name(self._last_selected_task_name) + + # Force a task changed emit. + self.task_changed.emit() + + def select_task_name(self, task_name): + super(CreateDialogTasksWidget, self).select_task_name(task_name) + if not self._enabled: + current = self.get_selected_task_name() + if current: + self._last_selected_task_name = current + self._clear_selection() + + def set_enabled(self, enabled): + self._enabled = enabled + if not enabled: + last_selected_task_name = self.get_selected_task_name() + if last_selected_task_name: + self._last_selected_task_name = last_selected_task_name + self._clear_selection() + + elif self._last_selected_task_name is not None: + self.select_task_name(self._last_selected_task_name) From 3878c5262a5293ed250c85a15be6aa5531da1da2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:22:57 +0100 Subject: [PATCH 04/14] added widgett for pre create attributes --- .../publisher/widgets/precreate_widget.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 openpype/tools/publisher/widgets/precreate_widget.py diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py new file mode 100644 index 0000000000..7f0228946e --- /dev/null +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -0,0 +1,61 @@ +from Qt import QtWidgets + +from openpype.widgets.attribute_defs import create_widget_for_attr_def + + +class AttributesWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(AttributesWidget, self).__init__(parent) + + layout = QtWidgets.QGridLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self._layout = layout + + self._widgets = [] + + def current_value(self): + output = {} + for widget in self._widgets: + attr_def = widget.attr_def + if attr_def.is_value_def: + output[attr_def.key] = widget.current_value() + return output + + def clear_attr_defs(self): + while self._layout.count(): + item = self._layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + + self._widgets = [] + + def set_attr_defs(self, attr_defs): + self.clear_attr_defs() + + row = 0 + for attr_def in attr_defs: + widget = create_widget_for_attr_def(attr_def, self) + + expand_cols = 2 + if attr_def.is_value_def and attr_def.is_label_horizontal: + expand_cols = 1 + + col_num = 2 - expand_cols + + if attr_def.label: + label_widget = QtWidgets.QLabel(attr_def.label, self) + self._layout.addWidget( + label_widget, row, 0, 1, expand_cols + ) + if not attr_def.is_label_horizontal: + row += 1 + + self._layout.addWidget( + widget, row, col_num, 1, expand_cols + ) + self._widgets.append(widget) + + row += 1 From 9c6a57aa587fe7577d9214e55311029d36e34039 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:23:37 +0100 Subject: [PATCH 05/14] creator can define precreate attribute definitions and allowing context change --- openpype/pipeline/create/creator_plugins.py | 26 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index aa2e3333ce..8247581d94 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -80,7 +80,7 @@ class BaseCreator: self.create_context.creator_removed_instance(instance) @abstractmethod - def create(self, options=None): + def create(self): """Create new instance. Replacement of `process` method from avalon implementation. @@ -199,15 +199,22 @@ class Creator(BaseCreator): # - may not be used if `get_detail_description` is overriden detailed_description = None + # It does make sense to change context on creation + # - in some cases it may confuse artists because it would not be used + # e.g. for buld creators + create_allow_context_change = True + @abstractmethod - def create(self, subset_name, instance_data, options=None): + def create(self, subset_name, instance_data, pre_create_data): """Create new instance and store it. Ideally should be stored to workfile using host implementation. Args: subset_name(str): Subset name of created instance. - instance_data(dict): + instance_data(dict): Base data for instance. + pre_create_data(dict): Data based on pre creation attributes. + Those may affect how creator works. """ # instance = CreatedInstance( @@ -258,6 +265,19 @@ class Creator(BaseCreator): return None + def get_pre_create_attr_defs(self): + """Plugin attribute definitions needed for creation. + Attribute definitions of plugin that define how creation will work. + Values of these definitions are passed to `create` method. + NOTE: + Convert method should be implemented which should care about updating + keys/values when plugin attributes change. + Returns: + list: Attribute definitions that can be tweaked for + created instance. + """ + return [] + class AutoCreator(BaseCreator): """Creator which is automatically triggered without user interaction. From f0b7f72325ff51a9f7cf4681c2fa81c44562d18c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:25:34 +0100 Subject: [PATCH 06/14] creator dialog has context widget and creator's attributes --- .../tools/publisher/widgets/create_dialog.py | 202 ++++++++++++++++-- .../tools/publisher/widgets/images/delete.png | Bin 12343 -> 0 bytes openpype/tools/publisher/widgets/models.py | 201 ----------------- openpype/tools/publisher/widgets/widgets.py | 144 +------------ openpype/tools/publisher/window.py | 23 +- 5 files changed, 201 insertions(+), 369 deletions(-) delete mode 100644 openpype/tools/publisher/widgets/images/delete.png delete mode 100644 openpype/tools/publisher/widgets/models.py diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 84fc6d4e97..05936265bb 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -15,6 +15,9 @@ from openpype.pipeline.create import ( ) from .widgets import IconValuePixmapLabel +from .assets_widget import CreateDialogAssetsWidget +from .tasks_widget import CreateDialogTasksWidget +from .precreate_widget import AttributesWidget from ..constants import ( VARIANT_TOOLTIP, CREATOR_IDENTIFIER_ROLE, @@ -202,7 +205,34 @@ class CreateDialog(QtWidgets.QDialog): self._name_pattern = name_pattern self._compiled_name_pattern = re.compile(name_pattern) + context_widget = QtWidgets.QWidget(self) + + assets_widget = CreateDialogAssetsWidget(controller, context_widget) + tasks_widget = CreateDialogTasksWidget(controller, context_widget) + + context_layout = QtWidgets.QVBoxLayout(context_widget) + context_layout.setContentsMargins(0, 0, 0, 0) + context_layout.setSpacing(0) + context_layout.addWidget(assets_widget, 2) + context_layout.addWidget(tasks_widget, 1) + + pre_create_scroll_area = QtWidgets.QScrollArea(self) + pre_create_contet_widget = QtWidgets.QWidget(pre_create_scroll_area) + pre_create_scroll_area.setWidget(pre_create_contet_widget) + pre_create_scroll_area.setWidgetResizable(True) + + pre_create_contet_layout = QtWidgets.QVBoxLayout( + pre_create_contet_widget + ) + pre_create_attributes_widget = AttributesWidget( + pre_create_contet_widget + ) + pre_create_contet_layout.addWidget(pre_create_attributes_widget, 0) + pre_create_contet_layout.addStretch(1) + creator_description_widget = CreatorDescriptionWidget(self) + # TODO add HELP button + creator_description_widget.setVisible(False) creators_view = QtWidgets.QListView(self) creators_model = QtGui.QStandardItemModel() @@ -235,6 +265,14 @@ class CreateDialog(QtWidgets.QDialog): form_layout.addRow("Name:", variant_layout) form_layout.addRow("Subset:", subset_name_input) + mid_widget = QtWidgets.QWidget(self) + mid_layout = QtWidgets.QVBoxLayout(mid_widget) + mid_layout.setContentsMargins(0, 0, 0, 0) + mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self)) + mid_layout.addWidget(creators_view, 1) + mid_layout.addLayout(form_layout, 0) + mid_layout.addWidget(create_btn, 0) + left_layout = QtWidgets.QVBoxLayout() left_layout.addWidget(QtWidgets.QLabel("Choose family:", self)) left_layout.addWidget(creators_view, 1) @@ -242,20 +280,36 @@ class CreateDialog(QtWidgets.QDialog): left_layout.addWidget(create_btn, 0) layout = QtWidgets.QHBoxLayout(self) - layout.addLayout(left_layout, 0) - layout.addSpacing(5) - layout.addWidget(creator_description_widget, 1) + layout.setSpacing(10) + layout.addWidget(context_widget, 1) + layout.addWidget(mid_widget, 1) + layout.addWidget(pre_create_scroll_area, 1) + + prereq_timer = QtCore.QTimer() + prereq_timer.setInterval(50) + prereq_timer.setSingleShot(True) + + prereq_timer.timeout.connect(self._on_prereq_timer) create_btn.clicked.connect(self._on_create) variant_input.returnPressed.connect(self._on_create) variant_input.textChanged.connect(self._on_variant_change) creators_view.selectionModel().currentChanged.connect( - self._on_item_change + self._on_creator_item_change ) variant_hints_menu.triggered.connect(self._on_variant_action) + assets_widget.selection_changed.connect(self._on_asset_change) + assets_widget.current_context_required.connect( + self._on_current_session_context_request + ) + tasks_widget.task_changed.connect(self._on_task_change) controller.add_plugins_refresh_callback(self._on_plugins_refresh) + self._pre_create_attributes_widget = pre_create_attributes_widget + self._context_widget = context_widget + self._assets_widget = assets_widget + self._tasks_widget = tasks_widget self.creator_description_widget = creator_description_widget self.subset_name_input = subset_name_input @@ -269,12 +323,54 @@ class CreateDialog(QtWidgets.QDialog): self.creators_view = creators_view self.create_btn = create_btn + self._prereq_timer = prereq_timer + + def _context_change_is_enabled(self): + return self._context_widget.isEnabled() + + def _get_asset_name(self): + asset_name = None + if self._context_change_is_enabled(): + asset_name = self._assets_widget.get_selected_asset_name() + + if asset_name is None: + asset_name = self._asset_name + return asset_name + + def _get_task_name(self): + task_name = None + if self._context_change_is_enabled(): + # Don't use selection of task if asset is not set + asset_name = self._assets_widget.get_selected_asset_name() + if asset_name: + task_name = self._tasks_widget.get_selected_task_name() + + if not task_name: + task_name = self._task_name + return task_name + @property def dbcon(self): return self.controller.dbcon + def _set_context_enabled(self, enabled): + self._assets_widget.set_enabled(enabled) + self._tasks_widget.set_enabled(enabled) + self._context_widget.setEnabled(enabled) + def refresh(self): - self._prereq_available = True + # Get context before refresh to keep selection of asset and + # task widgets + asset_name = self._get_asset_name() + task_name = self._get_task_name() + + self._prereq_available = False + + # Disable context widget so refresh of asset will use context asset + # name + self._set_context_enabled(False) + + self._assets_widget.refresh() # Refresh data before update of creators self._refresh_asset() @@ -282,21 +378,36 @@ class CreateDialog(QtWidgets.QDialog): # data self._refresh_creators() + self._assets_widget.set_current_asset_name(self._asset_name) + self._assets_widget.select_asset_by_name(asset_name) + self._tasks_widget.set_asset_name(asset_name) + self._tasks_widget.select_task_name(task_name) + + self._invalidate_prereq() + + def _invalidate_prereq(self): + self._prereq_timer.start() + + def _on_prereq_timer(self): + prereq_available = True + if self.creators_model.rowCount() < 1: + prereq_available = False + if self._asset_doc is None: # QUESTION how to handle invalid asset? - self.subset_name_input.setText("< Asset is not set >") - self._prereq_available = False + prereq_available = False - if self.creators_model.rowCount() < 1: - self._prereq_available = False + if prereq_available != self._prereq_available: + self._prereq_available = prereq_available - self.create_btn.setEnabled(self._prereq_available) - self.creators_view.setEnabled(self._prereq_available) - self.variant_input.setEnabled(self._prereq_available) - self.variant_hints_btn.setEnabled(self._prereq_available) + self.create_btn.setEnabled(prereq_available) + self.creators_view.setEnabled(prereq_available) + self.variant_input.setEnabled(prereq_available) + self.variant_hints_btn.setEnabled(prereq_available) + self._on_variant_change() def _refresh_asset(self): - asset_name = self._asset_name + asset_name = self._get_asset_name() # Skip if asset did not change if self._asset_doc and self._asset_doc["name"] == asset_name: @@ -324,6 +435,9 @@ class CreateDialog(QtWidgets.QDialog): ) self._subset_names = set(subset_docs.distinct("name")) + if not asset_doc: + self.subset_name_input.setText("< Asset is not set >") + def _refresh_creators(self): # Refresh creators and add their families to list existing_items = {} @@ -366,25 +480,62 @@ class CreateDialog(QtWidgets.QDialog): if not indexes: index = self.creators_model.index(0, 0) self.creators_view.setCurrentIndex(index) + else: + index = indexes[0] + + identifier = index.data(CREATOR_IDENTIFIER_ROLE) + + self._set_creator(identifier) def _on_plugins_refresh(self): # Trigger refresh only if is visible if self.isVisible(): self.refresh() - def _on_item_change(self, new_index, _old_index): + def _on_asset_change(self): + self._refresh_asset() + + asset_name = self._assets_widget.get_selected_asset_name() + self._tasks_widget.set_asset_name(asset_name) + if self._context_change_is_enabled(): + self._invalidate_prereq() + + def _on_task_change(self): + if self._context_change_is_enabled(): + self._invalidate_prereq() + + def _on_current_session_context_request(self): + self._assets_widget.set_current_session_asset() + if self._task_name: + self._tasks_widget.select_task_name(self._task_name) + + def _on_creator_item_change(self, new_index, _old_index): identifier = None if new_index.isValid(): identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) + self._set_creator(identifier) + def _set_creator(self, identifier): creator = self.controller.manual_creators.get(identifier) self.creator_description_widget.set_plugin(creator) self._selected_creator = creator if not creator: + self._pre_create_attributes_widget.set_attr_defs([]) + self._set_context_enabled(False) return + if ( + creator.create_allow_context_change + != self._context_change_is_enabled() + ): + self._set_context_enabled(creator.create_allow_context_change) + self._refresh_asset() + + attr_defs = creator.get_pre_create_attr_defs() + self._pre_create_attributes_widget.set_attr_defs(attr_defs) + default_variants = creator.get_default_variants() if not default_variants: default_variants = ["Main"] @@ -410,12 +561,19 @@ class CreateDialog(QtWidgets.QDialog): if self.variant_input.text() != value: self.variant_input.setText(value) - def _on_variant_change(self, variant_value): - if not self._prereq_available or not self._selected_creator: + def _on_variant_change(self, variant_value=None): + if not self._prereq_available: + return + + # This should probably never happen? + if not self._selected_creator: if self.subset_name_input.text(): self.subset_name_input.setText("") return + if variant_value is None: + variant_value = self.variant_input.text() + match = self._compiled_name_pattern.match(variant_value) valid = bool(match) self.create_btn.setEnabled(valid) @@ -425,7 +583,7 @@ class CreateDialog(QtWidgets.QDialog): return project_name = self.controller.project_name - task_name = self._task_name + task_name = self._get_task_name() asset_doc = copy.deepcopy(self._asset_doc) # Calculate subset name with Creator plugin @@ -522,9 +680,9 @@ class CreateDialog(QtWidgets.QDialog): family = index.data(FAMILY_ROLE) subset_name = self.subset_name_input.text() variant = self.variant_input.text() - asset_name = self._asset_name - task_name = self._task_name - options = {} + asset_name = self._get_asset_name() + task_name = self._get_task_name() + pre_create_data = self._pre_create_attributes_widget.current_value() # Where to define these data? # - what data show be stored? instance_data = { @@ -537,7 +695,7 @@ class CreateDialog(QtWidgets.QDialog): error_info = None try: self.controller.create( - creator_identifier, subset_name, instance_data, options + creator_identifier, subset_name, instance_data, pre_create_data ) except CreatorError as exc: diff --git a/openpype/tools/publisher/widgets/images/delete.png b/openpype/tools/publisher/widgets/images/delete.png deleted file mode 100644 index ab02768ba31460baedaaa4d965c6879e9781517e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12343 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 diff --git a/openpype/tools/publisher/widgets/models.py b/openpype/tools/publisher/widgets/models.py deleted file mode 100644 index 0cfd771ef1..0000000000 --- a/openpype/tools/publisher/widgets/models.py +++ /dev/null @@ -1,201 +0,0 @@ -import re -import collections - -from Qt import QtCore, QtGui - - -class AssetsHierarchyModel(QtGui.QStandardItemModel): - """Assets hiearrchy model. - - For selecting asset for which should beinstance created. - - Uses controller to load asset hierarchy. All asset documents are stored by - their parents. - """ - def __init__(self, controller): - super(AssetsHierarchyModel, self).__init__() - self._controller = controller - - self._items_by_name = {} - - def reset(self): - self.clear() - - self._items_by_name = {} - assets_by_parent_id = self._controller.get_asset_hierarchy() - - items_by_name = {} - _queue = collections.deque() - _queue.append((self.invisibleRootItem(), None)) - while _queue: - parent_item, parent_id = _queue.popleft() - children = assets_by_parent_id.get(parent_id) - if not children: - continue - - children_by_name = { - child["name"]: child - for child in children - } - items = [] - for name in sorted(children_by_name.keys()): - child = children_by_name[name] - item = QtGui.QStandardItem(name) - items_by_name[name] = item - items.append(item) - _queue.append((item, child["_id"])) - - parent_item.appendRows(items) - - self._items_by_name = items_by_name - - def name_is_valid(self, item_name): - return item_name in self._items_by_name - - def get_index_by_name(self, item_name): - item = self._items_by_name.get(item_name) - if item: - return item.index() - return QtCore.QModelIndex() - - -class TasksModel(QtGui.QStandardItemModel): - """Tasks model. - - Task model must have set context of asset documents. - - Items in model are based on 0-infinite asset documents. Always contain - an interserction of context asset tasks. When no assets are in context - them model is empty if 2 or more are in context assets that don't have - tasks with same names then model is empty too. - - Args: - controller (PublisherController): Controller which handles creation and - publishing. - """ - def __init__(self, controller): - super(TasksModel, self).__init__() - self._controller = controller - self._items_by_name = {} - self._asset_names = [] - self._task_names_by_asset_name = {} - - def set_asset_names(self, asset_names): - """Set assets context.""" - self._asset_names = asset_names - self.reset() - - @staticmethod - def get_intersection_of_tasks(task_names_by_asset_name): - """Calculate intersection of task names from passed data. - - Example: - ``` - # Passed `task_names_by_asset_name` - { - "asset_1": ["compositing", "animation"], - "asset_2": ["compositing", "editorial"] - } - ``` - Result: - ``` - # Set - {"compositing"} - ``` - - Args: - task_names_by_asset_name (dict): Task names in iterable by parent. - """ - tasks = None - for task_names in task_names_by_asset_name.values(): - if tasks is None: - tasks = set(task_names) - else: - tasks &= set(task_names) - - if not tasks: - break - return tasks or set() - - def is_task_name_valid(self, asset_name, task_name): - """Is task name available for asset. - - Args: - asset_name (str): Name of asset where should look for task. - task_name (str): Name of task which should be available in asset's - tasks. - """ - task_names = self._task_names_by_asset_name.get(asset_name) - if task_names and task_name in task_names: - return True - return False - - def reset(self): - """Update model by current context.""" - if not self._asset_names: - self._items_by_name = {} - self._task_names_by_asset_name = {} - self.clear() - return - - task_names_by_asset_name = ( - self._controller.get_task_names_by_asset_names(self._asset_names) - ) - self._task_names_by_asset_name = task_names_by_asset_name - - new_task_names = self.get_intersection_of_tasks( - task_names_by_asset_name - ) - old_task_names = set(self._items_by_name.keys()) - if new_task_names == old_task_names: - return - - root_item = self.invisibleRootItem() - for task_name in old_task_names: - if task_name not in new_task_names: - item = self._items_by_name.pop(task_name) - root_item.removeRow(item.row()) - - new_items = [] - for task_name in new_task_names: - if task_name in self._items_by_name: - continue - - item = QtGui.QStandardItem(task_name) - self._items_by_name[task_name] = item - new_items.append(item) - root_item.appendRows(new_items) - - -class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): - """Recursive proxy model. - - Item is not filtered if any children match the filter. - - Use case: Filtering by string - parent won't be filtered if does not match - the filter string but first checks if any children does. - """ - def filterAcceptsRow(self, row, parent_index): - regex = self.filterRegExp() - if not regex.isEmpty(): - model = self.sourceModel() - source_index = model.index( - row, self.filterKeyColumn(), parent_index - ) - if source_index.isValid(): - pattern = regex.pattern() - - # Check current index itself - value = model.data(source_index, self.filterRole()) - if re.search(pattern, value, re.IGNORECASE): - return True - - rows = model.rowCount(source_index) - for idx in range(rows): - if self.filterAcceptsRow(idx, source_index): - return True - return False - - return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow( - row, parent_index - ) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 073e5f4bc2..3af59507ca 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -10,11 +10,6 @@ from avalon.vendor import qtawesome from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm -from .models import ( - AssetsHierarchyModel, - TasksModel, - RecursiveSortFilterProxyModel, -) from openpype.tools.utils import ( PlaceholderLineEdit, IconButton, @@ -22,6 +17,8 @@ from openpype.tools.utils import ( BaseClickableFrame ) from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from .assets_widget import AssetsDialog +from .tasks_widget import TasksModel from .icons import ( get_pixmap, get_icon_path @@ -307,143 +304,6 @@ class AbstractInstanceView(QtWidgets.QWidget): ).format(self.__class__.__name__)) -class AssetsDialog(QtWidgets.QDialog): - """Dialog to select asset for a context of instance.""" - def __init__(self, controller, parent): - super(AssetsDialog, self).__init__(parent) - self.setWindowTitle("Select asset") - - model = AssetsHierarchyModel(controller) - proxy_model = RecursiveSortFilterProxyModel() - proxy_model.setSourceModel(model) - proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - - filter_input = PlaceholderLineEdit(self) - filter_input.setPlaceholderText("Filter assets..") - - asset_view = QtWidgets.QTreeView(self) - asset_view.setModel(proxy_model) - asset_view.setHeaderHidden(True) - asset_view.setFrameShape(QtWidgets.QFrame.NoFrame) - asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) - asset_view.setAlternatingRowColors(True) - asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows) - asset_view.setAllColumnsShowFocus(True) - - ok_btn = QtWidgets.QPushButton("OK", self) - cancel_btn = QtWidgets.QPushButton("Cancel", self) - - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.addStretch(1) - btns_layout.addWidget(ok_btn) - btns_layout.addWidget(cancel_btn) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(filter_input, 0) - layout.addWidget(asset_view, 1) - layout.addLayout(btns_layout, 0) - - filter_input.textChanged.connect(self._on_filter_change) - ok_btn.clicked.connect(self._on_ok_clicked) - cancel_btn.clicked.connect(self._on_cancel_clicked) - - self._filter_input = filter_input - self._ok_btn = ok_btn - self._cancel_btn = cancel_btn - - self._model = model - self._proxy_model = proxy_model - - self._asset_view = asset_view - - self._selected_asset = None - # Soft refresh is enabled - # - reset will happen at all cost if soft reset is enabled - # - adds ability to call reset on multiple places without repeating - self._soft_reset_enabled = True - - def showEvent(self, event): - """Refresh asset model on show.""" - super(AssetsDialog, self).showEvent(event) - # Refresh on show - self.reset(False) - - def reset(self, force=True): - """Reset asset model.""" - if not force and not self._soft_reset_enabled: - return - - if self._soft_reset_enabled: - self._soft_reset_enabled = False - - self._model.reset() - - def name_is_valid(self, name): - """Is asset name valid. - - Args: - name(str): Asset name that should be checked. - """ - # Make sure we're reset - self.reset(False) - # Valid the name by model - return self._model.name_is_valid(name) - - def _on_filter_change(self, text): - """Trigger change of filter of assets.""" - self._proxy_model.setFilterFixedString(text) - - def _on_cancel_clicked(self): - self.done(0) - - def _on_ok_clicked(self): - index = self._asset_view.currentIndex() - asset_name = None - if index.isValid(): - asset_name = index.data(QtCore.Qt.DisplayRole) - self._selected_asset = asset_name - self.done(1) - - def set_selected_assets(self, asset_names): - """Change preselected asset before showing the dialog. - - This also resets model and clean filter. - """ - self.reset(False) - self._asset_view.collapseAll() - self._filter_input.setText("") - - indexes = [] - for asset_name in asset_names: - index = self._model.get_index_by_name(asset_name) - if index.isValid(): - indexes.append(index) - - if not indexes: - return - - index_deque = collections.deque() - for index in indexes: - index_deque.append(index) - - all_indexes = [] - while index_deque: - index = index_deque.popleft() - all_indexes.append(index) - - parent_index = index.parent() - if parent_index.isValid(): - index_deque.append(parent_index) - - for index in all_indexes: - proxy_index = self._proxy_model.mapFromSource(index) - self._asset_view.expand(proxy_index) - - def get_selected_asset(self): - """Get selected asset name.""" - return self._selected_asset - - class ClickableLineEdit(QtWidgets.QLineEdit): """QLineEdit capturing left mouse click. diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b668888281..642bd17589 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -33,7 +33,7 @@ class PublisherWindow(QtWidgets.QDialog): default_width = 1000 default_height = 600 - def __init__(self, parent=None): + def __init__(self, parent=None, reset_on_show=None): super(PublisherWindow, self).__init__(parent) self.setWindowTitle("OpenPype publisher") @@ -41,6 +41,9 @@ class PublisherWindow(QtWidgets.QDialog): icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) + if reset_on_show is None: + reset_on_show = True + if parent is None: on_top_flag = QtCore.Qt.WindowStaysOnTopHint else: @@ -55,6 +58,7 @@ class PublisherWindow(QtWidgets.QDialog): | on_top_flag ) + self._reset_on_show = reset_on_show self._first_show = True self._refreshing_instances = False @@ -117,12 +121,16 @@ class PublisherWindow(QtWidgets.QDialog): subset_view_btns_layout.addWidget(change_view_btn) # Layout of view and buttons - subset_view_layout = QtWidgets.QVBoxLayout() + # - widget 'subset_view_widget' is necessary + # - only layout won't be resized automatically to minimum size hint + # on child resize request! + subset_view_widget = QtWidgets.QWidget(subset_views_widget) + subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget) subset_view_layout.setContentsMargins(0, 0, 0, 0) subset_view_layout.addLayout(subset_views_layout, 1) subset_view_layout.addLayout(subset_view_btns_layout, 0) - subset_views_widget.set_center_widget(subset_view_layout) + subset_views_widget.set_center_widget(subset_view_widget) # Whole subset layout with attributes and details subset_content_widget = QtWidgets.QWidget(subset_frame) @@ -249,7 +257,8 @@ class PublisherWindow(QtWidgets.QDialog): self._first_show = False self.resize(self.default_width, self.default_height) self.setStyleSheet(style.load_stylesheet()) - self.reset() + if self._reset_on_show: + self.reset() def closeEvent(self, event): self.controller.save_changes() @@ -382,6 +391,12 @@ class PublisherWindow(QtWidgets.QDialog): context_title = self.controller.get_context_title() self.set_context_label(context_title) + # Give a change to process Resize Request + QtWidgets.QApplication.processEvents() + # Trigger update geometry of + widget = self.subset_views_layout.currentWidget() + widget.updateGeometry() + def _on_subset_change(self, *_args): # Ignore changes if in middle of refreshing if self._refreshing_instances: From 2d7521289a088e4bf3f15f573be42b78ee1dd81c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:25:51 +0100 Subject: [PATCH 07/14] ui attribute definitions are skipped for storing data --- openpype/pipeline/create/context.py | 3 +-- openpype/tools/publisher/control.py | 4 +++- openpype/tools/publisher/widgets/widgets.py | 13 +++++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 7b0f50b1dc..f508e43315 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -94,6 +94,7 @@ class AttributeValues: attr_defs_by_key = { attr_def.key: attr_def for attr_def in attr_defs + if attr_def.is_value_def } for key, value in values.items(): if key not in attr_defs_by_key: @@ -306,8 +307,6 @@ class PublishAttributes: self._plugin_names_order = [] self._missing_plugins = [] self.attr_plugins = attr_plugins or [] - if not attr_plugins: - return origin_data = self._origin_data data = self._data diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 860c009f15..a94fe34e79 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -605,7 +605,9 @@ class PublisherController: found_idx = idx break - value = instance.creator_attributes[attr_def.key] + value = None + if attr_def.is_value_def: + value = instance.creator_attributes[attr_def.key] if found_idx is None: idx = len(output) output.append((attr_def, [instance], [value])) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 3af59507ca..cedd5817c8 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1175,12 +1175,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout = QtWidgets.QFormLayout(content_widget) for attr_def, attr_instances, values in result: widget = create_widget_for_attr_def(attr_def, content_widget) - if len(values) == 1: - value = values[0] - if value is not None: - widget.set_value(values[0]) - else: - widget.set_value(values, True) + if attr_def.is_value_def: + if len(values) == 1: + value = values[0] + if value is not None: + widget.set_value(values[0]) + else: + widget.set_value(values, True) label = attr_def.label or attr_def.key content_layout.addRow(label, widget) From 23c3bc8d0a31875da642aee0ea1298ee34e73831 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:26:09 +0100 Subject: [PATCH 08/14] style changes of header view nad checkbox --- openpype/style/data.json | 2 +- openpype/style/style.css | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 1db0c732cf..b545f5de51 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -66,7 +66,7 @@ }, "nice-checkbox": { "bg-checked": "#56a06f", - "bg-unchecked": "#434b56", + "bg-unchecked": "#21252B", "bg-checker": "#D3D8DE", "bg-checker-hover": "#F0F2F5" }, diff --git a/openpype/style/style.css b/openpype/style/style.css index d9b0ff7421..f0ffe4dfc6 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -387,10 +387,16 @@ QHeaderView::section:only-one { QHeaderView::down-arrow { image: url(:/openpype/images/down_arrow.png); + padding-right: 4px; + subcontrol-origin: padding; + subcontrol-position: center right; } QHeaderView::up-arrow { image: url(:/openpype/images/up_arrow.png); + padding-right: 4px; + subcontrol-origin: padding; + subcontrol-position: center right; } /* Checkboxes */ @@ -1198,6 +1204,10 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { font-size: 36pt; } +#OverlayFrame { + background: rgba(0, 0, 0, 127); +} + #BreadcrumbsPathInput { padding: 2px; font-size: 9pt; From 95176b6e345e8d70a6e24c78e9bd052382b35481 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:26:16 +0100 Subject: [PATCH 09/14] modified example creator --- .../testhost/plugins/create/test_creator_1.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/testhost/plugins/create/test_creator_1.py b/openpype/hosts/testhost/plugins/create/test_creator_1.py index 6ec4d16467..0ee5111979 100644 --- a/openpype/hosts/testhost/plugins/create/test_creator_1.py +++ b/openpype/hosts/testhost/plugins/create/test_creator_1.py @@ -13,6 +13,8 @@ class TestCreatorOne(Creator): family = "test" description = "Testing creator of testhost" + create_allow_context_change = False + def get_icon(self): return resources.get_openpype_splash_filepath() @@ -48,7 +50,17 @@ class TestCreatorOne(Creator): def get_attribute_defs(self): output = [ - lib.NumberDef("number_key", label="Number") + lib.NumberDef("number_key", label="Number"), + ] + return output + + def get_pre_create_attr_defs(self): + output = [ + lib.BoolDef("use_selection", label="Use selection"), + lib.UISeparatorDef(), + lib.UILabelDef("Testing label"), + lib.FileDef("filepath", folders=True, label="Filepath"), + lib.FileDef("filepath_2", multipath=True, folders=True, label="Filepath 2") ] return output From 20f5e8fc1b2263882adec624e1b961a0308a3fda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:51:25 +0100 Subject: [PATCH 10/14] hound fixes --- openpype/hosts/testhost/plugins/create/test_creator_1.py | 4 +++- openpype/tools/publisher/widgets/assets_widget.py | 2 -- openpype/widgets/attribute_defs/widgets.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/testhost/plugins/create/test_creator_1.py b/openpype/hosts/testhost/plugins/create/test_creator_1.py index 0ee5111979..3684f38592 100644 --- a/openpype/hosts/testhost/plugins/create/test_creator_1.py +++ b/openpype/hosts/testhost/plugins/create/test_creator_1.py @@ -60,7 +60,9 @@ class TestCreatorOne(Creator): lib.UISeparatorDef(), lib.UILabelDef("Testing label"), lib.FileDef("filepath", folders=True, label="Filepath"), - lib.FileDef("filepath_2", multipath=True, folders=True, label="Filepath 2") + lib.FileDef( + "filepath_2", multipath=True, folders=True, label="Filepath 2" + ) ] return output diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 5d5372cbce..b8696a2665 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -1,7 +1,5 @@ import collections -import avalon.api - from Qt import QtWidgets, QtCore, QtGui from openpype.tools.utils import ( PlaceholderLineEdit, diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 2eb22209db..a6f1b8d6c9 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -1,4 +1,3 @@ -import os import uuid from Qt import QtWidgets, QtCore From f8be5763b57af0e94b85af74ae95ba1579578349 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 18:05:32 +0100 Subject: [PATCH 11/14] renamed method 'get_attribute_defs' to 'get_instance_attr_defs' --- openpype/hosts/testhost/plugins/create/auto_creator.py | 2 +- openpype/hosts/testhost/plugins/create/test_creator_1.py | 8 ++++++-- openpype/hosts/testhost/plugins/create/test_creator_2.py | 4 ++-- .../hosts/testhost/plugins/publish/collect_context.py | 2 +- .../hosts/testhost/plugins/publish/collect_instance_1.py | 2 +- openpype/pipeline/create/context.py | 2 +- openpype/pipeline/create/creator_plugins.py | 2 +- 7 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/testhost/plugins/create/auto_creator.py b/openpype/hosts/testhost/plugins/create/auto_creator.py index 0690164ae5..45c573e487 100644 --- a/openpype/hosts/testhost/plugins/create/auto_creator.py +++ b/openpype/hosts/testhost/plugins/create/auto_creator.py @@ -11,7 +11,7 @@ class MyAutoCreator(AutoCreator): identifier = "workfile" family = "workfile" - def get_attribute_defs(self): + def get_instance_attr_defs(self): output = [ lib.NumberDef("number_key", label="Number") ] diff --git a/openpype/hosts/testhost/plugins/create/test_creator_1.py b/openpype/hosts/testhost/plugins/create/test_creator_1.py index 3684f38592..45c30e8a27 100644 --- a/openpype/hosts/testhost/plugins/create/test_creator_1.py +++ b/openpype/hosts/testhost/plugins/create/test_creator_1.py @@ -1,3 +1,4 @@ +import json from openpype import resources from openpype.hosts.testhost.api import pipeline from openpype.pipeline import ( @@ -35,7 +36,10 @@ class TestCreatorOne(Creator): for instance in instances: self._remove_instance_from_context(instance) - def create(self, subset_name, data, options=None): + def create(self, subset_name, data, pre_create_data): + print("Data that can be used in create:\n{}".format( + json.dumps(pre_create_data, indent=4) + )) new_instance = CreatedInstance(self.family, subset_name, data, self) pipeline.HostContext.add_instance(new_instance.data_to_store()) self.log.info(new_instance.data) @@ -48,7 +52,7 @@ class TestCreatorOne(Creator): "different_variant" ] - def get_attribute_defs(self): + def get_instance_attr_defs(self): output = [ lib.NumberDef("number_key", label="Number"), ] diff --git a/openpype/hosts/testhost/plugins/create/test_creator_2.py b/openpype/hosts/testhost/plugins/create/test_creator_2.py index 4b1430a6a2..e66304a038 100644 --- a/openpype/hosts/testhost/plugins/create/test_creator_2.py +++ b/openpype/hosts/testhost/plugins/create/test_creator_2.py @@ -15,7 +15,7 @@ class TestCreatorTwo(Creator): def get_icon(self): return "cube" - def create(self, subset_name, data, options=None): + def create(self, subset_name, data, pre_create_data): new_instance = CreatedInstance(self.family, subset_name, data, self) pipeline.HostContext.add_instance(new_instance.data_to_store()) self.log.info(new_instance.data) @@ -38,7 +38,7 @@ class TestCreatorTwo(Creator): for instance in instances: self._remove_instance_from_context(instance) - def get_attribute_defs(self): + def get_instance_attr_defs(self): output = [ lib.NumberDef("number_key"), lib.TextDef("text_key") diff --git a/openpype/hosts/testhost/plugins/publish/collect_context.py b/openpype/hosts/testhost/plugins/publish/collect_context.py index 0ab98fb84b..bbb8477cdf 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_context.py +++ b/openpype/hosts/testhost/plugins/publish/collect_context.py @@ -19,7 +19,7 @@ class CollectContextDataTestHost( hosts = ["testhost"] @classmethod - def get_attribute_defs(cls): + def get_instance_attr_defs(cls): return [ attribute_definitions.BoolDef( "test_bool", diff --git a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py index 3c035eccb6..979ab83f11 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py +++ b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py @@ -20,7 +20,7 @@ class CollectInstanceOneTestHost( hosts = ["testhost"] @classmethod - def get_attribute_defs(cls): + def get_instance_attr_defs(cls): return [ attribute_definitions.NumberDef( "version", diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index f508e43315..4454d31d83 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -419,7 +419,7 @@ class CreatedInstance: # Stored creator specific attribute values # {key: value} creator_values = copy.deepcopy(orig_creator_attributes) - creator_attr_defs = creator.get_attribute_defs() + creator_attr_defs = creator.get_instance_attr_defs() self._data["creator_attributes"] = CreatorAttributeValues( self, creator_attr_defs, creator_values, orig_creator_attributes diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 8247581d94..1ac2c420a2 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -163,7 +163,7 @@ class BaseCreator: dynamic_data=dynamic_data ) - def get_attribute_defs(self): + def get_instance_attr_defs(self): """Plugin attribute definitions. Attribute definitions of plugin that hold data about created instance From 7ec4d501802135fd725e5f37682bc351ca38483f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Jan 2022 13:48:03 +0100 Subject: [PATCH 12/14] precreate widget is separated from create dialog completely --- .../tools/publisher/widgets/create_dialog.py | 38 +++------- .../publisher/widgets/precreate_widget.py | 76 ++++++++++++++++++- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 05936265bb..f9f8310e09 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -17,7 +17,7 @@ from openpype.pipeline.create import ( from .widgets import IconValuePixmapLabel from .assets_widget import CreateDialogAssetsWidget from .tasks_widget import CreateDialogTasksWidget -from .precreate_widget import AttributesWidget +from .precreate_widget import PreCreateWidget from ..constants import ( VARIANT_TOOLTIP, CREATOR_IDENTIFIER_ROLE, @@ -216,22 +216,11 @@ class CreateDialog(QtWidgets.QDialog): context_layout.addWidget(assets_widget, 2) context_layout.addWidget(tasks_widget, 1) - pre_create_scroll_area = QtWidgets.QScrollArea(self) - pre_create_contet_widget = QtWidgets.QWidget(pre_create_scroll_area) - pre_create_scroll_area.setWidget(pre_create_contet_widget) - pre_create_scroll_area.setWidgetResizable(True) + # Precreate attributes widgets + pre_create_widget = PreCreateWidget(self) - pre_create_contet_layout = QtWidgets.QVBoxLayout( - pre_create_contet_widget - ) - pre_create_attributes_widget = AttributesWidget( - pre_create_contet_widget - ) - pre_create_contet_layout.addWidget(pre_create_attributes_widget, 0) - pre_create_contet_layout.addStretch(1) - - creator_description_widget = CreatorDescriptionWidget(self) # TODO add HELP button + creator_description_widget = CreatorDescriptionWidget(self) creator_description_widget.setVisible(False) creators_view = QtWidgets.QListView(self) @@ -273,17 +262,11 @@ class CreateDialog(QtWidgets.QDialog): mid_layout.addLayout(form_layout, 0) mid_layout.addWidget(create_btn, 0) - left_layout = QtWidgets.QVBoxLayout() - left_layout.addWidget(QtWidgets.QLabel("Choose family:", self)) - left_layout.addWidget(creators_view, 1) - left_layout.addLayout(form_layout, 0) - left_layout.addWidget(create_btn, 0) - layout = QtWidgets.QHBoxLayout(self) layout.setSpacing(10) layout.addWidget(context_widget, 1) layout.addWidget(mid_widget, 1) - layout.addWidget(pre_create_scroll_area, 1) + layout.addWidget(pre_create_widget, 1) prereq_timer = QtCore.QTimer() prereq_timer.setInterval(50) @@ -306,7 +289,8 @@ class CreateDialog(QtWidgets.QDialog): controller.add_plugins_refresh_callback(self._on_plugins_refresh) - self._pre_create_attributes_widget = pre_create_attributes_widget + self._pre_create_widget = pre_create_widget + self._context_widget = context_widget self._assets_widget = assets_widget self._tasks_widget = tasks_widget @@ -519,10 +503,11 @@ class CreateDialog(QtWidgets.QDialog): creator = self.controller.manual_creators.get(identifier) self.creator_description_widget.set_plugin(creator) + self._pre_create_widget.set_plugin(creator) self._selected_creator = creator + if not creator: - self._pre_create_attributes_widget.set_attr_defs([]) self._set_context_enabled(False) return @@ -533,9 +518,6 @@ class CreateDialog(QtWidgets.QDialog): self._set_context_enabled(creator.create_allow_context_change) self._refresh_asset() - attr_defs = creator.get_pre_create_attr_defs() - self._pre_create_attributes_widget.set_attr_defs(attr_defs) - default_variants = creator.get_default_variants() if not default_variants: default_variants = ["Main"] @@ -682,7 +664,7 @@ class CreateDialog(QtWidgets.QDialog): variant = self.variant_input.text() asset_name = self._get_asset_name() task_name = self._get_task_name() - pre_create_data = self._pre_create_attributes_widget.current_value() + pre_create_data = self._pre_create_widget.current_value() # Where to define these data? # - what data show be stored? instance_data = { diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py index 7f0228946e..c7a215d178 100644 --- a/openpype/tools/publisher/widgets/precreate_widget.py +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -1,8 +1,82 @@ -from Qt import QtWidgets +from Qt import QtWidgets, QtCore from openpype.widgets.attribute_defs import create_widget_for_attr_def +class PreCreateWidget(QtWidgets.QWidget): + def __init__(self, parent): + super(PreCreateWidget, self).__init__(parent) + + # Precreate attribute defininitions of Creator + scroll_area = QtWidgets.QScrollArea(self) + contet_widget = QtWidgets.QWidget(scroll_area) + scroll_area.setWidget(contet_widget) + scroll_area.setWidgetResizable(True) + + attributes_widget = AttributesWidget(contet_widget) + contet_layout = QtWidgets.QVBoxLayout(contet_widget) + contet_layout.setContentsMargins(0, 0, 0, 0) + contet_layout.addWidget(attributes_widget, 0) + contet_layout.addStretch(1) + + # Widget showed when there are no attribute definitions from creator + empty_widget = QtWidgets.QWidget(self) + empty_widget.setVisible(False) + + # Label showed when creator is not selected + no_creator_label = QtWidgets.QLabel( + "Creator is not selected", + empty_widget + ) + no_creator_label.setWordWrap(True) + + # Creator does not have precreate attributes + empty_label = QtWidgets.QLabel( + "This creator had no configurable options", + empty_widget + ) + empty_label.setWordWrap(True) + empty_label.setVisible(False) + + empty_layout = QtWidgets.QVBoxLayout(empty_widget) + empty_layout.setContentsMargins(0, 0, 0, 0) + empty_layout.addStretch(1) + empty_layout.addWidget(empty_label, 0, QtCore.Qt.AlignCenter) + empty_layout.addWidget(no_creator_label, 0, QtCore.Qt.AlignCenter) + empty_layout.addStretch(1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(scroll_area, 1) + main_layout.addWidget(empty_widget, 1) + + self._scroll_area = scroll_area + self._empty_widget = empty_widget + + self._empty_label = empty_label + self._no_creator_label = no_creator_label + self._attributes_widget = attributes_widget + + def current_value(self): + return self._attributes_widget.current_value() + + def set_plugin(self, creator): + attr_defs = [] + creator_selected = False + if creator is not None: + creator_selected = True + attr_defs = creator.get_pre_create_attr_defs() + + self._attributes_widget.set_attr_defs(attr_defs) + + attr_defs_available = len(attr_defs) > 0 + self._scroll_area.setVisible(attr_defs_available) + self._empty_widget.setVisible(not attr_defs_available) + + self._empty_label.setVisible(creator_selected) + self._no_creator_label.setVisible(not creator_selected) + + class AttributesWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(AttributesWidget, self).__init__(parent) From d7393646a3a084e3502dec720abbf51aba1385ac Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Jan 2022 13:48:18 +0100 Subject: [PATCH 13/14] handle default value of 'is_label_horizontal' --- openpype/pipeline/lib/attribute_definitions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/lib/attribute_definitions.py b/openpype/pipeline/lib/attribute_definitions.py index 111eb44429..189a5e7acd 100644 --- a/openpype/pipeline/lib/attribute_definitions.py +++ b/openpype/pipeline/lib/attribute_definitions.py @@ -46,6 +46,8 @@ class AbtractAttrDef: def __init__( self, key, default, label=None, tooltip=None, is_label_horizontal=None ): + if is_label_horizontal is None: + is_label_horizontal = True self.key = key self.label = label self.tooltip = tooltip @@ -342,8 +344,11 @@ class FileDef(AbtractAttrDef): # Change horizontal label is_label_horizontal = kwargs.get("is_label_horizontal") - if is_label_horizontal is None and multipath: - kwargs["is_label_horizontal"] = False + if is_label_horizontal is None: + is_label_horizontal = True + if multipath: + is_label_horizontal = False + kwargs["is_label_horizontal"] = is_label_horizontal self.multipath = multipath self.folders = folders From 3160199b044d63ffbc4b4d8bb3c7a852578dd355 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Jan 2022 14:15:19 +0100 Subject: [PATCH 14/14] fix grammar --- openpype/tools/publisher/widgets/precreate_widget.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py index c7a215d178..eaadfe890b 100644 --- a/openpype/tools/publisher/widgets/precreate_widget.py +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -32,7 +32,7 @@ class PreCreateWidget(QtWidgets.QWidget): # Creator does not have precreate attributes empty_label = QtWidgets.QLabel( - "This creator had no configurable options", + "This creator has no configurable options", empty_widget ) empty_label.setWordWrap(True) @@ -40,10 +40,8 @@ class PreCreateWidget(QtWidgets.QWidget): empty_layout = QtWidgets.QVBoxLayout(empty_widget) empty_layout.setContentsMargins(0, 0, 0, 0) - empty_layout.addStretch(1) empty_layout.addWidget(empty_label, 0, QtCore.Qt.AlignCenter) empty_layout.addWidget(no_creator_label, 0, QtCore.Qt.AlignCenter) - empty_layout.addStretch(1) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0)