From 396bdfde8a05d37be283f4ba411debc500ee4328 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Jan 2022 17:16:32 +0100 Subject: [PATCH 01/62] 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 zcmb_dX+Tp~7QXLgArO|R6)~cHiB+*e76c?QkwBn=O&EkY1au^kJV+3dGzl8E8c=Xr zN5PFMDosaR+KSW#1sx=65m86uf?B0a9G!wv6d16W0i2g$T}C_eXMW_}d+zsr=bn4c zy(gc=#m@I|qq+eAJYu3F;{kxNDGVSSHe5dTQ9S_o<0^^NAQg+ashTW?T%lQpGK^VT z3CLM8749od+sW=W1YIG>Mfjv7_3ysPyuj*lgR=|Grl28;PR-JnYQ4Y~t) zi>_5_3>v*sqkVaRH@0{Qza_-d_il+%u6-9-EdKBGtgJVVp*KXWz!LEysc*#e>jrvB zjuvIcqk2uYE)|VhfvOEtotbdtg&U_bqM6c2RTiq&W7*}?gV}-qfhPSHD%50ZbP3qC zLiu#IQ{>1UHzrG-jwY%U2IVVnuK*FMPJat=M3fjP8IHj*Dqk zX*rQ{1I7wu1+oL!fdR8w5_TXrh{X*KoW){uSuCe#CmVYY3b{f4zxa+@jqosqSj>%9 z=?xlPj&o`BswbeIj9Q~+rpkH85yV+(t~}L|BtBiM(R^PGg& zUMi*=UBfy6)=HQ!dkJQ|>_(^>Ye71!*KkgVmjeJ5sj$ktRS^LEHfC>Z{5WFad(B}P z3#%5!359Fcge3Ljs>+Fb{Pz=s&u3iis1bDcR*tzwJ?chS_;F*2ot)uOvD)lcq$F3Y zOa5?a$Jge_%8T_K`o>=>&o*=v1@ByVIQJ}~(B}mB^l&E7*InK_!M9)5ES@j9+WLc4 zvF*guoYJO09Uo4sws(vK=kdOpB~y7%Nt6UIdDD1DDig)+V;)PcU9Icc{@wh|WVgR< z&pmLdX8OJhLs^HtJ?*FOFn$)bY7dlbJz74#{EPb+(r#|KP;lvs(6RN;%D!LPt2Pwj zd+sSJT!)Mkmxw=RiF+X_=>RYh-Zp8BXl^1Eo!=app}zmh$D zu1Ohqa9&$~;;hU1D;OZH|B&ZM5ZwNXCgySm$zp>G{(75WTQ8l_;fShwY@nTJ z_4;F!|Mqpg6dvP9NY&t7@pfiPtY8Q8&!RB!XfkEu62Y~?b{ucu*|i+gNoZ&QY=)Nb zElQ9<;>a%D5}pUuXV6g4#knCoZ)(BYL&`+v<+$ChN}9sNN%4@o94L$OlDqu8NFuxq z<&AHpJiWVq&gx%$k}WrY-Mfq`xCNghn%CG|woq1>*(qc>jym~kTBcz4)TMJ`t|{(}BK`|ebrkq^d79_e!Cqe0S}>G*7djeEFs1a)HI3|ot$cql zw6_-5$w@9e=_0r|R4-mtPL^3sJH~YeTcjO?!QCU4Np*7y*HO<*+IGjXzZmH%4Kh^~ z_rS-h!)tJxz0Z*A=9+eRpXu7$0=3mrmI`#ht9!;$Q)`PGuXbNjU-wq%QZi}NlMVg? za(Lfg*AtYq=u`EFKmfia>~;XQmaMru%Up~+oBjiQl+tGME_!e>lWdJB%io*{FTjMK zhy||Uef)#2ewgrV5Ht-RW?q9l^SqAVLaFMlKZILA`qaUw$qO+F5#U?I`w@5+0xhXe zi*x4g{Er?s3JI)5=7a&^Ah@_T9cQBDo7!;}ilRRIDPXN34C)~4TXzEZ6jh-r9&ndK z+zCJ^HMP5#Dc-Uk$Pe!(pq;PtuOY z0+KI?kiNgg>!tlc+*$#gO?v0xA6IOF7m?mM1Zmw1i5qBE3mZ=jEfL_FDN36U?BOdM zOp%dKyGrg;1pvZRoFB^*xVhIIF#(wLfB0w!C%|RgqtmmS{AwOHz~{D}YMflxWofPW zrNAO~VWdODls);Prq63Ed7lgjxAbjvm-T?%Q2!KxyZQRQtosW`z8Nnau_ZO1BpAzS zGcBF@W6PV9Q>MB-_gYo3llYTcbfc%}#*jTerK9j^j{mc|A)wu7>Pj-7da&cZbQiIw z%JkUZddree^_eIOC0_#3-{)(e2_H9U!@>2w@sQ|qiYy+Vc<8}`M-3M|w(tF9c9S>n5uBWZ|{-P&j%aoe;t4qsF$44j#msMA2_gVjdSV^?zfuTKV4a(sd z%Rq7OA`B`6&+^VV!sK+A7OLHYs*=krY^Rv p;09cG6KXH)X5F|^aj-k;IplIgmMuFrs}bwFASNm{@?eCl=-+XbdguTE 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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 2695c218fb9d6345b845e021805d28dad5161032 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Jan 2022 15:10:13 +0100 Subject: [PATCH 12/62] nuke: add color space check functionality to api --- openpype/hosts/nuke/api/__init__.py | 7 +++++ openpype/hosts/nuke/api/utils.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index f7ebcb41da..b571c4098c 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -29,6 +29,10 @@ from .lib import ( maintained_selection ) +from .utils import ( + colorspace_exists_on_node, + get_colorspace_list +) __all__ = ( "file_extensions", @@ -54,4 +58,7 @@ __all__ = ( "update_container", "maintained_selection", + + "colorspace_exists_on_node", + "get_colorspace_list" ) diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index 0ed84f9560..5b0c607292 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -82,3 +82,50 @@ def bake_gizmos_recursively(in_group=None): if node.Class() == "Group": bake_gizmos_recursively(node) + + +def colorspace_exists_on_node(node, colorspace_name): + """ Check if colorspace exists on node + + Look through all options in the colorpsace knob, and see if we have an + exact match to one of the items. + + Args: + node (nuke.Node): nuke node object + colorspace_name (str): color profile name + + Returns: + bool: True if exists + """ + try: + colorspace_knob = node['colorspace'] + except ValueError: + # knob is not available on input node + return False + all_clrs = get_colorspace_list(colorspace_knob) + + return colorspace_name in all_clrs + + +def get_colorspace_list(colorspace_knob): + """Get available colorspace profile names + + Args: + colorspace_knob (nuke.Knob): nuke knob object + + Returns: + list: list of strings names of profiles + """ + + all_clrs = list(colorspace_knob.values()) + reduced_clrs = [] + + if not colorspace_knob.getFlag(nuke.STRIP_CASCADE_PREFIX): + return all_clrs + + # strip colorspace with nested path + for clrs in all_clrs: + clrs = clrs.split('/')[-1] + reduced_clrs.append(clrs) + + return reduced_clrs From 9f5057aafe678fa41f67620a9c18252eac5902a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Jan 2022 15:10:42 +0100 Subject: [PATCH 13/62] nuke: load clip with colorspace from representation data --- openpype/hosts/nuke/plugins/load/load_clip.py | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 3a9c9ca691..621a464c69 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -9,7 +9,8 @@ from openpype.hosts.nuke.api.lib import ( from openpype.hosts.nuke.api import ( containerise, update_container, - viewer_update_and_undo_stop + viewer_update_and_undo_stop, + colorspace_exists_on_node ) from openpype.hosts.nuke.api import plugin @@ -65,12 +66,37 @@ class LoadClip(plugin.NukeLoader): + plugin.get_review_presets_config() ) - def load(self, context, name, namespace, options): + def _set_colorspace(self, node, version_data, repre_data, path=None): + output_color = None + path = path or self.fname.replace("\\", "/") + # get colorspace + colorspace = repre_data.get("colorspace") + colorspace = colorspace or version_data.get("colorspace") + # colorspace from `project_anatomy/imageio/nuke/regexInputs` + iio_colorspace = get_imageio_input_colorspace(path) + + # Set colorspace defined in version data + if ( + colorspace is not None + and colorspace_exists_on_node( + node, str(colorspace)) is not False + ): + node["colorspace"].setValue(str(colorspace)) + output_color = str(colorspace) + elif iio_colorspace is not None: + node["colorspace"].setValue(iio_colorspace) + output_color = iio_colorspace + + return output_color + + + def load(self, context, name, namespace, options): + repres = context["representation"] # reste container id so it is always unique for each instance self.reset_container_id() - is_sequence = len(context["representation"]["files"]) > 1 + is_sequence = len(repres["files"]) > 1 file = self.fname.replace("\\", "/") @@ -79,10 +105,9 @@ class LoadClip(plugin.NukeLoader): version = context['version'] version_data = version.get("data", {}) - repr_id = context["representation"]["_id"] - colorspace = version_data.get("colorspace") - iio_colorspace = get_imageio_input_colorspace(file) - repr_cont = context["representation"]["context"] + repr_id = repres["_id"] + + repr_cont = repres["context"] self.log.info("version_data: {}\n".format(version_data)) self.log.debug( @@ -116,7 +141,7 @@ class LoadClip(plugin.NukeLoader): "Representation id `{}` is failing to load".format(repr_id)) return - read_name = self._get_node_name(context["representation"]) + read_name = self._get_node_name(repres) # Create the Loader with the filename path set read_node = nuke.createNode( @@ -128,11 +153,8 @@ class LoadClip(plugin.NukeLoader): with viewer_update_and_undo_stop(): read_node["file"].setValue(file) - # Set colorspace defined in version data - if colorspace: - read_node["colorspace"].setValue(str(colorspace)) - elif iio_colorspace is not None: - read_node["colorspace"].setValue(iio_colorspace) + set_colorspace = self._set_colorspace( + read_node, version_data, repres["data"]) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -145,6 +167,13 @@ class LoadClip(plugin.NukeLoader): for k in add_keys: if k == 'version': data_imprint.update({k: context["version"]['name']}) + elif k == 'colorspace': + colorspace = repres["data"].get(k) + colorspace = colorspace or version_data.get(k) + data_imprint.update({ + "db_colorspace": colorspace, + "set_colorspace": set_colorspace + }) else: data_imprint.update( {k: context["version"]['data'].get(k, str(None))}) @@ -193,10 +222,13 @@ class LoadClip(plugin.NukeLoader): }) version_data = version.get("data", {}) repr_id = representation["_id"] - colorspace = version_data.get("colorspace") - iio_colorspace = get_imageio_input_colorspace(file) + repr_cont = representation["context"] + # colorspace profile + colorspace = representation["data"].get("colorspace") + colorspace = colorspace or version_data.get("colorspace") + self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) @@ -229,12 +261,9 @@ class LoadClip(plugin.NukeLoader): # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): - - # Set colorspace defined in version data - if colorspace: - read_node["colorspace"].setValue(str(colorspace)) - elif iio_colorspace is not None: - read_node["colorspace"].setValue(iio_colorspace) + set_colorspace = self._set_colorspace( + read_node, version_data, representation["data"], + path=file) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -243,7 +272,8 @@ class LoadClip(plugin.NukeLoader): "frameStart": str(first), "frameEnd": str(last), "version": str(version.get("name")), - "colorspace": colorspace, + "db_colorspace": colorspace, + "set_colorspace": set_colorspace, "source": version_data.get("source"), "handleStart": str(self.handle_start), "handleEnd": str(self.handle_end), From 9746cb4eb87c459c0975e57e2c6b14ef6a325220 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Jan 2022 15:18:06 +0100 Subject: [PATCH 14/62] nuke: load clip internal method moved to the end of class methods --- openpype/hosts/nuke/plugins/load/load_clip.py | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 621a464c69..c94fbd59b5 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -66,31 +66,6 @@ class LoadClip(plugin.NukeLoader): + plugin.get_review_presets_config() ) - def _set_colorspace(self, node, version_data, repre_data, path=None): - output_color = None - path = path or self.fname.replace("\\", "/") - # get colorspace - colorspace = repre_data.get("colorspace") - colorspace = colorspace or version_data.get("colorspace") - - # colorspace from `project_anatomy/imageio/nuke/regexInputs` - iio_colorspace = get_imageio_input_colorspace(path) - - # Set colorspace defined in version data - if ( - colorspace is not None - and colorspace_exists_on_node( - node, str(colorspace)) is not False - ): - node["colorspace"].setValue(str(colorspace)) - output_color = str(colorspace) - elif iio_colorspace is not None: - node["colorspace"].setValue(iio_colorspace) - output_color = iio_colorspace - - return output_color - - def load(self, context, name, namespace, options): repres = context["representation"] # reste container id so it is always unique for each instance @@ -406,3 +381,27 @@ class LoadClip(plugin.NukeLoader): } return self.node_name_template.format(**name_data) + + def _set_colorspace(self, node, version_data, repre_data, path=None): + output_color = None + path = path or self.fname.replace("\\", "/") + # get colorspace + colorspace = repre_data.get("colorspace") + colorspace = colorspace or version_data.get("colorspace") + + # colorspace from `project_anatomy/imageio/nuke/regexInputs` + iio_colorspace = get_imageio_input_colorspace(path) + + # Set colorspace defined in version data + if ( + colorspace is not None + and colorspace_exists_on_node( + node, str(colorspace)) is not False + ): + node["colorspace"].setValue(str(colorspace)) + output_color = str(colorspace) + elif iio_colorspace is not None: + node["colorspace"].setValue(iio_colorspace) + output_color = iio_colorspace + + return output_color \ No newline at end of file From 7ec4d501802135fd725e5f37682bc351ca38483f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Jan 2022 13:48:03 +0100 Subject: [PATCH 15/62] 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 16/62] 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 17/62] 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) From c400c79c6a1cb46dc2714beaad8e28b7a75f8e8e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Jan 2022 15:13:11 +0100 Subject: [PATCH 18/62] nuke: keep consistent variable names --- openpype/hosts/nuke/plugins/load/load_clip.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index c94fbd59b5..13fa4f84c5 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -67,11 +67,11 @@ class LoadClip(plugin.NukeLoader): ) def load(self, context, name, namespace, options): - repres = context["representation"] + repre = context["representation"] # reste container id so it is always unique for each instance self.reset_container_id() - is_sequence = len(repres["files"]) > 1 + is_sequence = len(repre["files"]) > 1 file = self.fname.replace("\\", "/") @@ -80,13 +80,13 @@ class LoadClip(plugin.NukeLoader): version = context['version'] version_data = version.get("data", {}) - repr_id = repres["_id"] + repre_id = repre["_id"] - repr_cont = repres["context"] + repre_cont = repre["context"] self.log.info("version_data: {}\n".format(version_data)) self.log.debug( - "Representation id `{}` ".format(repr_id)) + "Representation id `{}` ".format(repre_id)) self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) @@ -101,7 +101,7 @@ class LoadClip(plugin.NukeLoader): first = 1 last = first + duration elif "#" not in file: - frame = repr_cont.get("frame") + frame = repre_cont.get("frame") assert frame, "Representation is not sequence" padding = len(frame) @@ -113,10 +113,10 @@ class LoadClip(plugin.NukeLoader): if not file: self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) + "Representation id `{}` is failing to load".format(repre_id)) return - read_name = self._get_node_name(repres) + read_name = self._get_node_name(repre) # Create the Loader with the filename path set read_node = nuke.createNode( @@ -129,7 +129,7 @@ class LoadClip(plugin.NukeLoader): read_node["file"].setValue(file) set_colorspace = self._set_colorspace( - read_node, version_data, repres["data"]) + read_node, version_data, repre["data"]) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -143,7 +143,7 @@ class LoadClip(plugin.NukeLoader): if k == 'version': data_imprint.update({k: context["version"]['name']}) elif k == 'colorspace': - colorspace = repres["data"].get(k) + colorspace = repre["data"].get(k) colorspace = colorspace or version_data.get(k) data_imprint.update({ "db_colorspace": colorspace, @@ -196,9 +196,9 @@ class LoadClip(plugin.NukeLoader): "_id": representation["parent"] }) version_data = version.get("data", {}) - repr_id = representation["_id"] + repre_id = representation["_id"] - repr_cont = representation["context"] + repre_cont = representation["context"] # colorspace profile colorspace = representation["data"].get("colorspace") @@ -217,7 +217,7 @@ class LoadClip(plugin.NukeLoader): first = 1 last = first + duration elif "#" not in file: - frame = repr_cont.get("frame") + frame = repre_cont.get("frame") assert frame, "Representation is not sequence" padding = len(frame) @@ -225,7 +225,7 @@ class LoadClip(plugin.NukeLoader): if not file: self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) + "Representation id `{}` is failing to load".format(repre_id)) return read_name = self._get_node_name(representation) @@ -370,12 +370,12 @@ class LoadClip(plugin.NukeLoader): def _get_node_name(self, representation): - repr_cont = representation["context"] + repre_cont = representation["context"] name_data = { - "asset": repr_cont["asset"], - "subset": repr_cont["subset"], + "asset": repre_cont["asset"], + "subset": repre_cont["subset"], "representation": representation["name"], - "ext": repr_cont["representation"], + "ext": repre_cont["representation"], "id": representation["_id"], "class_name": self.__class__.__name__ } From 28128f3827e810886fac74238987af9e48156971 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Jan 2022 15:14:12 +0100 Subject: [PATCH 19/62] flake8: new line at the end --- openpype/hosts/nuke/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 13fa4f84c5..4fc49e4cee 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -404,4 +404,4 @@ class LoadClip(plugin.NukeLoader): node["colorspace"].setValue(iio_colorspace) output_color = iio_colorspace - return output_color \ No newline at end of file + return output_color From c4358e12b22c63a5485b9b2ad6552a8ec52655c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jan 2022 18:59:48 +0100 Subject: [PATCH 20/62] it is possible to tell that mongo url environment should be passed to job --- .../publish/collect_default_deadline_server.py | 4 ++++ .../publish/submit_aftereffects_deadline.py | 3 +++ .../plugins/publish/submit_harmony_deadline.py | 3 +++ .../publish/submit_houdini_remote_publish.py | 15 ++++++++++++--- .../publish/submit_houdini_render_deadline.py | 4 ++++ .../plugins/publish/submit_maya_deadline.py | 3 +++ .../plugins/publish/submit_nuke_deadline.py | 4 ++++ .../plugins/publish/submit_publish_job.py | 13 +++++++++---- 8 files changed, 42 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py index 53231bd7e4..fc056342a8 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -9,6 +9,8 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.01 label = "Default Deadline Webservice" + pass_mongo_url = False + def process(self, context): try: deadline_module = context.data.get("openPypeModules")["deadline"] @@ -19,3 +21,5 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): # get default deadline webservice url from deadline module self.log.debug(deadline_module.deadline_urls) context.data["defaultDeadline"] = deadline_module.deadline_urls["default"] # noqa: E501 + + context.data["deadlinePassMongoUrl"] = self.pass_mongo_url diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py index 69159fda1a..c5a691f8ae 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -67,6 +67,9 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS" ] + # Add mongo url if it's enabled + if self._instance.context.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py index 37041a84b1..e52e76064e 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -276,6 +276,9 @@ class HarmonySubmitDeadline( "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS" ] + # Add mongo url if it's enabled + if self._instance.context.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py index c3228bfe52..199b7834b9 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py @@ -105,15 +105,21 @@ class HoudiniSubmitPublishDeadline(pyblish.api.ContextPlugin): # Clarify job name per submission (include instance name) payload["JobInfo"]["Name"] = job_name + " - %s" % instance self.submit_job( - payload, instances=[instance], deadline=AVALON_DEADLINE + context, + payload, + instances=[instance], + deadline=AVALON_DEADLINE ) else: # Submit a single job self.submit_job( - payload, instances=instance_names, deadline=AVALON_DEADLINE + context, + payload, + instances=instance_names, + deadline=AVALON_DEADLINE ) - def submit_job(self, payload, instances, deadline): + def submit_job(self, context, payload, instances, deadline): # Ensure we operate on a copy, a shallow copy is fine. payload = payload.copy() @@ -125,6 +131,9 @@ class HoudiniSubmitPublishDeadline(pyblish.api.ContextPlugin): # similar environment using it, e.g. "houdini17.5;pluginx2.3" "AVALON_TOOLS", ] + # Add mongo url if it's enabled + if context.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") environment = dict( {key: os.environ[key] for key in keys if key in os.environ}, diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py index fa146c0d30..2a8ed1653a 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -101,6 +101,10 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" "AVALON_TOOLS", ] + # Add mongo url if it's enabled + if context.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") + environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py index 51a19e2aad..c5292a8dd0 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py @@ -498,6 +498,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS" ] + # Add mongo url if it's enabled + if instance.context.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py index ae9cd985eb..5a4779de8a 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -249,6 +249,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "TOOL_ENV", "FOUNDRY_LICENSE" ] + # Add mongo url if it's enabled + if instance.context.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") + # add allowed keys from preset if any if self.env_allowed_keys: keys += self.env_allowed_keys diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py index 516bd755d0..c9a8eedf30 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py @@ -227,6 +227,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): environment["OPENPYPE_USERNAME"] = instance.context.data["user"] environment["OPENPYPE_PUBLISH_JOB"] = "1" environment["OPENPYPE_RENDER_JOB"] = "0" + # Add mongo url if it's enabled + if instance.context.get("deadlinePassMongoUrl"): + mongo_url = os.environ.get("OPENPYPE_MONGO") + if mongo_url: + environment["OPENPYPE_MONGO"] = mongo_url args = [ 'publish', @@ -273,18 +278,18 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: payload["JobInfo"]["JobDependency0"] = job["_id"] - i = 0 - for index, key in enumerate(environment): + index = 0 + for key in environment: if key.upper() in self.enviro_filter: payload["JobInfo"].update( { "EnvironmentKeyValue%d" - % i: "{key}={value}".format( + % index: "{key}={value}".format( key=key, value=environment[key] ) } ) - i += 1 + index += 1 # remove secondary pool payload["JobInfo"].pop("SecondaryPool", None) From 9bfee3a404a2d2a0706863689795cb8754a0be5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Jan 2022 19:07:30 +0100 Subject: [PATCH 21/62] adde settings defying if mongo url is passed --- .../defaults/project_settings/deadline.json | 3 +++ .../projects_schema/schema_project_deadline.json | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 9fb964b494..f4e46fcddc 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -1,6 +1,9 @@ { "deadline_servers": [], "publish": { + "CollectDefaultDeadlineServer": { + "pass_mongo_url": false + }, "ValidateExpectedFiles": { "enabled": true, "active": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index eb9eeb5448..7c44791160 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -17,6 +17,19 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectDefaultDeadlineServer", + "label": "Default Deadline Webservice", + "children": [ + { + "type": "boolean", + "key": "pass_mongo_url", + "label": "Pass Mongo url to job" + } + ] + }, { "type": "dict", "collapsible": true, From d257003d665c18d18cf36c801b78cafe27a93206 Mon Sep 17 00:00:00 2001 From: murphy Date: Tue, 1 Feb 2022 17:47:08 +0100 Subject: [PATCH 22/62] fixed relative links to .md files --- website/docs/admin_distribute.md | 2 +- website/docs/admin_hosts_maya.md | 2 +- website/docs/admin_hosts_tvpaint.md | 6 ++-- website/docs/admin_openpype_commands.md | 2 +- website/docs/admin_settings.md | 11 +++---- website/docs/admin_settings_local.md | 28 ++++++++++++++++++ website/docs/admin_settings_system.md | 4 +-- website/docs/artist_hosts_blender.md | 10 +++---- website/docs/artist_hosts_maya.md | 22 +++++++------- website/docs/artist_hosts_nuke_tut.md | 14 ++++----- website/docs/artist_tools.md | 4 +-- .../settings/settings_colour_coding.png | Bin 0 -> 15757 bytes .../docs/assets/settings/settings_local.png | Bin 0 -> 7212 bytes website/docs/dev_build.md | 2 +- website/docs/manager_ftrack.md | 8 ++--- website/docs/manager_ftrack_actions.md | 4 +-- website/docs/module_deadline.md | 4 +-- website/docs/module_ftrack.md | 16 +++++----- website/docs/module_slack.md | 2 +- website/docusaurus.config.js | 2 +- 20 files changed, 86 insertions(+), 57 deletions(-) create mode 100644 website/docs/admin_settings_local.md create mode 100644 website/docs/assets/settings/settings_colour_coding.png create mode 100644 website/docs/assets/settings/settings_local.png diff --git a/website/docs/admin_distribute.md b/website/docs/admin_distribute.md index b574a21cb2..d5399cfbcc 100644 --- a/website/docs/admin_distribute.md +++ b/website/docs/admin_distribute.md @@ -33,7 +33,7 @@ You have two ways of making this happen #### Automatic Updates Everytime and Artist launches OpenPype on their workstation, it will look to a pre-defined -[openPype update location](#self) for any versions that are newer than the +[openPype update location](admin_settings_system.md#openpype-deployment-control) for any versions that are newer than the latest, locally installed version. If such version is found, it will be downloaded, automatically extracted to the correct place and launched. This will become the default version to run for the artist, until a higher version is detected in the update location again. diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 05a231c21a..93bf32798f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -12,7 +12,7 @@ sidebar_label: Maya Render Settings Validator is here to make sure artists will submit renders we correct settings. Some of these settings are needed by OpenPype but some -can be defined by TD using [OpenPype Settings UI](admin_settings). +can be defined by TD using [OpenPype Settings UI](admin_settings.md). OpenPype enforced settings include: diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md index a99cd19010..aa4354c2cf 100644 --- a/website/docs/admin_hosts_tvpaint.md +++ b/website/docs/admin_hosts_tvpaint.md @@ -10,13 +10,13 @@ import TabItem from '@theme/TabItem'; ## Subset name templates Definition of possibile subset name templates in TVPaint integration. -### [Render Layer](artist_hosts_tvpaint#render-layer) +### Render Layer Render layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. - Key **render_layer** is alias for variant (user's input). - For key **render_pass** is used predefined value `"Beauty"` (ATM value can't be changed). -### [Render pass](artist_hosts_tvpaint#render-pass) +### Render pass Render pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. - Key **render_layer** is filled with value of **render_pass** from `renderLayer` group. - Key **render_pass** is alias for variant (user's input). @@ -26,5 +26,5 @@ It is recommended to use same subset name template for both **renderLayer** and - Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` ::: -### [Review](artist_hosts_tvpaint#review) and [Workfile](artist_hosts_tvpaint#workfile) +### Review and Workfile Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 0831cf4f5a..0c90f93396 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -26,7 +26,7 @@ openpype_console --use-version=3.0.0-foo+bar `--validate-version` to validate integrity of given version -For more information [see here](admin_use#run-openpype). +For more information [see here](admin_use.md#run-openpype). ## Commands diff --git a/website/docs/admin_settings.md b/website/docs/admin_settings.md index 0cd10f81dc..d590f18724 100644 --- a/website/docs/admin_settings.md +++ b/website/docs/admin_settings.md @@ -9,11 +9,10 @@ import TabItem from '@theme/TabItem'; OpenPype stores all of it's settings and configuration in the mongo database. To make the configuration as easy as possible we provide a robust GUI where you can access and change everything that is configurable -**Settings** GUI can be started from the tray menu. +**Settings** GUI can be started from the tray menu Admin -> Studio Settings. Please keep in mind that these settings are set-up for the full studio and not per-individual. If you're looking for individual artist settings, you can head to -[Local Settings](#local-settings) section in the artist documentation. - +[Local Settings](admin_settings_local.md) section in the artist documentation. ## Categories @@ -23,16 +22,18 @@ We use simple colour coding to show you any changes to the settings: - **Orange**: [Project Override](#project-overrides) - **Blue**: Changed and unsaved value +![Colour coding](assets\settings\settings_colour_coding.png) + You'll find that settings are split into categories: -### [System](admin_settings_system) +### System System sections contains all settings that can be configured on a studio level, but cannot be changed on a per-project basis. These include mostly high level options like path to mongo database, toggling major modules on and off and configuring studio wide application availability. -### [Project](admin_settings_project) +### Project Project tab contains most of OpenPype settings and all of them can be configured and overriden on a per-project basis if need be. This includes most of the workflow behaviors like what formats to export, naming conventions, publishing validations, automatic assets loaders and a lot more. diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md new file mode 100644 index 0000000000..b254beb53b --- /dev/null +++ b/website/docs/admin_settings_local.md @@ -0,0 +1,28 @@ +--- +id: admin_settings_local +title: Working with local settings +sidebar_label: Working with local settings +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +OpenPype stores some of it's settings and configuration in local file system. These settings are specific to each individual machine and provides the mechanism for local overrides + +**Local Settings** GUI can be started from the tray menu. + +![Local Settings](assets/settings/settings_local.png) + +## Categories + +### OpenPype Mongo URL + +### General + +### Experimental tools + +### Applications + +### Project Settings + + diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 78be9fb01e..5c2908e933 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -28,13 +28,13 @@ Uses `subst` command, if configured volume character in `Destination` field alre ### OpenPype deployment control **`Versions Repository`** - Location where automatic update mechanism searches for zip files with -OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute#2-openpype-codebase) +OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute.md#2-openpype-codebase) **`Production version`** - Define what is current production version. When value is not set then latest version available in versions repository is resolved as production version. **`Staging version`** - Define what is current staging version. When value is not set then latest staging version available in versions repository is resolved as staging version. -For more information about Production and Staging go to [Distribute](admin_distribute#staging-vs-production). +For more information about Production and Staging go to [Distribute](admin_distribute.md#staging-vs-production). **Production version** and **Staging version** fields will define which version will be used in studio. Filling explicit version will force new OpenPype processes to use it. That gives more control over studio deployment especially when some workstations don't have access to version repository (e.g. remote users). It can be also used to downgrade studio version when newer version have production breaking bug. diff --git a/website/docs/artist_hosts_blender.md b/website/docs/artist_hosts_blender.md index 877e99bff4..cfbcced22f 100644 --- a/website/docs/artist_hosts_blender.md +++ b/website/docs/artist_hosts_blender.md @@ -55,12 +55,12 @@ can edit that field to change it to different asset (but that one must already e `Subset` field is a name you can decide on. It should describe what kind of data you have in the model. For example, you can name it `Proxy` to indicate that this is -low resolution stuff. See [Subset](artist_concepts#subset). +low resolution stuff. See [Subset](artist_concepts.md#subset).