From c4d9c795fd5100701d2785330ed1383b30847f34 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Sun, 8 Sep 2019 20:34:56 +1200 Subject: [PATCH 1/7] wip menu refactor --- NodeGraphQt/base/graph.py | 36 ++++++++++++-- NodeGraphQt/base/menu.py | 85 ++++++++++++++++++++-------------- NodeGraphQt/errors.py | 20 ++------ NodeGraphQt/widgets/viewer.py | 27 ++++++++--- docs/_images/favicon.png | Bin 30813 -> 30072 bytes docs/_templates/localtoc.html | 2 +- 6 files changed, 106 insertions(+), 64 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 7217ef49..653516f4 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -348,21 +348,47 @@ def end_undo(self): def context_menu(self): """ - Returns the node graph root context menu object. + Returns the main context menu from the node graph. Returns: Menu: context menu object. """ - return Menu(self._viewer, self._viewer.context_menu()) + return Menu(self._viewer, self._viewer.context_menus()['Main']) - def disable_context_menu(self, disabled=True): + def get_context_menu(self, name='Main'): """ - Disable/Enable node graph context menu. + Returns the context menu specified by the name. + + Menu types: + "Main" - context menu from the node graph. + "Node" - context menu from a Node. + "Port" - context menu from a Port. + + Args: + name (str): menu name. + + Returns: + Menu: context menu object. + """ + menus = self._viewer.context_menus() + if name not in menus: + return + return Menu(self._viewer, menus['Main']) + + def disable_context_menu(self, disabled=True, name='Main'): + """ + Disable/Enable the main context menu from the node graph. + + Menu types: + "Main" - context menu from the node graph. + "Node" - context menu from a Node. + "Port" - context menu from a Port. Args: disabled (bool): true to enable context menu. + name (str): menu name. (default: Main) """ - menu = self._viewer.context_menu() + menu = self._viewer.context_menus()[name] menu.setDisabled(disabled) menu.setVisible(not disabled) diff --git a/NodeGraphQt/base/menu.py b/NodeGraphQt/base/menu.py index 700a58fc..a2b92b7e 100644 --- a/NodeGraphQt/base/menu.py +++ b/NodeGraphQt/base/menu.py @@ -2,6 +2,7 @@ from distutils.version import LooseVersion from NodeGraphQt import QtGui, QtCore, QtWidgets +from NodeGraphQt.errors import NodeMenuError from NodeGraphQt.widgets.stylesheet import STYLE_QMENU @@ -31,6 +32,15 @@ def name(self): """ return self.qmenu.title() + def set_name(self, name): + """ + Set the name for the menu. + + Args: + name (str): label name. + """ + self.qmenu.setTitle(name) + def get_menu(self, name): """ Returns the child menu by name. @@ -45,7 +55,7 @@ def get_menu(self, name): if action.menu() and action.menu().title() == name: return Menu(self.__viewer, action.menu()) - def get_command(self, name): + def get_action(self, name): """ Returns the child menu command by name. @@ -57,7 +67,7 @@ def get_command(self, name): """ for action in self.qmenu.actions(): if not action.menu() and action.text() == name: - return MenuCommand(self.__viewer, action) + return MenuAction(self.__viewer, action) def all_commands(self): """ @@ -78,9 +88,9 @@ def get_actions(menu): child_actions = get_actions(self.qmenu) return [MenuCommand(self.__viewer, a) for a in child_actions] - def add_menu(self, name): + def create_menu(self, name): """ - Adds a child menu to the current menu. + Create a child menu to the current menu. Args: name (str): menu name. @@ -88,56 +98,58 @@ def add_menu(self, name): Returns: NodeGraphQt.Menu: the appended menu item. """ - menu = QtWidgets.QMenu(name, self.qmenu) - menu.setStyleSheet(STYLE_QMENU) + menu = QtWidgets.QMenu(name, self.__viewer) self.qmenu.addMenu(menu) return Menu(self.__viewer, menu) - def add_command(self, name, func=None, shortcut=None): + def create_action(self, name): """ - Adds a command to the menu. + Create a command to the menu. Args: - name (str): command name. - func (function): command function. - shortcut (str): function shotcut key. + name (NodeGraphQt.MenuCommand): menu command. + - Returns: - NodeGraphQt.MenuCommand: the appended command. """ action = QtWidgets.QAction(name, self.__viewer) - if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): - action.setShortcutVisibleInContextMenu(True) - if shortcut: - action.setShortcut(shortcut) - if func: - action.triggered.connect(func) qaction = self.qmenu.addAction(action) - return MenuCommand(self.__viewer, qaction) + return MenuAction(self.__viewer, qaction) - def add_separator(self): + def create_separator(self): """ - Adds a separator to the menu. + Create a separator to the menu. """ self.qmenu.addSeparator() -class MenuCommand(object): +class MenuAction(QtWidgets.QAction): """ base class for a menu command. """ - def __init__(self, viewer, qaction): + #: signal emits when the action has triggered in the node graph. + executed = QtCore.QSignal(object) + + def __init__(self, parent=None, viewer=None, name=''): + super(MenuAction, self).__init__(parent) self.__viewer = viewer - self.__qaction = qaction + if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): + self.setShortcutVisibleInContextMenu(True) + self.setText(name) + self.triggered.connect(self._on_triggered) def __repr__(self): cls_name = self.__class__.__name__ return 'NodeGraphQt.{}(\'{}\')'.format(cls_name, self.name()) - @property - def qaction(self): - return self.__qaction + def _on_triggered(self): + menu = self.menu() + pos = self.mapToScene(menu.pos()) + + items = self.__viewer._items_near(pos) + + + self.triggered.emit() def name(self): """ @@ -146,7 +158,16 @@ def name(self): Returns: str: label name. """ - return self.qaction.text() + return self.text() + + def set_name(self, name): + """ + Set the name for the menu command. + + Args: + name (str): label name. + """ + self.setText(name) def set_shortcut(self, shortcut=None): """ @@ -157,9 +178,3 @@ def set_shortcut(self, shortcut=None): """ shortcut = shortcut or QtGui.QKeySequence() self.qaction.setShortcut(shortcut) - - def run_command(self): - """ - execute the menu command. - """ - self.qaction.trigger() diff --git a/NodeGraphQt/errors.py b/NodeGraphQt/errors.py index 0a557e0b..db30bd5d 100644 --- a/NodeGraphQt/errors.py +++ b/NodeGraphQt/errors.py @@ -1,18 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - - -class NodePropertyError(Exception): - pass - - -class NodeWidgetError(Exception): - pass - - -class NodeRegistrationError(Exception): - pass - - -class PortRegistrationError(Exception): - pass +class NodePropertyError(Exception): pass +class NodeWidgetError(Exception): pass +class NodeRegistrationError(Exception): pass +class PortRegistrationError(Exception): pass diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index ab89e102..ab8534de 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -73,8 +73,6 @@ def __init__(self, parent=None): self.scene().addItem(self._SLICER_PIPE) self._undo_stack = QtWidgets.QUndoStack(self) - self._context_menu = QtWidgets.QMenu('main', self) - self._context_menu.setStyleSheet(STYLE_QMENU) self._search_widget = TabSearchWidget(self) self._search_widget.search_submitted.connect(self._on_search_submitted) @@ -84,7 +82,13 @@ def __init__(self, parent=None): menu_bar.setNativeMenuBar(False) # shortcuts don't work with "setVisibility(False)". menu_bar.resize(0, 0) - menu_bar.addMenu(self._context_menu) + + self._context_menus = {} + for name in ['Main', 'Node', 'Port']: + menu = QtWidgets.QMenu(name, self) + menu.setStyleSheet(STYLE_QMENU) + self._context_menus[name] = menu + menu_bar.addMenu(menu) self.acyclic = True self.LMB_state = False @@ -155,8 +159,17 @@ def resizeEvent(self, event): def contextMenuEvent(self, event): self.RMB_state = False - if self._context_menu.isEnabled(): - self._context_menu.exec_(event.globalPos()) + + pos = self.mapToScene(self._previous_pos) + items = self._items_near(pos) + ports = [i for i in items if isinstance(i, PortItem)] + nodes = [i for i in items if isinstance(i, AbstractNodeItem)] + if self._context_menus['Port'].isEnabled() and ports: + self._context_menus['Port'].exec_(event.globalPos()) + elif self._context_menus['Node'].isEnabled() and nodes: + self._context_menus['Node'].exec_(event.globalPos()) + elif self._context_menus['Main'].isEnabled(): + self._context_menus['Main'].exec_(event.globalPos()) else: return super(NodeViewer, self).contextMenuEvent(event) @@ -591,8 +604,8 @@ def tab_search_toggle(self): self._search_widget.setVisible(state) self.clearFocus() - def context_menu(self): - return self._context_menu + def context_menus(self): + return self._context_menus def question_dialog(self, text, title='Node Graph'): dlg = QtWidgets.QMessageBox.question( diff --git a/docs/_images/favicon.png b/docs/_images/favicon.png index 428849f6b0a0ce637f2d9cc69e3934712b892459..a68a5b0e40bd88140993b15310f39b1c1f7c05eb 100644 GIT binary patch delta 13479 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 delta 14226 zcma)?Ra2Z@(}i&fmOvmlA-D&3x8T9u-QDHJN$@~Oa0%}2GJ^+q9b}N;GPn=l^8?<4 zuMc;1)jHUF_qA$O-yb2a!w~Br=mY|6oLp?51++d33UToW@d&bTa;Ki5bN|nC9t;6m zOFltPOD-!uHhw`Hb2c7MUUN1<9vf>mb3O|$8y*X88%`U6#$=2ml>b*}s_7fv|0%_c zt(t0tj+YvP?c7+0`;HqW%76?fbx@KW;L}o&k<{{AJ;@1da`x5U6u)Y{HL=a3{TV4Q zg@pMh94!dtEv*HX7)-lagW_XzIHQHOM75S|oVQ+uHnZ$8Rt2t-b9wbgyk?>w;RcBQ zYi~cIASpy5;HSX;PKZ8Lb%%J~CT#|u2A>J?->t3n+uad6t_4o5+nzUvfHs4Ifqmae zCYzyG;05%QnRABz*SF_B)OF2oy6E@@(Lh)C;e-PAjS*qsv zx~4A{x_1usj(x7{-$3ko>o<@i7QYNv6Lfgu1~5p!E{RJYA)CG$d%xV>b;PKwJ|y$r z+EXFydv8`=<+{u#{O%V!t1CTHW=9M8VtYvs^#*H_+)xI(UW#Fv2bAgp((7Valz2a^ zFGNR%zU7<@xWMZ-pM>!Yozz|niO}07SAtg7!YfYvbI4AY*6k1`6K)B?=XJZx&`o61 z1`xYos8vx^f*RZ8EpQ%Ed-k43eK;pMMA&i`U__VDJ=R+5k9Xt!oNN~{a@Lq(7>n2rcgxmehGo zII69coX&VAhN&Zz(mf+jS9^t`WI(X}vV|U|R8JkC4`69L3w%Ds*N}?Md@iTN8SAcn_N|;aSpM8$Fu>Xz4_dU#2(Ch0`g7V zwHH;T*BrNJ-0gKLy@0zospl=hh%@#d@W*Dai#Qy9myY-RN5JAtx1un*gQTP0H^u>o zO&YO89_4*NfJsG;@DLbeJTfI(Wg53SYbcg!6~%Vov!1u-h%R;;V5DqQkPj zZHg2apY%3UTAqK7%KW#Qk3HfC$s0q4ziXe-k7z4gG2h952tvETg(C9(rb36K4O1bi zfg$UvPaltaiA=h3Nv4OXI6ofzT(E5Fbm7sW6YysU-H{w@#Qw^NyyRQ>`g@}t>4!@I z0&n&mK=O08MxqBlq{lkvDQ5fT&bV}Vz7s$ep%wRMd+`C+xz4}vze?DcC!JqCg^KyS zS=}kK3++x(#k_M3Ei57-dbU|Qeyt$~!StmmT9sCw+xv?LA=va2h4h)$Pr2yyaxZ}BU_EgW0v)wmQ<2i+~* zr!p&(5d3BDmYwL?X!4$I6@AKK6Lrqqvv;WNf!*nw09K|sV#hzX(-)Dqel-bQ6VNmO zymKh9|Fih@XDFlaVYJHt$r_^au)Zilpwg=#cu}FR$7UzTAtmJbWABCG2nV98eTeg0 z@a3NHrXui5u0Y|6FNVt{Hl4bT4K~4d>EYwg0yBb!Ie#h1lFU*;FF*@k6nWK*3|ij` zfX4(NWCvsibS$)8c=R8ULkGd!VR68JU-SfN>St6n{)hZUgq5>9Gt1S~x(6Hl@i^bl z;f9AoyTVvp+HB|lwr^}FIx9oP5qg*c7@l0Ed^2Y^Pi;98_1@>_NZW?zwcm_M{4m`9 z1b^d}M)2wgF1&qHP77DwBgLS+@bC=gh9Q-50?^>)dz6RCBJdDS&$CZ3p5)Yun+L&mFCEljyAs8oI;HY3L+xf&&hWoU= zx0LJ#KP$3t2{VpR?MV*HLk1F86ipmddt6RiuT|aNmdpaPZ?7;iLq)$+vQSO;gx|mL zh%OCegW7sugNeG9%X>r}V&u7A~RAmFs|J0pNC0(-Lu`u zQR|(kdnoGCJTmV|G`}V01>9kruGJoBBqDMkQ3iiB_`Spa8_w;Cpz~A`m~=e;5}N2z zglCga9Y1fEr(3GpBbD>a?iN3Q@&b@hL!k3TJuDTGgt_w3pQ<6&%<2{boH!DigSzR={pn-d(!N# z0G<$T$FDiUJ{%kz$WhTXtK5u=)P)m|88 z?WaH1bGB-DQGZ-vgH_YX*P~|_7islSd4{(y4jc=r1CLLzjYt6l>-}{jDx9uw6g9E9 z=QOHVm!cX~P=bFA!WFWWwb!}KkbNy+apL-jepksVNj3{M!Y{1r<^|TByE0u_(B0No z^qP0r?8|70B9X3tz<(|BcwS5%naoe+MN!|AeTjKn9~3ew(||${^ZldE!D!T7AKm0h zM$LW#0CxQR!~kBug}f7vfP?LI(H=4kbzd7rKbkIytW#j$zEIo-)cL1`9MeVyvxB>F zBR0TD$b!h?^waHQOGIo!E`leTSO-S7lKnEy$#tZaInr^n-Hy~chYVPtx^#0Chz|~u zzVNTukDfRr%C6KJE12owXaxt^JMP>5*%R#m;=L;vK-^dtn-ZHnzrnBz)l|5z0>MfB zLKLrJld`n1ic&vLjO8O<{!tTvyTD|TO7C{7-S=qW>tzkT($~+Wl=pf(?>h-i>I&Pn zSb}S>NcqGaXfq5ywy2(Td6MD@|INosmdvAx=!pvZ%?+DC+!kuAcy$wR*m(Ya-MInq zHQH@Yb*1jhqI0k+u6||X`>YhA)!5Kb=gwXfARB=*8DkkwpP!VqB0pc-Gh8hy#m+6D z>)~ular-?yuA=|uc#lr0r;YrUE}+vV92Y7lyI_v|MUnpwm&ge@3EjjV(d_X9xHz!V zL{thLihxEZ1SDLgTaD;kzK8nV!ubL6bb}5yi-ke}3W$01koU2jxtk`oBQ2$MfZ$D|%-h zlOuP`J6wN0g`Zx0!6M|L=92jxjC%h*ixA%T74D-@eto1Ng1cQj3PJrBM|@O5|5gA7 zBaIDZkHOc7;V)|b@JEHFN+)+)l|Q8|dC@2$o4WZ*K|o5-4BsbrZdA;w;->GoaVe&Q zFO6P|3?&7~?go6ow@hGYCRDYzsllbCD^|ciU+>#5n7lr6mC`Hu{r8$Bemw@$+;`$s z!j7z?)YI%*MEiBMNk$;W8ztKWo9PGIe~=Gx4RQj-zEAUfrA>(tTSzi_29rZ#m-*Uu&c&F6+cMP3=-EeLO zGGdH^S0q1VCCXy}U01+0&Vnc#b!ziwQPJ!A@nW&BS<}wTYUhtFL`~t|>PcKX54rtJ zVh>F!EJ&N+Cz;5rUBK+#n8+6(CEs}I}kNr zm4cWA%ZoVr^Ct)2t4TuH*7Rppr=uciHuu$vOX4fbnXO|W{JK(RArGHRVO_O|SuK3Y?wZ(D0D@ zRePY0vjP~_UL0}x79BqoIhqe4yOa$NpjqPZzSdwU7;voFYH+w3Cca__R35yn-U$J% z(2wd|BX0vw+%q&Us=C5K#gAWnEX2&em6;m1e%;)h_2?U(FpQaRe|x7y#V@`RLe(Sx zh;yVEd&Phc!#YIOlpd%VWeB9-64F37TfYUVGkCywC-68_wxX~o-quI!MnX*F7IwMF-ao|mP3w> zCE;DXTOTQ$7v1?*3OMC?&IL}4g`F9u1zGYM5+99s7S<87vyLDCSokPSq~!65x{GS& zn7(=HgQIuao49pH>jf4e20KtYz)$|5abl91ZQP<*eN+PWh=g>VUb(uK3V?DG69=0m zZWtgD&|`=eXqTU_;BIBvMiPMG(1oYl1pbs+NmaJz1g01bn$zA``2#XWt&_noT0aRQec#Z6_{O1fQsj+_OXgpBkfl z|5`qYo(Nvrq~^#e4fTEH?k7z0B-9Y(ds6^hi)6~a5n`+PXDAt8uEg4;DY^EY<%4-Z zKj*q}RM+ANdT0`Pw^-QsQ-V~{n}r)8pKg@ItA+th1)on#K`lQ9PY_AfqgoBF;| zq~Jxp9>cV&{W2|3b*czYvZ!-BC!l4ev0BZ*fTBiN<* zx*BU;1b=HOn1>*Ji z@)N%5B~Z%*@x{V_B1G({`uv)U-+b+pz-_W8H%M3JJMVN=?YnZp8?Lf>-vIo%98~yB zhsq2(QjWAQJy)wZyHi*~YF$y3+Oce=zg08WzC5wE*0&x)p3nS7Ta5blu`MB}I^mfH z`V`7R=oh>*=&HN=C$B7+q2QYr>vu{Yi-zHLQ*<#6B|zoY&cv`QQjD*@LOCA$;v?&} z`9w$`%0NSfxcZzGX{h1X&R@VxII>-XV0W;NGI%^{Y!ItgIL0@n?ks6VvBojPUL9YI zL~jItGv(%bt?X7F`~NzS}9Ed3c~ z&p)^6k+V%i6sO(XRJl@KV+@V#A!+-bgMUkR=o+=QxC5_=9Wfe3-~*0fCeUA^Kc;E0 zQKIDaV2MU$TSaMbh1WczOhWB~&cZ)NSvGF;+YhhbNdWq}zRAhpuR_6?L>Xed++`fe zu0uFJg^H&3##r|TH)qnl!|$B_Z4;)5KY&a7#q-u(a>yG;r~cl8;_RV44R_ve#ailo zxZdf%kPS;XRkv(0HUY8DUGR(~W6{-Fm-haRl$o9l4^6rKh^4&Q?sPr`<&)IRHM(G|Lp-Mb7DnFZLbxS9U#)M5fP*LeK*Jp|9s)g4mO=Dq$|6lZ2G#VC%?!u3K=7k20fwTn2w+I-=E0TIjr^5J;1{%o zhc;%1evGxjM}+R|jtJ&W#kZl)iKzbgx*G3zgxYMK(C>l6(wX7Rz8(2*@R!hae8C^c zOx=a?m)kN=C1^rm>@c6>NLd>RzuO->(ofe3gN=(y0w~gw_eT`TVyix!PEK7>zE-E| z4E4CwZv;dX(}CP3{~7O^)+EFamB_m4`7$()OS*m_Pd!@!skK_I*>mow=c!e*p2t1d zN?!g>fA)0T0j-mqMm2l1h9&Bw-_Z~*KLYl zU+yDYx-H9|@CI8<@8m47Sn|XMH5qIr;|>8En#$V#3qbu~yWBc7E1&O4H*iTbS1oy& zA>Rek_IGu)IJiN!_j9TBe2KPdC`~X2EeL*@k$vPhDviJ%RMq<2LgE79QxK1D_C+4A z26uDT{y1NV0>;LXj^y9Gu>S6f=vCgBsG-=jyL)*jfZ$T1&sEZ%uTXaF<8#fj_NNSP zKo^JF07RGBAX%S2pv!*_ypBi+avMee)&lB!m3f9yuf??^NEA=tncG<3&4mPZUKHVV zH>y8M-~O6CZE|}>Ij&;-tN+{>mt4C zcQhq|oSpf@Eg`#@M+~@h5q3xWy*TOB=W$YN5pZT1Y0=ydlRcTGK4{bmb`mZ8nmrO^ zrm>JT_UPBC`ZQW(RkadSv41fjr%jgKp}LWYWmMJ{pVe4v+lxtFq@IdHzABPzjN>wx z-F}scNEJUjUmok8dM)f#eXDBmsnxh_`Mn~+jOHX_r*BHg9<|BuqF=_}UkJwdv+|;X z9e}A{F2W0ldTQfqG?D{m1t`_%kos7FIA>-i^UAtV*7Tz`9f=>re5g}T`v@c?QObq+ zFPX` z$USybFcW*ph31PI6l8;=>Qr>12TzP@M(&E~;(ThzRZK1Xm~P=BVS!j9iSlgq6t!+5 zJ&_6#qnZD*x?f|rjoBOkjk=J=YZ_H5D;B0BP;Jmp=G`%rpJIDrv~r9j1a(k6VBC*k zLr39pkgNeo|J`h&d@ts8qxXk3uT^5s*;bn!2rQ^@f{s9x0G3MJb7ZXUslAeMB6q{T zS9BH;)|v_#Fu-=Z`Mr%LD|=f)WB&d* z`th|iAh@`r@Y?VVCV|DuY6$Tuz=m_MgRA?9H;j4WjVEGetl{OAApak>KOLlwfo=m5 z%z{-bf^xn;n_Ohh9lZfZz;17NS7@}Jexm4D-W@{-(OKAknDA#M6focU;AlQbaLUu~x>B5Hh4yQOcVjU05+6BURL_vKT!2;yq>CX!uO^=m;J4yB&Ui%Uv-4 zmgQQh;eE3AMi>y06!uqHiTL<3TW=D!dtd(*#Rc`I(tbppOpiejK1DH8)$p`2yNBG> zT>qjud~P>|)07(g2q}H1{U?k&E*x6f22{-h>$4LTNK;=S94C5DIpqLMwf=>*@=qed zW8_3$%ql#HvGqY`|CGzNco8&%(ybKv+x5?WR^!gn3J@@#-UpJo_VTtea}JZoAW2JA z2PfiF(?UX;Jyc3!#zwiubd@)egSgn5eAs=j=H0K@LY@G8&_YH(MJ1L!_*8{v4I92N_PK&zRe8JbG1-4i#P2;kfnVlN@q2Yx~FH^ zvpN8@Q7aO2#nzDBRBgT*Ig0K;g}>3Lc)gu8bD zldtB7r~#Or%{YNHZwcrm60S>Kg>Fcfwy>Kh1M9M(1@%}xhY`I zLvE4>K|oXIq>B06Kz6$9>~>wB{S9ILvap2NQl?cB&2otR<#WX8)E4w@m#0r(@82xF zx_DcRM^WscXpdF09UI#bhLBOyeKM2b4|R3=46n; zf%qIvHCtio^{Yzgu`~CP?(ih(Sm)}%Urxfibmi-7!cn0(j13*wGmdUwkC36wB9FHP zOHt7JMe<2`xycbtJ8gYR*}+oU^}aN}!O=emYmfJAIuoRPw3FsKs~(8#xGzc_wUXu5 zc@SyW6e3S$za@teO7al4Qd<0;?!Q2c7X1nunacdN--zK|GI2ikN8k9;>(k9uB|cRr zA*UsyRxN9Ar@c_R&B&DXev>Q{eHi0*z*umwR0_w}IV)Bt+T(s|<@iq(k1x$!)dL4P z>V0BU0Uu!cfo4vWOV)saa9b5`F1Z7(+hY`Uxt;|jI#r0^ zGG&`M_Zd3B6X?|*=kNW(cOL*+lm7;g^J^3mUdjHJA0?uZfM=kc|gYlB2?!){$WVlU0(}f_YhLU#wybjRL%;#v1EtAM4zjevB6)o_5=@ zHK@OYaHg8OI8VfOJ6Ap1!oPE4BV^6(kwF=q%@X30`R&nxfHUIn zy0QIOz^z@79i4IjY->L(t18G(m+2urTyVOVrVVsINJAg=&-rdIjip~YJTQ(9BpyTM z_Ds_iWU)tfD&V;H`Ix#&!`k#l6Lz|%EXpQ$3#{Z~)U-stC2b*Z2<^g@rdo|dOs4bd zKg%5Ag^8nyeT`uE|G`o>kZcT%*6Y_@!pPkRv^cc(**zmRs7^cy+ga(I*ohjk1|X>5 zE>&33J9efCmdI&MqSQ}#%(5!99DIE(M4_-NUt-*OnR8@W<)SZlh%VOGZMRCai zdA;*eNMl{v+JudV+BiGX+2Qz^Z0UYw2bNXclUaaRn6?Z7He#G zWQKHi@|`(9v4kDIpGjmsjqz(1IZp!1Y~WG_9z4?@lF8tU9a_!awTz4Bscf__e~kvs zymM@R#kG9#Bfx4zJ@H_N@s9s<)LUTy!j_OZk~HAH>W|#3znOz@1)7EbF^S_EWd%M| z1k&34)*EKA@*GRkK;NA>U^FhjC72KLvut3QM6Bb~=u*0NBXOgGR{PCGD7~t0p`4XA z*!(O}bq|7&9;9{z+_d+c2g$t=`khZoJ_Hh_^fd{4$|} z5N`C6P1NTOzdAu(z4MJ$bL5wv*~3-w>Pod3o=CL|`e+e^Hl(&$sCrq@yGc0y6u-?( z-H2z~D9JS+VIfUE;@Uvx=5fAE?FuxJm->jMb5nEg-oNwhaO(0ug$8SW^Jn{O5;)Sr zP9Wu9?57lNzZ(8!YqVz(m{SHI4@P_8g*sv;u%^Eca1Bz>Znlz6lWUQC+y^6LM&$L3 zD^pT$un&9IG2D zR-~_pk7)1b)10VS&SDx%IWKv_?vP&zW_}~P$|&-<*wn?V7|8@ru|g?^qU=Icz81(;8XtRcnhdsrT0nckO|XOVaGuThw2o`BFg;%=6Lu7#SSt%p3bSS|6+?!k zZ4clp?jI@mxtv_9fs#lgH~IKyN#O(ssvg`0qQ{!V_vLozhD@tSkm{1q1xtuyS<~np ze-x$WYj1Hu0&rHqwG{K_Rx|vCP&jDX8RFZ3#r({IzG;?24%yL%Cqyzafb0gPU!|Zk z;6maDB%w2B8EC1?$w&2oYpDsn^g;FC}92RZ~?dBp}MK18+8x6c^G#&VhtF= z@p<1dGuf_+SiDo)@HbE2g!93#cI`B+VN#^lSnN$LU@Nt0Pz0}NKxdZZl31vJFcXpg zNcV93r5^j%PdO=KjK)ZLeeUav!l58;5D~Jj%uUE0uS5kFp%}jK&;7Fv%awb%nr?HX>M@LU(p> zqE-a>j>?kKq!Ft#R1w*2{XoFo#ViM;*&?TV3K>s3LB+SI%K#Hkt=8_08*0ng9>j-o>oNh%Z4oss zjutW&JsMtm1gU?{7;DGCq*BuS8rgS!QJ>n*!@3DRH!T{Nj?MtYrbOG&VVhr{+7H&q z-EqFE?hsC1d%F~GmWw(TueWy&ztsZZ>k9^!F?FGx?!>7?8|#_ZfxpSV#Qhf!UmIcO z15?xRY=X>y`wiA%!i!sN3vQN}Clf{E$bJF3rwP?Izp&q|T-NoU&tSjthqfx&l?fN>s5@EEQvPZit z;^L)!zO1a4#TRabVV1<8#V8K@>wvI-!vqGF*nB0w25V#JR>B>2iszEga&+r#)e3~p zRYTVt*v;5akNyMJ#*9F;&4TKwPhs>*x4lfz@_`|auP9b>`+A?y$(+x5O_M&ry>FW; zmp%B4ue^?z4y?6W@=hssGqrE%hQeQ9*)fbm(CJY86#1i?-(fweZ8nR zC{%gF%yC7LQ`+Xt+=q#m@g#7qNrjXkUcVsoYnMdm%k;0jywu-C0QQEBrxcK*)~9i{U&=Y{48^fPEtYTDe*M##jrTI&SY7Zyc5$z+s_U*i+<6Yd3$`HKU8r}+aJ+85Jni@uP>PU^TvhN*>3)OjmNVC?c;o3zgm+_+ppxY*@0BcWJye;4b-MGp^n z1g(=oAa_QLrDClrSpqVI`*VhU=IR3zb>#$8Bcg-3p3`)C)<;uUg2tJy9%5Gx3wDnd zUWxZ14|D^;$bjg1Zw01NaFqSs1<%yKyD$dMB?xSmckoAf<8amdt?jh!Hv$>7kqHJX z6P$R}x$bj}^c9b(EwJ&Tk~RGIi^NCTM*m za|Z4YeLWhT91-&asx!}BdRc{zD5cb_tkp!H(DTdSO8}B7t_an=RdUDBlX%$3RN*Z} ze1}DTf0iv@#x|$ctT@vNHoud9wAv;0T3P+uCXM1NzkF4@j5U4b`6m?BdFMA_kVR7A zq6iWO|J^om?VA$+0q+lu`dY6&6-bJ?uUvaVLEahNsZsZ9z4MJk>ho3QRkG3;FNN|n zB)d3L5kN5nI|Tb)h3o-sWxJQ_C_C{E^08L#G*j^f$FGs}S|LMhML{9A?XZ6@zVQ3| zQjVc#=6BaC-xIA?2MXT%L;ZZ+6}g9B7rXS0dr^w;oQ;bv>@<_Y`>rhGRq}(bMT=I0 z;-gR0eMyfWd|6N70uL)ghe~u;jY0zzO~dRTeSo_{cLcN5(dz)HuBUwUP!3VLcW7|R zMomLnknH4rvCWKSq+ccK%(8~h>@=s_Q?$UKWnD|*ZWR1>LYGfSW!?HaP z@ZCeTb~)p^UL$W$hh9xp@l9$JaoMUrMtd)}dbC3QFgGaCf|M?BswVyv%cl<$2~L9V z7l6W_SOUa~|AM-_tahJdPoo2z0x6*a1D0jlSbauM_v%1SVz*+SLr&?qA!*_Gm)6|)h zAf%J;;26j%*rBxL9s}m+b;~G|>q~6tyA&V4!m>r+b0?Gm*>T!^_zzK#FE9C$z)hz> zEB_~JM3!$f`_&c4YK>voRw%+#)Wglnus5}2DD89*xJI{^(!rr-v-b$P1wNq`8HWYV zQ@ZT6yvA!@6w1w}Zh8IX6~Q?2ps35^lisp!L%+E7(7%g0u>=p%jg3B|jW1=60!>RN z@n2rcW=v~~O$9NhPBp=EhyK+8KuB+Sh)zbg{NeuZ0Mxta@A;jl`-kJymW-z^m$=TK z_E*VU>1u@;zZrVi`>vzXPTjB5AZqc3;Bpz?3CCuM8yAM7KC4WBd4Gyiqf+T%BNx<+NSNlzT)-=*&PneDmp`PiwkWFe^~r7`vy!#pco8b zJJmUIO`gROy>)Gc-uNBu0D1N%+Cl<5w~~T;F=lS@7QG};1M!re5TRsd zTt~TldIrNrCEcz`DR@wRwP8EFD2Q612<|;aX#i*!8KIz3ei!?1B5(1!Q&gL{WDiMf zf{a?w;gURu@QK^{b$gS}9|D)Jdq-LG!^XnIdm&46QAdT%6M5Xu^cN&Wh$$1m{PMDW?QDh4BPf9)WbS)x z$MP75+L;~yW`gyRA7Y%*!8Ua#;Y}y>WIegJ(QVj+i4^+J?FdwyK*dODq!J|=m*uiF%Xe0nhi175kH7{-$nrj3cK z8s=ZFrkR3ssK3q)gqJcqL27NxV#+YP62Dz9r=Nh?_oetn#Q&Z(DW0Bc5onCs^kYC9 z^W(uT3+DI6iQ0B@1BNehtKiBgSEnW+om)165NoW4#LFxnBOh1j z`Cz~&v9;v;gxPH*2Ww`(53j&dZhyQuLX9)N7`#UPHO2$s661re0;n0!G?pRmI?HoP zLMhmkcByQm)!2QBak7R~xp#D_Y2is39QT~(2+bK~i8x}bi zbg`lMN* z?5B#6t#}_4UJ2$iWqx6ja(aubrXj+khw$eJo;JaXucuV~d|JSrlg`^k>)q>O=+ zIZ*%L^)odMNNv5WPlbA$-dfbQ3Ejq-R(tZl6`tykn?JFL>C-xQ23UQ_ik}P*?*ZM& zt<@-kbcwv^GSb7VTrj_YRGQ?IbWiihf!~)u0p*^8OL^oVo_-EOU$pl%0wG)mB~iuk zc>-)dx&B(nN^ZL4aGBdL4xm~cAL`Sm?k1*Px6qn# zo^vmMCf!VquTvz%YWOyr#RA~zs%HfXkYdjIPpO>$C}#bfDHe)(N>-(H2jG{RNMk*` zS#0vEM^Tvyb&)D)!9j1UujN8N=9**ROp6)ewq6$a6OMUL9q@uR0&~v~lWkN_O2kP# z81yaP@wExf@7~tbPYrn+OoQAy>p8U=y@$_Xs&cW6*h)bnUWn!lRg_mF;ZehMWDb0i zn|KUjbUbvu_rZ}B)&@bs zYNoSRReEJA77gq?gv6}0Ny=XzjSI?PKaV*^a$Cw%RroenOVmkj0Gd4pk?JGyzuvUj zVAAOIv5PUh9RC3E+P_IcC;egD3%Soi3&+_+2~fs7 zDoFazhe*Gtt83PVioT>h@R!cs7qqMN&)&k=HNNy-6Y~RU}b_=v%5JL*Cy;RRUky+(b9c?zK-DuAQs%a3%7p3q zI|Ztx8O8oH)+r7k_{Sm`tYGPclpyD@J^V^NuN4&igi!kF<#eW#8G>gV!P#fzO-*3*Id4Eu2_Ix?@WX(sT+uI{`acL+<{%p_SA@CeW^ zzb71{{ _('Table of Contents FOO') }} +

{{ _('Table of Contents') }}

{{ toc }} {%- endif %} From b5c8a4b23e1a2ccf7cdb084b07399b3f3a31af14 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Wed, 1 Jan 2020 13:27:31 +1300 Subject: [PATCH 2/7] wip node context menu and doc updates --- NodeGraphQt/__init__.py | 22 ++- NodeGraphQt/base/graph.py | 70 ++++++--- NodeGraphQt/base/menu.py | 179 ++++++++++++++-------- NodeGraphQt/base/{actions.py => utils.py} | 84 ++++++---- NodeGraphQt/errors.py | 1 + NodeGraphQt/widgets/actions.py | 57 +++++++ NodeGraphQt/widgets/viewer.py | 42 ++--- docs/menu.rst | 13 +- 8 files changed, 320 insertions(+), 148 deletions(-) rename NodeGraphQt/base/{actions.py => utils.py} (66%) create mode 100644 NodeGraphQt/widgets/actions.py diff --git a/NodeGraphQt/__init__.py b/NodeGraphQt/__init__.py index 601a8897..aa004d71 100644 --- a/NodeGraphQt/__init__.py +++ b/NodeGraphQt/__init__.py @@ -80,14 +80,14 @@ def __init__(self): '"NodeGraphQt.vendor.Qt ({})"'.format(qtpy_ver)) from .base.graph import NodeGraph -from .base.menu import Menu, MenuCommand +from .base.menu import NodesMenu, NodeGraphMenu, NodeGraphCommand from .base.node import NodeObject, BaseNode, BackdropNode from .base.port import Port from .pkg_info import __version__ as VERSION from .pkg_info import __license__ as LICENSE # functions -from .base.actions import setup_context_menu +from .base.utils import setup_context_menu # widgets from .widgets.node_tree import NodeTreeWidget @@ -95,7 +95,19 @@ def __init__(self): __version__ = VERSION __all__ = [ - 'BackdropNode', 'BaseNode', 'LICENSE', 'Menu', 'MenuCommand', 'NodeGraph', - 'NodeObject', 'NodeTreeWidget', 'Port', 'PropertiesBinWidget', 'VERSION', - 'constants', 'setup_context_menu' + 'BackdropNode', + 'BaseNode', + 'LICENSE', + 'NodeGraph', + 'NodeGraphCommand', + 'NodeGraphMenu', + 'NodeObject', + 'NodeTreeWidget', + 'NodesMenu', + 'Port', + 'PropertiesBinWidget', + 'VERSION', + 'constants', + 'setup_context_menu' ] + diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 760576e6..5d8335fb 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -10,7 +10,7 @@ NodeMovedCmd, PortConnectedCmd) from NodeGraphQt.base.factory import NodeFactory -from NodeGraphQt.base.menu import Menu +from NodeGraphQt.base.menu import NodeGraphMenu, NodesMenu from NodeGraphQt.base.model import NodeGraphModel from NodeGraphQt.base.node import NodeObject from NodeGraphQt.base.port import Port @@ -393,47 +393,73 @@ def context_menu(self): """ Returns the main context menu from the node graph. + Note: + This is a convenience function to + :meth:`NodeGraphQt.NodeGraph.get_context_menu` + with the arg ``menu="graph"`` + Returns: - Menu: context menu object. + NodeGraphQt.NodeGraphMenu: context menu object. + """ + return self.get_context_menu('graph') + + def context_nodes_menu(self): """ - return Menu(self._viewer, self._viewer.context_menus()['Main']) + Returns the main context menu for the nodes. - def get_context_menu(self, name='Main'): + Note: + This is a convenience function to + :meth:`NodeGraphQt.NodeGraph.get_context_menu` + with the arg ``menu="nodes"`` + + Returns: + NodeGraphQt.NodesMenu: context menu object. + """ + return self.get_context_menu('nodes') + + def get_context_menu(self, menu): """ Returns the context menu specified by the name. Menu types: - "Main" - context menu from the node graph. - "Node" - context menu from a Node. - "Port" - context menu from a Port. + "graph" - context menu from the node graph. + "nodes" - context menu for the nodes. Args: - name (str): menu name. + menu (str): menu name. Returns: - Menu: context menu object. + NodeGraphMenu or NodesMenu: context menu object. """ menus = self._viewer.context_menus() - if name not in menus: - return - return Menu(self._viewer, menus['Main']) + if menus.get(menu): + if menu == 'graph': + return NodeGraphMenu(self, menus[menu]) + elif menu == 'nodes': + return NodesMenu(self, menus[menu]) - def disable_context_menu(self, disabled=True, name='Main'): + def disable_context_menu(self, disabled=True, name='all'): """ - Disable/Enable the main context menu from the node graph. + Disable/Enable context menus from the node graph. - Menu types: - "Main" - context menu from the node graph. - "Node" - context menu from a Node. - "Port" - context menu from a Port. + Menu Types: + - ``"all"`` all context menus from the node graph. + - ``"graph"`` context menu from the node graph. + - ``"nodes"`` context menu for the nodes. Args: disabled (bool): true to enable context menu. - name (str): menu name. (default: Main) + name (str): menu name. (default: ``"all"``) """ - menu = self._viewer.context_menus()[name] - menu.setDisabled(disabled) - menu.setVisible(not disabled) + if name == 'all': + for k, menu in self._viewer.context_menus().items(): + menu.setDisabled(disabled) + menu.setVisible(not disabled) + return + menus = self._viewer.context_menus() + if menus.get(name): + menus[name].setDisabled(disabled) + menus[name].setVisible(not disabled) def acyclic(self): """ diff --git a/NodeGraphQt/base/menu.py b/NodeGraphQt/base/menu.py index a2b92b7e..c9fbbf54 100644 --- a/NodeGraphQt/base/menu.py +++ b/NodeGraphQt/base/menu.py @@ -1,27 +1,35 @@ #!/usr/bin/python from distutils.version import LooseVersion -from NodeGraphQt import QtGui, QtCore, QtWidgets +from NodeGraphQt import QtGui, QtCore from NodeGraphQt.errors import NodeMenuError -from NodeGraphQt.widgets.stylesheet import STYLE_QMENU +from NodeGraphQt.widgets.actions import BaseMenu, GraphAction, NodeAction -class Menu(object): +class NodeGraphMenu(object): """ - base class for a menu item. + The ``NodeGraphMenu`` is the context menu triggered from the node graph. + + This class is an abstraction layer for the QMenu object. """ - def __init__(self, viewer, qmenu): - self.__viewer = viewer - self.__qmenu = qmenu + def __init__(self, graph, qmenu): + self._graph = graph + self._qmenu = qmenu def __repr__(self): - cls_name = self.__class__.__name__ - return '<{}("{}") object at {}>'.format(cls_name, self.name(), hex(id(self))) + return '<{}("{}") object at {}>'.format( + self.__class__.__name__, self.name(), hex(id(self))) @property def qmenu(self): - return self.__qmenu + """ + The underlying qmenu. + + Returns: + BaseMenu: qmenu object. + """ + return self._qmenu def name(self): """ @@ -32,15 +40,6 @@ def name(self): """ return self.qmenu.title() - def set_name(self, name): - """ - Set the name for the menu. - - Args: - name (str): label name. - """ - self.qmenu.setTitle(name) - def get_menu(self, name): """ Returns the child menu by name. @@ -49,13 +48,13 @@ def get_menu(self, name): name (str): name of the menu. Returns: - NodeGraphQt.Menu: menu item. + NodeGraphQt.NodeGraphMenu: menu item. """ - for action in self.qmenu.actions(): - if action.menu() and action.menu().title() == name: - return Menu(self.__viewer, action.menu()) + menu = self.qmenu.get_menu(name) + if menu: + return NodeGraphMenu(self._graph, menu) - def get_action(self, name): + def get_command(self, name): """ Returns the child menu command by name. @@ -67,7 +66,7 @@ def get_action(self, name): """ for action in self.qmenu.actions(): if not action.menu() and action.text() == name: - return MenuAction(self.__viewer, action) + return NodeGraphCommand(self._graph, action) def all_commands(self): """ @@ -86,88 +85,128 @@ def get_actions(menu): actions += get_actions(action.menu()) return actions child_actions = get_actions(self.qmenu) - return [MenuCommand(self.__viewer, a) for a in child_actions] + return [NodeGraphCommand(self._graph, a) for a in child_actions] - def create_menu(self, name): + def add_menu(self, name): """ - Create a child menu to the current menu. + Adds a child menu to the current menu. Args: name (str): menu name. Returns: - NodeGraphQt.Menu: the appended menu item. + NodeGraphQt.NodeGraphMenu: the appended menu item. """ - menu = QtWidgets.QMenu(name, self.__viewer) + menu = BaseMenu(name, self.qmenu) self.qmenu.addMenu(menu) - return Menu(self.__viewer, menu) + return NodeGraphMenu(self._graph, menu) - def create_action(self, name): + def add_command(self, name, func=None, shortcut=None): """ - Create a command to the menu. + Adds a command to the menu. Args: - name (NodeGraphQt.MenuCommand): menu command. - + name (str): command name. + func (function): command function eg. "func(``graph``)". + shortcut (str): shotcut key. + Returns: + NodeGraphQt.MenuCommand: the appended command. """ - action = QtWidgets.QAction(name, self.__viewer) + action = GraphAction(name, self._graph.viewer()) + action.graph = self._graph + if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): + action.setShortcutVisibleInContextMenu(True) + if shortcut: + action.setShortcut(shortcut) + if func: + action.executed.connect(func) qaction = self.qmenu.addAction(action) - return MenuAction(self.__viewer, qaction) + return NodeGraphCommand(self._graph, qaction) - def create_separator(self): + def add_separator(self): """ - Create a separator to the menu. + Adds a separator to the menu. """ self.qmenu.addSeparator() -class MenuAction(QtWidgets.QAction): +class NodesMenu(NodeGraphMenu): """ - base class for a menu command. + The ``NodesMenu`` is the context menu triggered from the nodes. + + This class is an abstraction layer for the QMenu object. + + **Inherited from:** :class:`NodeGraphQt.NodeGraphMenu` """ - #: signal emits when the action has triggered in the node graph. - executed = QtCore.QSignal(object) + def add_command(self, name, func=None, shortcut=None, node_type=None): + """ + Re-implemented to add a command to the specified node type menu. + + Args: + name (str): command name. + func (function): command function eg. "func(``graph``, ``node``)". + shortcut (str): shotcut key. + node_type (str): specified node type for the command. + + Returns: + NodeGraphQt.MenuCommand: the appended command. + """ + if not node_type: + raise NodeMenuError('Node type not specified!') + + node_menu = self.qmenu.get_menu(node_type) + if not node_menu: + node_menu = BaseMenu(node_type, self.qmenu) + self.qmenu.addMenu(node_menu) - def __init__(self, parent=None, viewer=None, name=''): - super(MenuAction, self).__init__(parent) - self.__viewer = viewer + if not self.qmenu.isEnabled(): + self.qmenu.setDisabled(False) + + action = NodeAction(name, self._graph.viewer()) + action.graph = self._graph if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): - self.setShortcutVisibleInContextMenu(True) - self.setText(name) - self.triggered.connect(self._on_triggered) + action.setShortcutVisibleInContextMenu(True) + if shortcut: + action.setShortcut(shortcut) + if func: + action.executed.connect(func) + qaction = node_menu.addAction(action) + return NodeGraphCommand(self._graph, qaction) - def __repr__(self): - cls_name = self.__class__.__name__ - return 'NodeGraphQt.{}(\'{}\')'.format(cls_name, self.name()) - def _on_triggered(self): - menu = self.menu() - pos = self.mapToScene(menu.pos()) +class NodeGraphCommand(object): + """ + base class for a menu command. + """ - items = self.__viewer._items_near(pos) - + def __init__(self, graph, qaction): + self._graph = graph + self._qaction = qaction - self.triggered.emit() + def __repr__(self): + return '<{}("{}") object at {}>'.format( + self.__class__.__name__, self.name(), hex(id(self))) - def name(self): + @property + def qaction(self): """ - Returns the name for the menu command. + The underlying qaction. Returns: - str: label name. + BaseAction: qaction object. """ - return self.text() + return self._qaction - def set_name(self, name): + def name(self): """ - Set the name for the menu command. + Returns the name for the menu command. - Args: - name (str): label name. + Returns: + str: label name. """ - self.setText(name) + return self.qaction.text() def set_shortcut(self, shortcut=None): """ @@ -178,3 +217,9 @@ def set_shortcut(self, shortcut=None): """ shortcut = shortcut or QtGui.QKeySequence() self.qaction.setShortcut(shortcut) + + def run_command(self): + """ + execute the menu command. + """ + self.qaction.trigger() diff --git a/NodeGraphQt/base/actions.py b/NodeGraphQt/base/utils.py similarity index 66% rename from NodeGraphQt/base/actions.py rename to NodeGraphQt/base/utils.py index 1284a39d..355254b0 100644 --- a/NodeGraphQt/base/actions.py +++ b/NodeGraphQt/base/utils.py @@ -11,28 +11,22 @@ def setup_context_menu(graph): Args: graph (NodeGraphQt.NodeGraph): node graph. """ - root_menu = graph.context_menu() + root_menu = graph.get_context_menu('graph') file_menu = root_menu.add_menu('&File') edit_menu = root_menu.add_menu('&Edit') # create "File" menu. - file_menu.add_command('Open...', - lambda: _open_session(graph), - QtGui.QKeySequence.Open) - file_menu.add_command('Save...', - lambda: _save_session(graph), - QtGui.QKeySequence.Save) - file_menu.add_command('Save As...', - lambda: _save_session_as(graph), - 'Ctrl+Shift+s') - file_menu.add_command('Clear', lambda: _clear_session(graph)) + file_menu.add_command('Open...', _open_session, QtGui.QKeySequence.Open) + file_menu.add_command('Save...', _save_session, QtGui.QKeySequence.Save) + file_menu.add_command('Save As...', _save_session_as, 'Ctrl+Shift+s') + file_menu.add_command('Clear', _clear_session) file_menu.add_separator() - file_menu.add_command('Zoom In', lambda: _zoom_in(graph), '=') - file_menu.add_command('Zoom Out', lambda: _zoom_out(graph), '-') - file_menu.add_command('Reset Zoom', graph.reset_zoom, 'h') + file_menu.add_command('Zoom In', _zoom_in, '=') + file_menu.add_command('Zoom Out', _zoom_out, '-') + file_menu.add_command('Reset Zoom', _reset_zoom, 'h') # create "Edit" menu. undo_actn = graph.undo_stack().createUndoAction(graph.viewer(), '&Undo') @@ -48,29 +42,21 @@ def setup_context_menu(graph): edit_menu.qmenu.addAction(redo_actn) edit_menu.add_separator() - edit_menu.add_command('Clear Undo History', lambda: _clear_undo(graph)) + edit_menu.add_command('Clear Undo History', _clear_undo) edit_menu.add_separator() - edit_menu.add_command('Copy', graph.copy_nodes, QtGui.QKeySequence.Copy) - edit_menu.add_command('Paste', graph.paste_nodes, QtGui.QKeySequence.Paste) - edit_menu.add_command('Delete', - lambda: graph.delete_nodes(graph.selected_nodes()), - QtGui.QKeySequence.Delete) + edit_menu.add_command('Copy', _copy_nodes, QtGui.QKeySequence.Copy) + edit_menu.add_command('Paste', _paste_nodes, QtGui.QKeySequence.Paste) + edit_menu.add_command('Delete', _delete_nodes, QtGui.QKeySequence.Delete) edit_menu.add_separator() - edit_menu.add_command('Select all', graph.select_all, 'Ctrl+A') - edit_menu.add_command('Deselect all', graph.clear_selection, 'Ctrl+Shift+A') - edit_menu.add_command('Enable/Disable', - lambda: graph.disable_nodes(graph.selected_nodes()), - 'd') + edit_menu.add_command('Select all', _select_all_nodes, 'Ctrl+A') + edit_menu.add_command('Deselect all', _clear_node_selection, 'Ctrl+Shift+A') + edit_menu.add_command('Enable/Disable', _disable_nodes, 'd') - edit_menu.add_command('Duplicate', - lambda: graph.duplicate_nodes(graph.selected_nodes()), - 'Alt+c') - edit_menu.add_command('Center Selection', - graph.fit_to_selection, - 'f') + edit_menu.add_command('Duplicate', _duplicate_nodes, 'Alt+c') + edit_menu.add_command('Center Selection', _fit_to_selection, 'f') edit_menu.add_separator() @@ -100,6 +86,10 @@ def _zoom_out(graph): graph.set_zoom(zoom) +def _reset_zoom(graph): + graph.reset_zoom() + + def _open_session(graph): """ Prompts a file open dialog to load a session. @@ -168,3 +158,35 @@ def _clear_undo(graph): msg = 'Clear all undo history, Are you sure?' if viewer.question_dialog('Clear Undo History', msg): graph.undo_stack().clear() + + +def _copy_nodes(graph): + graph.copy_nodes() + + +def _paste_nodes(graph): + graph.paste_nodes() + + +def _delete_nodes(graph): + graph.delete_nodes(graph.selected_nodes()) + + +def _select_all_nodes(graph): + graph.select_all() + + +def _clear_node_selection(graph): + graph.clear_selection() + + +def _disable_nodes(graph): + graph.disable_nodes(graph.selected_nodes()) + + +def _duplicate_nodes(graph): + graph.duplicate_nodes(graph.selected_nodes()) + + +def _fit_to_selection(graph): + graph.fit_to_selection() diff --git a/NodeGraphQt/errors.py b/NodeGraphQt/errors.py index db30bd5d..e88e5956 100644 --- a/NodeGraphQt/errors.py +++ b/NodeGraphQt/errors.py @@ -1,5 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +class NodeMenuError(Exception): pass class NodePropertyError(Exception): pass class NodeWidgetError(Exception): pass class NodeRegistrationError(Exception): pass diff --git a/NodeGraphQt/widgets/actions.py b/NodeGraphQt/widgets/actions.py new file mode 100644 index 00000000..16ee562f --- /dev/null +++ b/NodeGraphQt/widgets/actions.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +from NodeGraphQt import QtCore, QtWidgets +from NodeGraphQt.widgets.stylesheet import STYLE_QMENU + + +class BaseMenu(QtWidgets.QMenu): + + def __init__(self, *args, **kwargs): + super(BaseMenu, self).__init__(*args, **kwargs) + self.setStyleSheet(STYLE_QMENU) + + # def hideEvent(self, event): + # super(BaseMenu, self).hideEvent(event) + # print('foo') + + def get_menu(self, name): + for action in self.actions(): + if action.menu() and action.menu().title() == name: + return action.menu() + + +class GraphAction(QtWidgets.QAction): + + executed = QtCore.Signal(object) + + def __init__(self, *args, **kwargs): + super(GraphAction, self).__init__(*args, **kwargs) + self.graph = None + self.triggered.connect(self._on_triggered) + + def _on_triggered(self): + self.executed.emit(self.graph) + + def get_action(self, name): + for action in self.qmenu.actions(): + if not action.menu() and action.text() == name: + return action + + +class NodeAction(QtWidgets.QAction): + + executed = QtCore.Signal(object, object) + + def __init__(self, *args, **kwargs): + super(NodeAction, self).__init__(*args, **kwargs) + self.graph = None + self.node_id = None + self.triggered.connect(self._on_triggered) + + def _on_triggered(self): + node = self.graph.get_node_by_id(self.node_id) + self.executed.emit(self.graph, node) + + def get_action(self, name): + for action in self.qmenu.actions(): + if not action.menu() and action.text() == name: + return action diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 1731ff18..66f34f3a 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import os import math +import os from NodeGraphQt import QtGui, QtCore, QtWidgets from NodeGraphQt.constants import (IN_PORT, OUT_PORT, @@ -12,8 +12,8 @@ from NodeGraphQt.qgraphics.pipe import Pipe, LivePipe from NodeGraphQt.qgraphics.port import PortItem from NodeGraphQt.qgraphics.slicer import SlicerPipe +from NodeGraphQt.widgets.actions import BaseMenu from NodeGraphQt.widgets.scene import NodeScene -from NodeGraphQt.widgets.stylesheet import STYLE_QMENU from NodeGraphQt.widgets.tab_search import TabSearchWidget ZOOM_MIN = -0.95 @@ -81,12 +81,12 @@ def __init__(self, parent=None): # shortcuts don't work with "setVisibility(False)". menu_bar.resize(0, 0) - self._context_menus = {} - for name in ['Main', 'Node', 'Port']: - menu = QtWidgets.QMenu(name, self) - menu.setStyleSheet(STYLE_QMENU) - self._context_menus[name] = menu - menu_bar.addMenu(menu) + self._ctx_menu = BaseMenu('NodeGraph', self) + self._ctx_node_menu = BaseMenu('Nodes', self) + menu_bar.addMenu(self._ctx_menu) + menu_bar.addMenu(self._ctx_node_menu) + + self._ctx_node_menu.setDisabled(True) self.acyclic = True self.LMB_state = False @@ -157,17 +157,20 @@ def resizeEvent(self, event): def contextMenuEvent(self, event): self.RMB_state = False + ctx_menu = self._ctx_menu - pos = self.mapToScene(self._previous_pos) - items = self._items_near(pos) - ports = [i for i in items if isinstance(i, PortItem)] - nodes = [i for i in items if isinstance(i, AbstractNodeItem)] - if self._context_menus['Port'].isEnabled() and ports: - self._context_menus['Port'].exec_(event.globalPos()) - elif self._context_menus['Node'].isEnabled() and nodes: - self._context_menus['Node'].exec_(event.globalPos()) - elif self._context_menus['Main'].isEnabled(): - self._context_menus['Main'].exec_(event.globalPos()) + if self._ctx_node_menu.isEnabled(): + pos = self.mapToScene(self._previous_pos) + items = self._items_near(pos) + nodes = [i for i in items if isinstance(i, AbstractNodeItem)] + # if nodes: + # node = nodes[0] + # ctx_menu = self._ctx_node_menu.get_menu(node.type_) + # for action in ctx_menu.actions(): + # if not action.menu(): + # action.node_id = node.id + if ctx_menu and ctx_menu.isEnabled(): + ctx_menu.exec_(event.globalPos()) else: return super(NodeViewer, self).contextMenuEvent(event) @@ -603,7 +606,8 @@ def tab_search_toggle(self): self.clearFocus() def context_menus(self): - return self._context_menus + return {'graph': self._ctx_menu, + 'nodes': self._ctx_node_menu} def question_dialog(self, text, title='Node Graph'): dlg = QtWidgets.QMessageBox.question( diff --git a/docs/menu.rst b/docs/menu.rst index 577f280f..0610b01c 100644 --- a/docs/menu.rst +++ b/docs/menu.rst @@ -52,15 +52,20 @@ add "Bar" command to the "Foo" menu. -Menu -**** +Graph Menus +*********** Node graph menu. ---- -.. autoclass:: NodeGraphQt.Menu +.. autoclass:: NodeGraphQt.NodeGraphMenu + :members: + +---- + +.. autoclass:: NodeGraphQt.NodesMenu :members: @@ -71,5 +76,5 @@ Node graph menu command. ---- -.. autoclass:: NodeGraphQt.MenuCommand +.. autoclass:: NodeGraphQt.NodeGraphCommand :members: From b0a69b91df646327598191990ed00f7f3d709ea5 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Thu, 2 Jan 2020 01:32:34 +1300 Subject: [PATCH 3/7] node menu tweak. --- NodeGraphQt/widgets/actions.py | 8 +++++--- NodeGraphQt/widgets/viewer.py | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/NodeGraphQt/widgets/actions.py b/NodeGraphQt/widgets/actions.py index 16ee562f..a435193f 100644 --- a/NodeGraphQt/widgets/actions.py +++ b/NodeGraphQt/widgets/actions.py @@ -9,9 +9,11 @@ def __init__(self, *args, **kwargs): super(BaseMenu, self).__init__(*args, **kwargs) self.setStyleSheet(STYLE_QMENU) - # def hideEvent(self, event): - # super(BaseMenu, self).hideEvent(event) - # print('foo') + def hideEvent(self, event): + super(BaseMenu, self).hideEvent(event) + for a in self.actions(): + if hasattr(a, 'node_id'): + a.node_id = None def get_menu(self, name): for action in self.actions(): diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 66f34f3a..ff3758b5 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -163,12 +163,12 @@ def contextMenuEvent(self, event): pos = self.mapToScene(self._previous_pos) items = self._items_near(pos) nodes = [i for i in items if isinstance(i, AbstractNodeItem)] - # if nodes: - # node = nodes[0] - # ctx_menu = self._ctx_node_menu.get_menu(node.type_) - # for action in ctx_menu.actions(): - # if not action.menu(): - # action.node_id = node.id + if nodes: + node = nodes[0] + ctx_menu = self._ctx_node_menu.get_menu(node.type_) + for action in ctx_menu.actions(): + if not action.menu(): + action.node_id = node.id if ctx_menu and ctx_menu.isEnabled(): ctx_menu.exec_(event.globalPos()) else: From 497b4a293dd04f97b504f0e10237a5b40febe71d Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Thu, 2 Jan 2020 13:40:56 +1300 Subject: [PATCH 4/7] docs update. --- NodeGraphQt/base/menu.py | 57 ++++++++++++++++++++++++++++++++------ NodeGraphQt/base/utils.py | 8 ++++++ docs/menu.rst | 58 +++++++++++++++------------------------ 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/NodeGraphQt/base/menu.py b/NodeGraphQt/base/menu.py index c9fbbf54..440d31b4 100644 --- a/NodeGraphQt/base/menu.py +++ b/NodeGraphQt/base/menu.py @@ -10,7 +10,17 @@ class NodeGraphMenu(object): """ The ``NodeGraphMenu`` is the context menu triggered from the node graph. - This class is an abstraction layer for the QMenu object. + example to accessing the node graph context menu. + + .. code-block:: python + :linenos: + + from NodeGraphQt import NodeGraph + + node_graph = NodeGraph() + + # get the context menu for the node graph. + context_menu = node_graph.get_context_menu('graph') """ def __init__(self, graph, qmenu): @@ -135,19 +145,52 @@ class NodesMenu(NodeGraphMenu): """ The ``NodesMenu`` is the context menu triggered from the nodes. - This class is an abstraction layer for the QMenu object. - **Inherited from:** :class:`NodeGraphQt.NodeGraphMenu` + + example for adding a command to the nodes context menu. + + .. code-block:: python + :linenos: + + from NodeGraphQt import BaseNode, NodeGraph + + # example node. + class MyNode(BaseNode): + + __identifier__ = 'com.chantasticvfx' + NODE_NAME = 'my node' + + def __init__(self): + super(MyNode, self).__init__() + self.add_input('in') + self.add_output('out') + + # create node graph. + node_graph = NodeGraph() + + # register example node. + node_graph.register_node(MyNode) + + # get the context menu for the nodes. + nodes_menu = node_graph.get_context_menu('nodes') + + # create a command + def test_func(graph, node): + print('Clicked on node: {}'.format(node.name())) + + nodes_menu.add_command('test', + func=test_func, + node_type='com.chantasticvfx.MyNode') + """ - def add_command(self, name, func=None, shortcut=None, node_type=None): + def add_command(self, name, func=None, node_type=None): """ Re-implemented to add a command to the specified node type menu. Args: name (str): command name. func (function): command function eg. "func(``graph``, ``node``)". - shortcut (str): shotcut key. node_type (str): specified node type for the command. Returns: @@ -168,8 +211,6 @@ def add_command(self, name, func=None, shortcut=None, node_type=None): action.graph = self._graph if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): action.setShortcutVisibleInContextMenu(True) - if shortcut: - action.setShortcut(shortcut) if func: action.executed.connect(func) qaction = node_menu.addAction(action) @@ -178,7 +219,7 @@ def add_command(self, name, func=None, shortcut=None, node_type=None): class NodeGraphCommand(object): """ - base class for a menu command. + Node graph menu command. """ def __init__(self, graph, qaction): diff --git a/NodeGraphQt/base/utils.py b/NodeGraphQt/base/utils.py index 355254b0..2d384f64 100644 --- a/NodeGraphQt/base/utils.py +++ b/NodeGraphQt/base/utils.py @@ -8,6 +8,14 @@ def setup_context_menu(graph): """ Sets up the node graphs context menu with some basic menus and commands. + .. code-block:: python + :linenos: + + from NodeGraphQt import NodeGraph, setup_context_menu + + graph = NodeGraph() + setup_context_menu(graph) + Args: graph (NodeGraphQt.NodeGraph): node graph. """ diff --git a/docs/menu.rst b/docs/menu.rst index 0610b01c..6c2f6540 100644 --- a/docs/menu.rst +++ b/docs/menu.rst @@ -4,66 +4,56 @@ Menus .. image:: _images/menu_hotkeys.png :width: 50% -The ``NodeGraphQt.setup_context_menu`` has a built in function that'll populate the node graphs context menu a few -default menus and commands. -.. code-block:: python - :linenos: - - from NodeGraphQt import NodeGraph, setup_context_menu - - graph = NodeGraph() - setup_context_menu(graph) - -example adding "Foo" menu to the node graphs context menu. +Here's an example where we add a ``"Foo"`` menu and then a ``"Bar"`` command with +the function ``my_test()`` registered. .. code-block:: python :linenos: from NodeGraphQt import NodeGraph + # test function. + def my_test(graph): + selected_nodes = graph.selected_nodes() + print('Number of nodes selected: {}'.format(len(selected_nodes))) + # create node graph. graph = NodeGraph() # get the main context menu. - root_menu = graph.context_menu() + context_menu = node_graph.get_context_menu('graph') # add a menu called "Foo". - foo_menu = root_menu.add_menu('Foo') - -add "Bar" command to the "Foo" menu. - -.. code-block:: python - :linenos: - :lineno-start: 11 - - # test function. - def my_test(): - print('Hello World') + foo_menu = context_menu.add_menu('Foo') # add "Bar" command to the "Foo" menu. + # we also assign a short cut key "Shift+t" for this example. foo_menu.add_command('Bar', my_test, 'Shift+t') ---- -.. autofunction:: NodeGraphQt.setup_context_menu - :noindex: - - +The ``NodeGraphQt.setup_context_menu`` is a built in function that'll populate +the node graphs context menu a few default menus and commands. -Graph Menus -*********** +.. autofunction:: NodeGraphQt.setup_context_menu + :noindex: -Node graph menu. +NodeGraph Menu +************** ----- +The context menu triggered from the node graph. .. autoclass:: NodeGraphQt.NodeGraphMenu :members: ----- + +Nodes Menu +********** + +The context menu triggered from a node. .. autoclass:: NodeGraphQt.NodesMenu :members: @@ -72,9 +62,5 @@ Node graph menu. Command ******* -Node graph menu command. - ----- - .. autoclass:: NodeGraphQt.NodeGraphCommand :members: From 2c12085ef223c6b73f3a758e49dd41a7819452ed Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Thu, 2 Jan 2020 14:41:18 +1300 Subject: [PATCH 5/7] clean up --- NodeGraphQt/widgets/actions.py | 4 +--- NodeGraphQt/widgets/viewer.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/NodeGraphQt/widgets/actions.py b/NodeGraphQt/widgets/actions.py index a435193f..d1c1e6fd 100644 --- a/NodeGraphQt/widgets/actions.py +++ b/NodeGraphQt/widgets/actions.py @@ -39,15 +39,13 @@ def get_action(self, name): return action -class NodeAction(QtWidgets.QAction): +class NodeAction(GraphAction): executed = QtCore.Signal(object, object) def __init__(self, *args, **kwargs): super(NodeAction, self).__init__(*args, **kwargs) - self.graph = None self.node_id = None - self.triggered.connect(self._on_triggered) def _on_triggered(self): node = self.graph.get_node_by_id(self.node_id) diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index ff3758b5..bd254244 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -83,10 +83,13 @@ def __init__(self, parent=None): self._ctx_menu = BaseMenu('NodeGraph', self) self._ctx_node_menu = BaseMenu('Nodes', self) + self._ctx_port_menu = BaseMenu('Ports', self) menu_bar.addMenu(self._ctx_menu) menu_bar.addMenu(self._ctx_node_menu) + menu_bar.addMenu(self._ctx_port_menu) self._ctx_node_menu.setDisabled(True) + self._ctx_port_menu.setDisabled(True) self.acyclic = True self.LMB_state = False @@ -157,7 +160,7 @@ def resizeEvent(self, event): def contextMenuEvent(self, event): self.RMB_state = False - ctx_menu = self._ctx_menu + ctx_menu = None if self._ctx_node_menu.isEnabled(): pos = self.mapToScene(self._previous_pos) @@ -166,10 +169,13 @@ def contextMenuEvent(self, event): if nodes: node = nodes[0] ctx_menu = self._ctx_node_menu.get_menu(node.type_) - for action in ctx_menu.actions(): - if not action.menu(): - action.node_id = node.id - if ctx_menu and ctx_menu.isEnabled(): + if ctx_menu: + for action in ctx_menu.actions(): + if not action.menu(): + action.node_id = node.id + + ctx_menu = ctx_menu or self._ctx_menu + if ctx_menu.isEnabled(): ctx_menu.exec_(event.globalPos()) else: return super(NodeViewer, self).contextMenuEvent(event) From 5f49672ddd4bfb867ef73cd3bf679a9b586706e0 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Thu, 2 Jan 2020 14:42:55 +1300 Subject: [PATCH 6/7] title fix --- docs/menu.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/menu.rst b/docs/menu.rst index 6c2f6540..e0ca7463 100644 --- a/docs/menu.rst +++ b/docs/menu.rst @@ -41,8 +41,8 @@ the node graphs context menu a few default menus and commands. :noindex: -NodeGraph Menu -************** +Graph Menu +********** The context menu triggered from the node graph. From c6be6b8353ba3cf1b4e12280016e956522bd60e5 Mon Sep 17 00:00:00 2001 From: jchanvfx Date: Thu, 2 Jan 2020 14:46:10 +1300 Subject: [PATCH 7/7] removed unused port menu --- NodeGraphQt/widgets/viewer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index bd254244..e2381c01 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -83,13 +83,10 @@ def __init__(self, parent=None): self._ctx_menu = BaseMenu('NodeGraph', self) self._ctx_node_menu = BaseMenu('Nodes', self) - self._ctx_port_menu = BaseMenu('Ports', self) menu_bar.addMenu(self._ctx_menu) menu_bar.addMenu(self._ctx_node_menu) - menu_bar.addMenu(self._ctx_port_menu) self._ctx_node_menu.setDisabled(True) - self._ctx_port_menu.setDisabled(True) self.acyclic = True self.LMB_state = False