From 2daaa02c3cd543b2561e64ed66d03e4f9c22a771 Mon Sep 17 00:00:00 2001 From: Alex Bautista Date: Fri, 2 Jan 2026 21:34:23 -0600 Subject: [PATCH] Initial commit: ComfyUI WatchPoint node --- README.md | 188 ++++++ __init__.py | 18 + __pycache__/__init__.cpython-312.pyc | Bin 0 -> 894 bytes __pycache__/watch_point.cpython-312.pyc | Bin 0 -> 39930 bytes js/watchpoint-floating.js | 633 ++++++++++++++++++++ watch_point.py | 756 ++++++++++++++++++++++++ watchpoint_settings.json | 10 + 7 files changed, 1605 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/watch_point.cpython-312.pyc create mode 100644 js/watchpoint-floating.js create mode 100644 watch_point.py create mode 100644 watchpoint_settings.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae74bf5 --- /dev/null +++ b/README.md @@ -0,0 +1,188 @@ +# 👁️ ComfyUI Watch Point + +**Dual preview system for ComfyUI workflows** + +Watch Point provides two simultaneous preview methods: +- 🖥️ **Monitor Preview**: External Tkinter window on any monitor +- 🖼️ **Floating Preview**: ComfyUI's built-in preview system + +## Features + +### Monitor Preview (Tkinter) +- 🎯 Display on any connected monitor +- 🔍 Zoom/Pan controls with mouse +- ⌨️ Keyboard shortcuts +- 💾 Save images directly from preview +- 📋 Copy to clipboard +- ⚙️ Customizable settings + +### Floating Preview (JavaScript) +- 📌 Uses ComfyUI's native preview system +- 🎨 Works with existing preview extensions +- 🔄 Auto-updates during workflow execution + +## Installation + +1. Clone into your ComfyUI custom_nodes directory: +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/yourusername/ComfyUI-WatchPoint.git +``` + +2. Install dependencies (if needed): +```bash +pip install pillow screeninfo +``` + +3. Restart ComfyUI + +## Usage + +1. Add **Watch Point** node to your workflow +2. Connect image input from any node +3. Enable/disable previews: + - `floating_preview`: Enable ComfyUI's built-in preview + - `monitor_preview`: Enable external monitor window +4. Select target monitor from dropdown +5. Connect output to next node (pass-through) + +### Example Workflow +``` +[Load Image] → [Watch Point] → [Upscale] → [Save Image] + ↓ + (Dual Preview) +``` + +## Controls (Monitor Preview) + +### Mouse +- **Scroll wheel**: Zoom in/out +- **Left click + drag**: Pan image +- **Right click**: Context menu + +### Keyboard +- **R**: Reset zoom and pan +- **T**: Toggle toolbar +- **1**: Zoom 1:1 (100% actual size) +- **ESC**: Close window + +### Toolbar Buttons +- **↻ Reset**: Reset zoom and pan +- **⊕ Zoom In**: Increase zoom level +- **⊖ Zoom Out**: Decrease zoom level +- **1:1 (100%)**: View image at actual size +- **Window Size**: Quick resize presets +- **⛶ Fullscreen**: Toggle fullscreen mode +- **⚙ Settings**: Open settings dialog + +## Settings + +Access settings via the **⚙ Settings** button in the toolbar. + +### Window Size +- Fixed sizes: 800x600, 1024x768, 1280x720, 1920x1080 +- Dynamic sizes: + - **Half Vertical**: Half screen width, full height + - **Half Horizontal**: Full width, half screen height + - **Quarter**: Half width, half height + +### Window Position +- Set exact X, Y coordinates for window placement +- Leave empty for automatic positioning +- Example: X=0, Y=0 for top-left corner + +### Save Options +- Default format: PNG, JPEG, or WebP +- JPEG quality slider (10-100) + +### UI Options +- Show/hide toolbar by default + +All settings are saved to `watchpoint_settings.json` + +## Configuration File + +Edit `watchpoint_settings.json` to customize defaults: + +```json +{ + "window_width": 800, + "window_height": 600, + "window_x": null, + "window_y": null, + "window_size_mode": "fixed", + "show_toolbar": true, + "save_format": "png", + "jpeg_quality": 90 +} +``` + +### Settings Explained +- `window_x`, `window_y`: Window position (null = auto) +- `window_size_mode`: "fixed", "Half Vertical", "Half Horizontal", or "Quarter" +- `show_toolbar`: Show toolbar on startup +- `save_format`: Default save format ("png", "jpeg", "webp") +- `jpeg_quality`: JPEG compression quality (10-100) + +## Use Cases + +### Dual Monitor Setup +Perfect for users with multiple monitors: +- Work on ComfyUI on main monitor +- View live preview on secondary monitor +- No need to switch windows or tabs + +### Quality Control +Use 1:1 zoom to inspect: +- Image quality at pixel level +- Upscaling artifacts +- Detail preservation + +### Live Monitoring +Watch your workflow progress in real-time: +- Both previews update simultaneously +- Choose the view that works best for your setup +- Keep an eye on intermediate results + +## Requirements + +- Python 3.7+ +- tkinter (usually included with Python) +- PIL/Pillow +- screeninfo (optional, for multi-monitor detection) + +## Optional Dependencies + +For clipboard support (Windows): +```bash +pip install pywin32 +``` + +## Troubleshooting + +### Window doesn't appear +- Check if `monitor_preview` is enabled (True) +- Try different monitor selection +- Check console for error messages + +### Window appears in wrong location +- Set explicit X, Y coordinates in settings +- Use `window_x: 0, window_y: 0` for top-left + +### Image not updating +- Ensure workflow is executing +- Check that image is connected to Watch Point input +- Try disabling/re-enabling monitor preview + +### Floating preview not working +- Ensure `floating_preview` is enabled (True) +- Check if other preview extensions are conflicting +- Restart ComfyUI + +## Credits + +Created for ComfyUI by [Your Name] + +## License + +MIT License - Feel free to use and modify! \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e9d0a08 --- /dev/null +++ b/__init__.py @@ -0,0 +1,18 @@ +""" +ComfyUI Watch Point +Dual preview system: External monitor (Tkinter) + Floating preview (JavaScript) +""" + +from .watch_point import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +# Indicate that we have JavaScript files +WEB_DIRECTORY = "js" + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] + +print("✅ Watch Point Extension: Loaded") +print(" 👁️ Dual Preview System") +print(" 📺 Monitor Preview: Tkinter window on external monitor") +print(" 🖼️ Floating Preview: JavaScript floating window") +print(" ⌨️ Shortcuts: Ctrl+Alt+W (toggle floating)") +print(" 📁 JavaScript extension registered") diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a42fefc0cd65ffd7cf3245b1e339b24f19c20f45 GIT binary patch literal 894 zcmZ8fO=}ZD7@pnDN8L7ph#tH+dTG|66%Qg?5o3}{BuzuM5*q@;l9{HfyE|cawlSVs z(3_xA(2F2=Qv3rV{sS)_;=$aECr?It5Ii|M`6`*i@I3F=yzevbmsCmz;+#Hz&v~u_ z@G~Nlqm9AgV*y?P4C(+YSalUot*f3^*A#%YdaMO-?5$FdtAHeM{DU&eQh?-%Ii8&3 z3v>L~9M@;OyJ##uB6^N`+ugfGShIO^2Ue)#b3Gs0E^ITh>yQo%x&bGi2@4N7VZLy9 z)OR>#FunOe1QTY!3|w=m%^klr%}U>~cWtZ5oHjS~U(pbT+CN#|$QMwql(j6ho~=}h z<=a;Oj5OwpR;85PLgnmwVdhis2ZqKX;aE&u{Zy@R3+0Q|LT+=Tx)q)iw!!NsM|+oT z`hi1z6PBor2@cPSCl&ov*dK)>Llji6I>t9Hx4o3cuv@Hd`vr# z`jE_N5nh|*zx*uUnCdYtGSdTYPsAD(46lrm2T$M0Tx*9i-VAwQ!W?IAChPJ{4W>D5 zwOleyHp1n}n+N;wNcE^UDF9@o<%qvy1ec9uKZy{gO@#W3o#?AkJNoLGwk#paMM1Xf zilBI#3HN(&D6HV$)itv*UX(^N3^?_WPcaE5#*1=$RZhbgA9#FI@L14qbAC z9T#PyEeNXO8fuE7{82R}_E!f=Q5hz{iBo&2fnM25{s8f>38M!te2>6#52Pi?^*~;N Vn>~<~!0drl30A&=%WO%M{SS|!1`_}P literal 0 HcmV?d00001 diff --git a/__pycache__/watch_point.cpython-312.pyc b/__pycache__/watch_point.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c0aff0f72a39948b35dce8a2ac4824e00422772 GIT binary patch literal 39930 zcmdVD4Oo;{o+nzL@K#Yk0mV0BDJT>If}m0S3SR<7QGrAyF_n_H)Iy-hTSb%#B$?ja z9nAD>^x2&iJDC+{dnV+z_i6G>-;LSnyKU0x8@so6r`}L@(Qn&NyEEDC$-Q^)65~wg zG55K9fB*A-RDmy=IGLU2zT&C#p6_$sbN*lFfBxs+r>ENuc%FUrNT6xNVEAi#Q6G!q z`TTF-xoF@Gym83TFaH|*jqKgjZ^o}_$TV!}w+vhRtwxGB52XyJ_NNYW{TzEY4%zzc z2)7KS4LkZB!|DC$N_a+p1}i7CKa>4B`<=YCKg&j?qzq*b=k(_wWop06Wau#PwpR_j z{d*?4UHy41EDd1}Ei9jfr6Vju3oGF5{=9&(zmT`{nfNUlH#U_sLW3Po9LYi(Xst?e`jOeQ2U{$dKyO#Y~qj9ojqQEwxRQdZT_=^zTr-x zuA`?r(zCyL&k>>HWF#CMI<)7hBRz+^Hb1qu^~~tD@$Rn1y87PHC-xpV{6etz$hJdo z8zsj{f7m-590`PjLa1-gfIOVCtBkGTb4&(&bH9PUbH9l<_L~ESev9AAn_e}1Ps!1r z!khb3c}qWsZ)W3D@QqXZ(~yQsO5@<2_-@Ajbl(1|0bd*63*XuS*U95M^=I)JaI?oV zJz0|durEAt>R>Q1683$|XfW8_cvzU51-fh8ZDYP6_d&t`Lco8lo>I~@xv4_*!9DCB8B^l>+^7BHP42$n z$&*8VcQ_avI_?wHymDNRKjaU)BUC$j=^hP+0^vY#L`hUHCt{7OZ>o1!*4NdolRH`C zZtrbHtwTZTL|}vuo?)2=@D;R&gU^irpZCDIXqYezCdVqt)X6bvdT4QwM z)#1RtM){TJ3g~n3o{nYsZRT%A=PI89Az2=CmfGaaN$R?vgY+g z(%CR=RK{k>7p2i)UsQtUqT!@r(inzMeuQqDAxyFC(Y~uGY*oW1Oy5&xK%3!MqrvbD z#&FU+iLX1Ds-{W&^2Q1C8$cElmO+|H%O1p-5yQ~Y(kR4O{y0^w$!PfY7K_1Vm@tOS zy!E7U(l~*+Q)UP|)La1UW}^W>JzE3nDcuoHUAPgGyHYZqm5iQyxoTJ)*21<;b!Y4A zw$^z(sgkR=wWqzkyQ_Q80dMnD&0YJNTlTdJX{bT6VL=Z21z*@NrT9ii{UbaEI>3id zNhzoNw1OpD``H2iD6Kn>RVbi5oHEfuQl@Vp3<#!yU&$DdjKd)sQG#`Dp$I-!Q6xWU z0wqK2tw<3%3kNeOCwGebb=vAhlQli}*4o;CU3)(BR>``V;4ezHTxdJreE#_bSINZ# z7Y@vvntgtDU@m*E`Q46pI_9=rYrf|D(W&=NUF*O8e6+mlrfctKX2Yt-KC>H~1q%fm zqxp4Howqadr!v2|n{IHex??b==l;^^S~TPLUML%tzt-$%E;YPgYHO}Czwa@^9nhu) zqRG!D)_;NLB0g7G0~Pql+N46H^6*CW1M?;V4C>pYb;2y<0MzJAs0rhQRmJi~!$=lV z+0^n9zeuChZKMgFGGP)ZC4J0g&xF>7$rOO7lmMpI2_tWP)5Ip2N)H(n%E3Ho#UwkJ zG|42R`%|im0VuWz0cuQR0VzfBhXA=eDUunmLb8TNhXP@t0!gJ@ZwQ!6oe&zp%JrTK z@c#RPos?kIC-~Kbn5>qNf9M3OO~|Fn^T^4kveWngYVwVH1N>Q`4j~~LMK;mYskRCM z5ogj=V?d*m(!L7k6~khR*;aC^Xw~$|%fpw3XItk>>*lh=(uVn>#;GSTVI0K^4rko4 zUUaOVd13a&n~v?bwf7fej)q0MA=@>T^2@a1+qs4D+$u4*YIe(f?xv|dxARxe{as8sQEk zPa^6Tn{G$o0y6>k&?K5P1KOA$gr)SRq6+CEn8cIj1hgAWL6OUzur}fJAlHN$IMDn( zrKN-E%QE9NtUBmg4X)oz zfXQ1W&MVWsA<4{29|Tz^(DD=rx=0Qs+B*~sjsk+C5n-W$>Pr>2P%5IigvZFCR!FI% zfgx{T_#|tXWDMUAdMR^6W^WjGQ>%*!o}{gF~#7>pDw>#bE!s5tGJz$e{tu9oioK^PE9deKEmLi`3*9JWp?Ec1kg`=7Q*E#BKevB+0}#nw2Rb^< z$4fVgr5op7oEy2(7jyPZH7_(YCZ(D=D>~{H9O8>u z+>LW7^SO^LG&P@ZlUpLXYG==guE&07Hso!M=05htVudlQ@RzQFd%r7}L%v+Jm{G`W zSAo{{1*a?StP-6-T65cf;oN;ML?8Qul7{vM!%wW)dr}NP$m)88#lp_9B(23 zF>W3=dn^%GC#VLE=T%7lJ#77SgD=>Jlpd2jUumi;Opi=;f`DH(AN5F(*C-otF(5zF z^0fK0(6}x6(~N)Ys9!W29F;-|k}l~uEh*OV3=9Y3IZR2!p+JZ_&eUD)v)2 zJ{}~vvFuS>KADPoAvBxhl5iL|sNupyHZ=!trQT}wKqxa|<5S7=sr>3jyC`KDQIB{;gj zvmiAj0M5vsgED<9Bbku<1SB;IJ6;jeL^YxDP}o20uJn)ijt}{fYMCs@BWy#>G8x_k zUw7bdFy*8)M%*%f-78t@>Kk~;=#|WMoA`*Oc68+Arxv6IBFY#D4rA?vLf#?&3;rPq zxChmZ0{3tv6CCjk`z2dw?D&b`5bqZd2{hu#7O1yEJ2|wb8Rii>$VcR~&_&K(a-M)A zIpoaVQD68}NXjM-kLvL9kbVq6GB=K5fewXwN^A;-Bnzb!y6M#t^1a{}%Hj1m637S0 zm2nv%p^6fvg#9C-pdeXgyk`b3lI*~)qhn#JL^w`~Y*Ytu8M=Fny;J|>HqkmzSOxYY z1BuNDjT9&G3(Faj*O4IfpWy&LfbdVfRk(I$%P$J6&UH;SPrY#4RXDxp^4?2(-`E!~ zsTE6V=S%8DSKU*Ix6#lo%gg-ug?KTk2F7u+f+o^HF`b*bx(CuYm3 z*n*8yT?>WfGe>8`(elQ*<~iRr^W0%bR`Q#tIzK5Wx}16`^|IrVW2S$uV7{OUi5;1* zcb@A!e{}Zwsm_?A{?xcOB4e<^wUJj*rIi-E15%K9Pe&dq-YJ54slwMx|qO`u) zR!HOIv$QmJ^n>X2WzWkth_A9=w&N=$0lW$G3ap5X(Q^o+hc)&Bsfhy6fOet^5bDaT z0Z`WOhbzJ4O*&Rclbe(#eZuj3MHA`2SD@b?K5xQ-9_3A>qeqqm2p{PRm#Xa&S~2r9 z1fhOunOd35n8+AhtENo+^7#?GOlBgsJQ1q;lvwAAZOUYAaxQHX7%=l-g<3}9S8Wk? ze+pL^SNS^>G0Msc;iATzL-tcRX$_M|f<2HES(mQBqXynOX?@9h-uUf*A^Xl4RTOX?|Dsh)2S++WLA>6coor#P}|zYkj`GW+8u{s#W=DV2{?MA_OFj4Ct* zqO|b7u&*yt+!`Dk;@u;`u$%TDl!5{S!I36+x8#)Hy`eya1X?^et`mW?5b>MurFZ&< zPPm`)3*o?kZ|Ghod+iJgfe5xgd_z*|p)s&oenAj0*q;(*@hJ~S2$2(pBRQ19nF#h% zH(~j7B}f*>etHDi1Y&cAX3D@Rv1c{kxMbY$=_bTQiWpu~Ix44gqw=88v}Ec#Em^~X@Q@#S0IdhQy)DhX?OPfp`@vJeaIlMQ7TRbyM(JIa z`&mZ?u${twYEu^KIuZ1S1_Zx4AzCkH8lW$vUuIP4D#xv1F zS$`$tI3&;bvh+ngSqZ9N;?&kFLqbg>UR5&sg&-RwxhdW=NXi=JJtdr>!pA8#RpBdy z7b$3hg0!s^>WDnre}Hf%x=SV^k|>D-h+iV)PG+$GXUaT;FUPoES3x{yy_mCpKBsbu z1O9hrzdm+u><`XNSr$0Q>-KZ@XzqsDCuVm>bGF5}roSb|cF|->-*&5b)#aU+c3wRl zD{eZ^ft9KP1ic+i`$}H!R@r-=<04Ut^0OT6~5CA+5f z-$pOf&!tb7OlQpaX47Y0j^;fUjE47u@^BMZ_8+WClPZ;Cq_L`MVVvo(H+k-NXjd!Z*W z>)tn1ca#}^va{LL$r(N}I6LzVAC=iUt>%xaGdd0C8wM-dG`P*>uww;O|1JbJ0LxOnwFlz%6_97~6;B(4i9( zl%9&QvEQQ9hK*Wza~9%Y9uyqUy_q^`0tVMwqT8-CEn4#=_D_WRuqNz=Fy5xtwV4F! z8vCrrDo8V44g=*+!7EuP>z$kLLw29rlGK$Ka^{r1yF390tNbS3;yPfpG~P zX)SAVd(wmfs+8eL#x16T+hZ&%=k+r7&MT$(&(fB+WC>yeQ?gJ~Nz&@$8Ez8(DK#>M zAq**9?GVc=Ih9TL1OhakkkX<0AZQ^1V*!Z|k|Dp$IKGVcNMSPLxCD>hMRe%ra9%Ma zacAG{dF$|%!*6-7czs{FS&Dirot(rJrSTC3b-E zMOTWg;Z|nW>u1iLdHtnxFHH}}GOKUp7Eag9=EQPq=CW>;SH9JCrEAvrulJqbzfk;m zw6r!_{P=xOhJvZq_tPSD=EoA7(`L$G+Dw^Do5{&% zB25q7)e z*{7hRHAH$^;Zsn?9D$I(+mkN5LUGnT0UKF;#`TymB_onbLCO4 zB*s04#pEu(Jb7s{zPe6aT{l;H-8H{@53Rovw&o~Dj4K5rnO}G@a3L^bc`Nfu=3G~_ zr#F_@hr-i{>rOkD7Uv2?uHen%mrq_gIm?6aig69fN?jvzYi@F7_pel1OKhe7N37XL zU8%h3s-{(wyjDr0C29U?YXx;GkHX+sq34)@5jv|?e|?ai2x4?rE^mL+G-3R%@t+y> z)^JjrbaY>q?wUPfyVB7vjonduOL`;!$nDBNyL8~sKYF_|(Jmc`_7Bpog2eYweZ2E( zmdc7_1{0;qo|D)t_!Hw~(vG;^4lUZQgOdViT0?zZ?0V^f4y32 z;#X)wxX%1|$k(Y%l%V>O;8v9ymiWDzuhJ#ZH!Bz~@DySrK*rSy29&oEj*o@I!4X2* zn|$=|Q>sb9#{#t)@Pcn>%C zC)qS5@_Xi*ni{rik=Uyceuc_BX4WI&7w`*lIAgT6^jB~E8~2{Ep&?n=F1PACm)yOI zya|K#YfAqUINhE+My&`pDCDPbpf@L<1(7U!_BHoODJ^Wkr8F<}0iaeuY=`hON@&^F zzNZhAKVw7!Ba%(_K`8*NhQ^&pw%#!ICQd%(6CnPlO`8|svman8?fXHpNIBktQ@)Xt zepO;mi9qE9M@|GzO4;5pJE7t|p>+tGCM0M|NQiz;tugl=9y}C%`Wex6Y|8ctXMf#x&UU{1V)cdUD0gJ$>1*Zj zroAHmxFa_LaN^uCkvkR}@W%%R#eu=-^FkD_TotXRbB=X!m=&AJ$F~d48Welbs3eW^G=!f#~R+Oeg(td@~blBAd^R@yK(rdvA>egSO1$w2W zqGlaZIJvZO&SQ(LR*;oI++dA-Hc#Qq*s(jAZzQ#3zfTCBI1zF7LL~2I+qv%MP;G53 zFphP|cicb3Kzk&sH8?tsgLybc7Z^Pr^a*?t{awk z=Mk~(HAiNQ2X~{2p5_HekD9ePjI!p4A z$}*!YB9fLpk5rZgW$940k5rZwW$A#DC1q)OLRj(y6M0sMCAtK1{y=2&jqBj&cV z+-Z-P+re_DKVt3-mOJwib30k?tgnz;Q(;Pc2R<9$PX}}+zQA~n#}z4Ufh3yF5;M5N zP8tuh)6cSSI+D3Vra+;}8w%Cb?~0`DXsNH+AEYCZyCPXiY!9S)*w>(V#}0OS`Y;`y z-W9Pc32GWB<_Z6}T#m472__~{EeSwBFNgC-Ml5UNgARK28s0W`L)JzB@of{^qLk%s zRF3-!Jjv?Rhao)EpvhjZCm?VC$-GNgnzM;@Ac;pNuwfUT8_7|@h=3MdmX0g6G zR^Q5?QF14G{+^v6lLZDLg`89Z%}K2nU~9qfwHrQ0McK$HLF<#78vZ7l3f3u8{$W&i=dMjUBB4GCrWNl;6-;k5luNWjWgyuh}iu?2gqm zvkpLiZojv!eP4TDyZ3NcciVx(M3DU@`YKsytrOEag57rLHehG{%*c{=?DAnpPKzHK z#WdI6m=Or@!AzecnVOPmJZ@Bg7T`c|NXVvHpiyVf$z1J^XdSM4Ap8kXw`g-0Uj^Dz z*fP_k@zKJn*|vD~Zn1iIjC&l!QX*=4)vP66xm~Q>9^-Z_iHR0g&+_rw-D2(inM!9} zanDZCvopr+T9RqHWF{Q1*dIzcTl zt~EJTWWKC9#_ty1fRVNk`JbCLn0}gHQsh z%O2XMlFt-}N?1-Es!&fsKfw--V;}r$)@P<#*$L6Fo6|x$nPQiT*LF3HhL?@Mx1Onz z>(Gv;AH!VCXSxDf-_T+bm@Rt}eL}pJHjzm8_y94PI+R}$Wnq#}w2qrp^{u6fUElVq z;bCnL1!`T-Kvu5pgW|N4X{j560xlT4teBIPDJwuP}D|<#T-OL@7ID`so@pHR4jT z%b8StWx%o@AXYw++6yp7iBq1u(O$RB(s_ zUWXH+)uRdDuHz+AdOw&-)=PMa;M8h308^GiTkZl5nUW5wCGM^h@kfVDKhD{KQ5tn5zo4#9iw}*ZP>NlGI$g&UL+h z;M{@f7HZQb(Y@)Wqv4CiLYxal$^~cMz29a%7R`F>%e&c3=PT8g%XGdfXR~I_(d_D& zqvrP3ozPu{Bx%|eFZ77`b5zZ46Cpt|nbUJg=h$|^Mta9Lo$fCdO|l`4d;h|6*J{XG z^W|b1io6%%F__;fX(f@tg+tWq6l9!Cdns)k#!=`u2?g0AA34M7l^0wuIgQhSR_L!PC-VV5{7nU;@YWV z2!6;P_JvM|a4eS{e~}NqkmefB!pKKn7{M$M1`$f(&`w^&FZ>0ob{OUv{#V)OJ1ZZP zB{MsefJA;$>b~ag*8T&%!oR_r@Gdz-0t&B^^S{A?=?I7*V8_X!DAq6pDvM-I-G`Gf zp?t^|>6ji5SQIk%l&-ufhjU=B;v~+?u%A<~4*~fA4myqa&rl%!oMPv5(L5N8It!y= zA;~&UKQeA59t*ftdt_dKI<1Ffld<<3NE5mO2M2C5bK;rnKF(aX;K-&kBaAC37rF8n zw;o4YF1~o-#l%4tDDRbAPP+vA5p`lg-F(5Osr1`v`P20ura$BVa!#aqtpn`%G5nYE$(4ryCg+Q5=3TWo3al4+ zJFh67S1smM$MYJ*yoR|aueHy0f7t%f{`dFK=k-i=+|FGy(=O(EW__Y-;}6Pk_+{{& z!9U+9x|*lj7joCmJV|zkp2tz+A2iH`-;KNz`SabPt7WSFlhy0vtLw$p^>Y>Tt8oe? z<|rY$;E8ij#2uxgqjY*K=D=_&A?2c@eCEm7c5H5~dvD$KaJ=KN*l{@OD8K1A^2M#p z0y14x2@2tS_e}jvSu}fn+)*JqD%h!$%6lP##2;j|r*#w=J}R2f)Gpc0E|p2#!Vh`q_`8i z>99E{OZgak%MRufU|_o>FxVomN-LHWETjCNs8jgA;6T*21wWEi77bEk)!o|yn%ha{ zPt;wArEK(dn%x9;l-bQ@R;KIq7tg&oedx89iN3-Kqd&^L4UbG@E!qsub&SK;AJL~! z7_6BtRf^UfM_>ePbTQI8!h58!pg8uhk;>>LyD-eaBk2Rfl8sfkhG_Xn+UI5mD4;7fRO9){ML#qxx@hh4+sF%_%mosP2{K zmGHX$yuxhJdW&+Dhn>R)9i~4%dfOB%2Pm(zMm;)h!W2m-lOYLZbCyzPTLH9_oKpKT zE4BOUd`I;1f%}@{pol%DNWQWrRMl^`AP7l%xRt+n0u>QtS2Q1l?eI$`D7BdQ@ntFw zd5C)OjDRHSKO&K85sCL_Ft<)>21Qb^-B>Tz08v)0g4))y7szdT$u# zjMdTGL)K~7Lb-2I38UP~sX-Pom{3cg$|J0|c%bG-rKr73C^!B1K zt#l^7{*+eG&U6w;HkPd*mwIqM?J&0~;$}9wk~gy5py${ow`_oxVAB7K zW=UN5GzgGU2rpC4ddaw!lrk>=I!+3{$R3UwmvisrGeFD#4(}&rMx<~+E zw$)&M$qE;W!N9S%Caz4(J^Ai4?>rN8v;gs{u~p!rZ#t^Km}*-VaxavI;=WbaoNjnO zy`&jNl~9HDXnVaQ2wFbv5~eojgo@r5<;{#GQYP{e@c%GGY4KPiscf@9FtX>Q+@SKu zf7`&G&v&Z`Qm8<48QIDR+Jx{|QE4K?sdpfQFfGn&d_v<%TnHw*a)^RVGxFG!KXB#1 z+<;iVZN8{U(f|G`=OrF_obVOYnf|rMiDTmgt+PGCPV`0SK(Dj`s(kPO5mBL>(`QE0 zRH}gu5Q_Xi^ham_>hZg;?4Iit%eKxJL306TRY-AR#Sf!XZ|aip?6$>4GA4LTBI>4M=F{% z;q4s2O7Ybd>_3h57V* zPeU~@=IB}|t6+)mCozZ)WhBN4d}>SuNLrZXA>%mC)8peIdikSAZhTKkG`Yf-(Ss%p zRQX|Ymesle5>X4Ymjs(mkh+0Dg3(m{R_IKj2@7oMq_A`H+L?UDGeRjxD_<^^%9m^U zX3FQZ@)Pa<#>!8a>`fT$11SeiV)>Jh?Ww4a$#)^c{rivNGD|=Okke`kIXGmLa-ZzZ zG@0^J$~a`)^qVBRqXTkG8Cx~G!z5t;8GY%N_0LI2C(G>UJZwT8JyOz6rfu9zl^eWj za(HGD|IV`DAJ=QWU4W(zieN@k1~_LHTOlIhTeZg}J^nz(bV=v+%CN@k6-Yi4t1 znr=F47qSYYt7^$=i^HHi>h*dl9J*nMG~Uci|Xe+WTaP$?v}$}wxUiMmAM@;T!5IGOv7 zFqF~sQz&o9O<{RAUQjI-RL>rpFW5enuBa!-$1SIi&80`Hci!Z7$tVJoR(ojYosH+u zWE7zbjzjqgUPYHr!KRlAk}yWJiKU4^w3>9uEjrPq=6ulJ{-hBn(Nfq+Gz~E7ZIoeu z-8iTLu0-7`3L`KEG{jxCsni3VH50xMbZCB1KE1Qeuz#(LWN7sxj{d#PHwfWFzBdIT{v@|ml$8whs$-c=vEDwfW&;F zhvvh$8R(@zemBr{1=TPmmncW)lpvqY=Oh^%w5s(lEg4%#xb)y9>$q9|O+rJiNDnYRf|2>V@nzcT+7c`xN(?!{Bf&nz-~HBN;Q>bz#?hPFXys zTFj}QeJYmIc#Zo}#(Np_TRY=h4~bh3MSG6Mw)QWSt|dF!$hA*7F@qEu>0x zRmEM{GTJ!DU0Z*nBI??B)71lG;Bi-SRLli~WJPgjndmH=>5A63T;F=VD!Ot1O=mX= zLtS6obt;C-dW|YYE zsZ1o&0psYv&E%UBkm7+BX_6qY&N*zJOPEO`7!C;TR%-_YhsZT4|5`OB@k{q$Bya$3 zZ7_$+)mVlBEDz1^NT^D{*st>SbTEMBB@4p<_)Wq9>5=?aI{&0WckN`7aG0RT639P* zl--iKr=vwOx9mUoX$G3gb`phSl(tX?r$`l$VP~SOXDNQ`=s18}!zN~OG%y0XZU{iO z*4-t0nc6~=do(bLOW0VXTgeb{Cg3r(0g%|0fW!hBMv_c{ATAvn2Lai2fX)(+z}9wH zfv%8Gu#>=&m1!$W8LSyv>!kDpIOp1`9m&dQA07>lC*GYn(W||+!FH;Y(z?gn*3}|q zu&dX!IDw~@q;(CC28A%gB!V9yGQ)KOK1jkCZJPp_CoFmkB*+y;DG7t)B$Q%sTn6Jt zMCg&ZGz3cyk#!O@r{e_=f#!=Ar>)a?3r9rzFZBQOXQuWr$v%PnxPxT(vzzA~u!y!r zbZnV>;TrbP+ZS>QFYdmudp@Upre~%%n&X*j16Cku%eTA#+OcNAQ51JnijK;dql)>V zN9Tz-HZVW#gxE0K9j|T{tDCPk#vGl=DLx@&vs!elp0PzY?78Xa083Ov8}TQM3=g?Q zhkGVJTE+ju;a9fk(Q?Y9#8mJ6;lTAZe{tg45nQYmcXWu34#qik0x;7;{nJ*iwbtpX zvu)qs|MvcyT+{t5jn?T`kVTdc>q)xwhkzihe1pZKsoWBvq)Ttc6_r~E*K?C^F@)7L z96c9?#??zOEX&nmajhUIqu}^`V@2BeddfG+=M;DvswOs6w{Zi8<#oq7$N9c^?m7{F z-1-@u*{+8os4a_jR-sEH?`GX}tR_`ZTh=cf&U+z3xc{nXo8djvPMG~G7XA$H!dv9b zlJhn>ACU9!$oUWCM9HCp0>X82Xfs(LeF}jz2n5P*({pK~eJEV*s3p&{eQKo$b=Y(fg2gk5#kbiy<$@@SkGmc&=(+qHX9 z*uJt}E3^!AF;Ru-T5S0~yQFm3Od~O@%B~L#KriS+f!( z)GXPJ(mJL6D_F9VZPwk&E4X<2!s(gy^LZX@`6O&wk;%DeQC*CyC!r2gp(Be}(??@m zg%+|(Ne^GL}dnzTCc`9C*y;!jt$r?s$}mK5Ay_OFpM zEpfdwXi+l^2x}v(Cy|;kUrkqOSFAOSn}Iwl1M)1(gatPXwbE8dQaL(jUZJi`Vt(iH zF{0amAlFg8hMxLd(wA`EpZls)Z5{5tX}3I@t->t!Jri>Rmu)Td0omlG#9P278j~ z*5Sq2{eOh|mOWTQh3XnwVFZdEIs(PIBcMOxB@Z3(RS&N3YN^`ZCF8AM+v_5Jh<$L`yT*TQu)eNd6SU!iSeS@Ln6QcV2zZ?YZZJ3KozyU!u@(v2s(A~|jT6Fyv|?v~X)-M!G3 z{FEjy7)1#`%*dngOF7^tO9I%%M#3nCJx|UVa?X;&)MCcT7a`|2aBxG%eW88V|dG&VdUrG#)N*6DH98Tpcsrzw?5a*&Nnt;%%>WXJMwnBezO zuP{Z?h*wH<7U}=^}`qAVzva6kx?NgPfk7TN! zU=O>Rq$#m|E19u%D?E*=CClOVmV=RWwrzVzzO|$&v5(5Mkv_da-Lc{>Fx;i$#Wh+% zDFqjQ5tsizQ4uNF<~$ib6|uwaw`_Vc#LRm4Me=*;5^6=e&wYH{tz1b0bq_YAlpVvX z%mU4DCL0gP+i_0OKNPW)ZP~I#h_N=v*?vVM$%gYo^8JFGTW}-`?H|KNX`^qO|HO%I z&l=$s3byYf%WtyD@KRlz-r~+$v^K6xKrz^z|LsFZ>O){6(tO z+SAq1*(X_<{W;~rQOQnfW{Q1uhpJ!0E_X|(YXu;OM)@$Aij$B?7!uY~Ng3!NTMTMO zr9ZNn^)%&;tfEWlYLDcDl-yWA$-~TqvJA{F4cp{U99d9FQqttT4t3_rvhiBg2K!iG z9BVK15fB2iq7HEoT^!x`*7hsgqnr0e5A{a-pNZujo3bx(*(Bh3v*&W(rM_9yT=o>M zCupSel#>@GD1JTnX^PRzH$7rCI~oZBaI`=W<>U~o0&KN**^`&%J!3 zC;HTpSpHGW9L^=*ck!lhIeaNRyLN6mzgW{hU-L|?=oqQDlk5|^7&H6k%HQ4i&c^GTZ>*2@9GTBM zIt4Lkj$*_0(9H8c$bQ%Lj_X=8v*Nl-Ysbt1%>OvIP2{#+^W8AUdivr$Pl-KGMUOoj z^(Pi!uH&Zg#bFKB~ zCRz*&o|?F4o9Nkgz3i^pSlD62d1NkEKB6)G{A|__Hoe>UPUAIv^CDO_!a%z6*p2nB zw~DV6&+WK={Du(A>A}TzoRejjrE)qddO1U)AuTPUwV30h`BaR*%EecaTD>?mhZQ{-8LtXwc92dJF+C~nW>y>d-sWV zo`^Sgh>aaLgaud8^cnQmR{5LC>bEUFSo?0}JC)b7W-YPGR@Ka{XJ$PB1^U-i5Ml3@8{nFh4FQiH&s@bgw4DW z0$(Q?2GS>ozwcfUbrcbVto=mSAB4yfA(XGWowXj=r0k4-oyp>|1 ztFE8dM6qmlQRlFOo)W6s`v;u^Wy*k>xGB-1M=sAgVG$Z8thhN(J0+7)!dRhasnoNi z^qSh<3h6mYKT^srvCtbwR^E!SEH_Hl6brv%uZt7uRG&J+E40%=?L=IOnm+OSpz&Ry zv~;C39YOCSl$JrIB~)_;*CyJg`UZ)LT=qO92Px|urM?57E3+ zT}ilMF*RJH#tiBUKyBSvlDD8Z=-)|sbCE?5zL^D2!YSxU5T{#4mvFU;AXW| ziC=XdK;g+I@8jdvopzBJ*Qq5zd>Ltdf`)pwr)lubPpTV_~hO6;Uo!xl9+Q<375 zda1+a^SOLpc)OY-VL9eL6)D>!_Yyab@uS})6{&oMnNnF=nG+47M41U?JSpQu8K3$h zMNGiyj-+?tCZVAK4-F@1`$RlP*y*@rZY|^<9t(xt$Ng?O;mFwVala6f*|YCZNz>%~ z5jlrIjLRILQA&ZSLt?s^-c_bbEHhQX6~SSRy^wi}KSQQul@N8fRNjJy=L2FZYT-bIDz}iBig@mNF?aoZZsnByc3N(< zuv1LyT&Pp7yp0xBf9$A{uhnG-KNhEG zK@o>k8n?@D(b786S-;3RY&m324VKhoTW9mG%DAgmbk)wg>R=6J<*5IZ>AIAAt9s*{ z^?Q4j3I;`|XIaR{PS5S?x}^ApC2zP`xw87Lm#@4WuV@l0n&vBZ#4B3FikA6`Hs!A3 zVOXSDsH~1xZWSxH#w&M-l{@AucPWu0Fe3iI$gglcs42yE$apI&_x$ioZ!F6*d+3&Y z>}IcUFgT%C zX0qiZ)SGul8@(~dbH9W^94jL3{mz9lqgkFW!E?!%5ThN*s%CpW4Be>z@e9{aMXL_P z9S23nL8fYU2&!fWFx}>wGrOFI4-743T?K}ZDjj=mhL0M{x_07{;jQrh)M@Imj3@iPDedAlc&O3%~c@uDveX zbdhyIpw7hB@eHD2NdGjhJcl;=D|lmi?E--S$c?)8F`G@Yfu=na+er~N(rSm;1Zwf5 zpDYkPE1ah0Jx9@2n7JP~CHxce+vTfMnC5#LC0k1l83|?=k;>{z)KV#j?l$$d?rZMt z_3m#zc(ALxqgN_ofo)yA2lq7}_0p}VTHLbXfsttW2G(7|brgp`L+BD5Tr6!g{>otc zl_Bj{2G?&4RlhNm{>qU38^c-_nDZ+GT{Dt-+Vo1=qQzod^J_~5>?cJn1;4Y0%|`cU zQ13Ome>akP%xcW~%s}q%9#5Mz8V?yigC$S=EVfw;)~xeYi)K^4DQd{UI3J{s5HKHQHj1=OT9_t+im*SO{i!}}Gf&GqK@>x^*!Kfb{Q;{X5v literal 0 HcmV?d00001 diff --git a/js/watchpoint-floating.js b/js/watchpoint-floating.js new file mode 100644 index 0000000..9c5c3e6 --- /dev/null +++ b/js/watchpoint-floating.js @@ -0,0 +1,633 @@ +// watchpoint_floating.js +// Watch Point Floating Preview Integration +// Only captures images from Watch Point nodes + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Default configuration +const DEFAULT_CONFIG = { + window: { + defaultWidth: 320, + defaultHeight: 320, + defaultPosition: "middle-right", + defaultOpacity: 100 + }, + opacity: { + levels: [20, 40, 60, 80, 100], + showIndicator: true, + indicatorDuration: 1000 + }, + positioning: { + mode: "snap", + snapPositions: { + "top-left": { x: 20, y: 20 }, + "top-center": { x: "center", y: 20 }, + "top-right": { x: "right", y: 20 }, + "middle-left": { x: 20, y: "center" }, + "middle-center": { x: "center", y: "center" }, + "middle-right": { x: "right", y: "center" }, + "bottom-left": { x: 20, y: "bottom" }, + "bottom-center": { x: "center", y: "bottom" }, + "bottom-right": { x: "right", y: "bottom" } + }, + showPositionIndicator: true, + positionIndicatorDuration: 800 + }, + ui: { + showHeader: true, + showTitle: true, + showCloseButton: true, + dragFromAnywhere: false + }, + shortcuts: { + toggle: "Ctrl+Alt+KeyW", + increaseOpacity: "Ctrl+Alt+Equal", + decreaseOpacity: "Ctrl+Alt+Minus", + snapUp: "Ctrl+Alt+ArrowUp", + snapDown: "Ctrl+Alt+ArrowDown", + snapLeft: "Ctrl+Alt+ArrowLeft", + snapRight: "Ctrl+Alt+ArrowRight", + snapToPosition1: "Ctrl+Alt+Numpad1", + snapToPosition2: "Ctrl+Alt+Numpad2", + snapToPosition3: "Ctrl+Alt+Numpad3", + snapToPosition4: "Ctrl+Alt+Numpad4", + snapToPosition5: "Ctrl+Alt+Numpad5", + snapToPosition6: "Ctrl+Alt+Numpad6", + snapToPosition7: "Ctrl+Alt+Numpad7", + snapToPosition8: "Ctrl+Alt+Numpad8", + snapToPosition9: "Ctrl+Alt+Numpad9" + }, + behavior: { + autoShow: true, + rememberPosition: false + } +}; + +// Configuration loader +async function loadConfig() { + try { + const response = await fetch('/extensions/ComfyUI-WatchPoint/watchpoint_config.json'); + if (response.ok) { + const userConfig = await response.json(); + console.log("✅ Watch Point Floating: Loaded user config"); + return { ...DEFAULT_CONFIG, ...userConfig }; + } + } catch (e) { + console.log("ℹ️ Watch Point Floating: No custom config found, using defaults"); + } + return DEFAULT_CONFIG; +} + +// Main extension +app.registerExtension({ + name: "WatchPoint.FloatingPreview", + + async setup() { + console.log("👁️ Watch Point Floating: Initializing..."); + + // Load configuration + const config = await loadConfig(); + + // Create the floating preview window + const pip = new WatchPointFloatingWindow(config); + pip.create(); + + // Listen for execution events + api.addEventListener("executed", (event) => { + const { node, output } = event.detail; + + // Get the actual node from the workflow + const workflowNode = app.graph.getNodeById(node); + + // Only capture if this is a WatchPoint node + if (workflowNode && workflowNode.type === "WatchPoint") { + // Check if floating_preview is enabled + const floatingEnabled = workflowNode.widgets?.find(w => w.name === "floating_preview")?.value; + + if (floatingEnabled && output && output.images && output.images.length > 0) { + const imageInfo = output.images[0]; + pip.updateImage(imageInfo); + console.log("👁️ Watch Point Floating: Updated from node", node); + } + } + }); + + // Add keyboard shortcuts + window.addEventListener("keydown", (e) => { + // Check if we're not typing in an input field + const activeElement = document.activeElement; + const isTyping = activeElement && ( + activeElement.tagName === 'INPUT' || + activeElement.tagName === 'TEXTAREA' || + activeElement.isContentEditable + ); + + if (isTyping) return; + + const key = e.code; + const shortcut = `${e.ctrlKey ? 'Ctrl+' : ''}${e.altKey ? 'Alt+' : ''}${e.shiftKey ? 'Shift+' : ''}${key}`; + + // Toggle preview + if (shortcut === config.shortcuts.toggle) { + e.preventDefault(); + e.stopPropagation(); + pip.toggle(); + console.log("🎹 Watch Point Floating: Toggled via", config.shortcuts.toggle); + } + + // Opacity controls + else if (shortcut === config.shortcuts.increaseOpacity || (e.ctrlKey && e.altKey && e.key === '+')) { + e.preventDefault(); + e.stopPropagation(); + pip.increaseOpacity(); + } + else if (shortcut === config.shortcuts.decreaseOpacity || (e.ctrlKey && e.altKey && e.key === '-')) { + e.preventDefault(); + e.stopPropagation(); + pip.decreaseOpacity(); + } + + // Snap positioning - Arrows + else if (shortcut === config.shortcuts.snapUp) { + e.preventDefault(); + e.stopPropagation(); + pip.moveRelative('up'); + } + else if (shortcut === config.shortcuts.snapDown) { + e.preventDefault(); + e.stopPropagation(); + pip.moveRelative('down'); + } + else if (shortcut === config.shortcuts.snapLeft) { + e.preventDefault(); + e.stopPropagation(); + pip.moveRelative('left'); + } + else if (shortcut === config.shortcuts.snapRight) { + e.preventDefault(); + e.stopPropagation(); + pip.moveRelative('right'); + } + + // Snap positioning - Numpad (absolute) + else if (shortcut === config.shortcuts.snapToPosition1) { + e.preventDefault(); + pip.snapToPosition('bottom-left'); + } + else if (shortcut === config.shortcuts.snapToPosition2) { + e.preventDefault(); + pip.snapToPosition('bottom-center'); + } + else if (shortcut === config.shortcuts.snapToPosition3) { + e.preventDefault(); + pip.snapToPosition('bottom-right'); + } + else if (shortcut === config.shortcuts.snapToPosition4) { + e.preventDefault(); + pip.snapToPosition('middle-left'); + } + else if (shortcut === config.shortcuts.snapToPosition5) { + e.preventDefault(); + pip.snapToPosition('middle-center'); + } + else if (shortcut === config.shortcuts.snapToPosition6) { + e.preventDefault(); + pip.snapToPosition('middle-right'); + } + else if (shortcut === config.shortcuts.snapToPosition7) { + e.preventDefault(); + pip.snapToPosition('top-left'); + } + else if (shortcut === config.shortcuts.snapToPosition8) { + e.preventDefault(); + pip.snapToPosition('top-center'); + } + else if (shortcut === config.shortcuts.snapToPosition9) { + e.preventDefault(); + pip.snapToPosition('top-right'); + } + }, true); + + console.log("⌨️ Watch Point Floating: Keyboard shortcuts registered"); + console.log(" - Toggle:", config.shortcuts.toggle); + console.log(" - Opacity: Ctrl+Alt+[+/-]"); + console.log(" - Position: Ctrl+Alt+[Arrows] or Ctrl+Alt+[Numpad1-9]"); + } +}); + +// Floating Preview Window Class +class WatchPointFloatingWindow { + constructor(config) { + this.config = config; + this.container = null; + this.img = null; + this.visible = false; + this.isDragging = false; + this.initialX = 0; + this.initialY = 0; + + // Size from config + this.size = { + width: config.window.defaultWidth, + height: config.window.defaultHeight + }; + + // Position from config + this.currentPosition = config.window.defaultPosition; + const pos = this.calculatePosition(this.currentPosition); + this.position = pos; + this.currentX = pos.x; + this.currentY = pos.y; + + // Opacity from config + this.opacityLevels = config.opacity.levels.map(l => l / 100); + this.currentOpacityIndex = this.opacityLevels.indexOf(config.window.defaultOpacity / 100); + if (this.currentOpacityIndex === -1) this.currentOpacityIndex = this.opacityLevels.length - 1; + + this.opacityIndicator = null; + this.positionIndicator = null; + + // Snap positions grid (for relative movement) + this.positionGrid = [ + ['top-left', 'top-center', 'top-right'], + ['middle-left', 'middle-center', 'middle-right'], + ['bottom-left', 'bottom-center', 'bottom-right'] + ]; + } + + calculatePosition(positionName) { + const snapPos = this.config.positioning.snapPositions[positionName]; + if (!snapPos) return { x: 20, y: window.innerHeight / 2 - this.size.height / 2 }; + + let x = snapPos.x; + let y = snapPos.y; + + // Calculate relative positions + if (x === "center") { + x = (window.innerWidth - this.size.width) / 2; + } else if (x === "right") { + x = window.innerWidth - this.size.width - 20; + } + + if (y === "center") { + y = (window.innerHeight - this.size.height) / 2; + } else if (y === "bottom") { + y = window.innerHeight - this.size.height - 20; + } + + return { x, y }; + } + + getCurrentGridPosition() { + // Find current position in grid + for (let row = 0; row < this.positionGrid.length; row++) { + for (let col = 0; col < this.positionGrid[row].length; col++) { + if (this.positionGrid[row][col] === this.currentPosition) { + return { row, col }; + } + } + } + return { row: 1, col: 2 }; // Default to middle-right + } + + moveRelative(direction) { + const current = this.getCurrentGridPosition(); + let newRow = current.row; + let newCol = current.col; + + switch(direction) { + case 'up': + newRow = Math.max(0, current.row - 1); + break; + case 'down': + newRow = Math.min(this.positionGrid.length - 1, current.row + 1); + break; + case 'left': + newCol = Math.max(0, current.col - 1); + break; + case 'right': + newCol = Math.min(this.positionGrid[0].length - 1, current.col + 1); + break; + } + + const newPosition = this.positionGrid[newRow][newCol]; + this.snapToPosition(newPosition); + } + + snapToPosition(positionName) { + this.currentPosition = positionName; + const pos = this.calculatePosition(positionName); + + this.currentX = pos.x; + this.currentY = pos.y; + + if (this.container) { + this.container.style.left = pos.x + "px"; + this.container.style.top = pos.y + "px"; + + if (this.config.positioning.showPositionIndicator) { + this.showPositionIndicator(positionName); + } + } + } + + showPositionIndicator(positionName) { + // Remove existing indicator if any + if (this.positionIndicator) { + this.positionIndicator.remove(); + } + + // Format position name for display + const displayName = positionName.split('-').map(w => + w.charAt(0).toUpperCase() + w.slice(1) + ).join(' '); + + // Create position indicator + this.positionIndicator = document.createElement("div"); + this.positionIndicator.textContent = displayName; + this.positionIndicator.style.cssText = ` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px 20px; + border-radius: 5px; + font-size: 14px; + font-weight: bold; + z-index: 10000; + pointer-events: none; + `; + + this.container.appendChild(this.positionIndicator); + + // Remove indicator after duration + setTimeout(() => { + if (this.positionIndicator) { + this.positionIndicator.remove(); + this.positionIndicator = null; + } + }, this.config.positioning.positionIndicatorDuration); + } + + create() { + // Create container + this.container = document.createElement("div"); + this.container.id = "watchpoint-floating-preview"; + this.container.style.cssText = ` + position: fixed; + width: ${this.size.width}px; + height: ${this.size.height}px; + left: ${this.position.x}px; + top: ${this.position.y}px; + background: #1a1a1a; + border: 2px solid #444; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + z-index: 9999; + display: none; + overflow: hidden; + resize: both; + min-width: 160px; + min-height: 120px; + `; + + let imageContainer; + + // Create header if enabled + if (this.config.ui.showHeader) { + const header = document.createElement("div"); + header.style.cssText = ` + background: #2a2a2a; + padding: 8px; + cursor: move; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; + `; + + if (this.config.ui.showTitle) { + const title = document.createElement("span"); + title.textContent = "👁️ Watch Point"; + title.style.cssText = ` + color: #fff; + font-size: 12px; + font-weight: bold; + `; + header.appendChild(title); + } + + if (this.config.ui.showCloseButton) { + const closeBtn = document.createElement("button"); + closeBtn.textContent = "×"; + closeBtn.style.cssText = ` + background: none; + border: none; + color: #fff; + font-size: 20px; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + line-height: 16px; + `; + closeBtn.onclick = () => this.hide(); + header.appendChild(closeBtn); + } + + this.container.appendChild(header); + this.setupDragging(header); + + imageContainer = document.createElement("div"); + imageContainer.style.cssText = ` + width: 100%; + height: calc(100% - 36px); + display: flex; + align-items: center; + justify-content: center; + background: #000; + overflow: hidden; + `; + } else { + // No header - full window is image + imageContainer = document.createElement("div"); + imageContainer.style.cssText = ` + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #000; + overflow: hidden; + `; + + if (this.config.ui.dragFromAnywhere) { + this.setupDragging(this.container); + } + } + + // Create image element + this.img = document.createElement("img"); + this.img.style.cssText = ` + max-width: 100%; + max-height: 100%; + object-fit: contain; + `; + + // Placeholder text + const placeholder = document.createElement("div"); + placeholder.id = "watchpoint-placeholder"; + placeholder.textContent = "Waiting for Watch Point..."; + placeholder.style.cssText = ` + color: #666; + font-size: 14px; + `; + + imageContainer.appendChild(placeholder); + imageContainer.appendChild(this.img); + this.container.appendChild(imageContainer); + document.body.appendChild(this.container); + + console.log("✅ Watch Point Floating: Created"); + } + + setupDragging(dragElement) { + dragElement.addEventListener("mousedown", (e) => { + this.isDragging = true; + + const rect = this.container.getBoundingClientRect(); + this.currentX = rect.left; + this.currentY = rect.top; + + this.initialX = e.clientX - this.currentX; + this.initialY = e.clientY - this.currentY; + + document.addEventListener("mousemove", this.drag); + document.addEventListener("mouseup", this.stopDrag); + }); + } + + drag = (e) => { + if (!this.isDragging) return; + + e.preventDefault(); + this.currentX = e.clientX - this.initialX; + this.currentY = e.clientY - this.initialY; + + this.container.style.left = this.currentX + "px"; + this.container.style.top = this.currentY + "px"; + } + + stopDrag = () => { + this.isDragging = false; + document.removeEventListener("mousemove", this.drag); + document.removeEventListener("mouseup", this.stopDrag); + + this.position.x = this.currentX; + this.position.y = this.currentY; + } + + updateImage(imageInfo) { + if (!this.container) return; + + const url = api.apiURL( + `/view?filename=${encodeURIComponent(imageInfo.filename)}&type=${imageInfo.type}&subfolder=${encodeURIComponent(imageInfo.subfolder || '')}` + ); + + this.img.src = url; + this.img.style.display = "block"; + + const placeholder = document.getElementById("watchpoint-placeholder"); + if (placeholder) { + placeholder.style.display = "none"; + } + + if (!this.visible && this.config.behavior.autoShow) { + this.show(); + } + + console.log("👁️ Watch Point Floating: Updated image"); + } + + show() { + if (!this.container) return; + this.container.style.display = "block"; + this.visible = true; + } + + hide() { + if (!this.container) return; + this.container.style.display = "none"; + this.visible = false; + } + + toggle() { + if (this.visible) { + this.hide(); + } else { + this.show(); + } + } + + increaseOpacity() { + if (this.currentOpacityIndex < this.opacityLevels.length - 1) { + this.currentOpacityIndex++; + this.applyOpacity(); + } + } + + decreaseOpacity() { + if (this.currentOpacityIndex > 0) { + this.currentOpacityIndex--; + this.applyOpacity(); + } + } + + applyOpacity() { + if (!this.container) return; + + const opacity = this.opacityLevels[this.currentOpacityIndex]; + this.container.style.opacity = opacity; + + if (this.config.opacity.showIndicator) { + this.showOpacityIndicator(opacity); + } + } + + showOpacityIndicator(opacity) { + if (this.opacityIndicator) { + this.opacityIndicator.remove(); + } + + this.opacityIndicator = document.createElement("div"); + this.opacityIndicator.textContent = `Opacity: ${Math.round(opacity * 100)}%`; + this.opacityIndicator.style.cssText = ` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px 20px; + border-radius: 5px; + font-size: 16px; + font-weight: bold; + z-index: 10000; + pointer-events: none; + `; + + this.container.appendChild(this.opacityIndicator); + + setTimeout(() => { + if (this.opacityIndicator) { + this.opacityIndicator.remove(); + this.opacityIndicator = null; + } + }, this.config.opacity.indicatorDuration); + } +} + +console.log("✅ Watch Point Floating Extension Loaded"); \ No newline at end of file diff --git a/watch_point.py b/watch_point.py new file mode 100644 index 0000000..2b3f032 --- /dev/null +++ b/watch_point.py @@ -0,0 +1,756 @@ +import os +import json +import tkinter as tk +from tkinter import filedialog, messagebox +from PIL import Image, ImageTk +import numpy as np +from threading import Thread, Lock +import io +import folder_paths +import torch + +# Icon Base64 (opcional, pega tu icono aquí) +ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsSAAALEgHS3X78AAAH40lEQVR4nO2dX0xb1x3Hv9exjamHr0GRNzRMAFXrGgztolQFVXRWI5VJCwp9yNI401SpKJOWvoSX9Q" + +try: + from screeninfo import get_monitors + SCREENINFO_AVAILABLE = True +except ImportError: + SCREENINFO_AVAILABLE = False + + +class WatchPoint: + """ + Watch Point - Dual Preview Monitor + Shows image in external monitor (Tkinter) and/or floating preview (ComfyUI built-in) + + Controls (Monitor Preview): + - Mouse wheel: Zoom in/out + - Left click + drag: Pan image + - Right click: Context menu + - T key: Toggle toolbar + - R key: Reset zoom and position + - 1 key: Zoom 1:1 (100%) + - ESC: Close window + """ + _windows = {} # monitor_idx -> window data + + @classmethod + def INPUT_TYPES(cls): + monitor_list = cls.get_monitors() + return { + "required": { + "images": ("IMAGE",), + "floating_preview": ("BOOLEAN", {"default": True}), + "monitor_preview": ("BOOLEAN", {"default": True}), + "monitor": (monitor_list, {"default": monitor_list[0]}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "watch" + OUTPUT_NODE = True + CATEGORY = "image/preview" + + @classmethod + def get_monitors(cls): + monitors = [] + if SCREENINFO_AVAILABLE: + try: + actual_monitors = get_monitors() + for i, m in enumerate(actual_monitors): + monitors.append(f"Monitor {i} ({m.width}x{m.height})") + except Exception: + pass + + if not monitors: + monitors = ["Monitor 0 (1920x1080)"] + + return monitors + + def watch(self, images, floating_preview=True, monitor_preview=True, monitor="Monitor 0 (1920x1080)"): + + # Determine monitor index + try: + display_idx = int(monitor.split(" ")[1]) + except Exception: + display_idx = 0 + + # Handle Monitor Preview (Tkinter) + if monitor_preview: + self._show_monitor_preview(images, display_idx) + else: + self._hide_monitor_preview(display_idx) + + # Handle Floating Preview (ComfyUI built-in) + # For floating preview to work, we need to save the image temporarily + # and return it in the expected format + result = [] + if floating_preview: + # Save preview images so ComfyUI's preview system can pick them up + result = self._prepare_preview(images) + + return {"ui": {"images": result}, "result": (images,)} + + def _show_monitor_preview(self, images, display_idx): + """Show image in external monitor using Tkinter""" + + # Convert tensor to PIL Image + image = images[0] + image = 255.0 * image.cpu().numpy() + pil_img = Image.fromarray(np.clip(image, 0, 255).astype(np.uint8)) + + # Check if window exists and is still running + if display_idx in self._windows: + # Check if the window is actually alive + if not self._windows[display_idx].get("running", False): + # Window was closed, remove it and create new one + try: + if "thread" in self._windows[display_idx]: + self._windows[display_idx]["thread"].join(timeout=0.1) + except: + pass + del self._windows[display_idx] + + # Create or update window + if display_idx not in self._windows: + lock = Lock() + self._windows[display_idx] = { + "image": pil_img, + "lock": lock, + "running": True, + "root": None, + } + t = Thread(target=self._window_loop, args=(display_idx,), daemon=True) + self._windows[display_idx]["thread"] = t + t.start() + else: + # Update existing window + try: + with self._windows[display_idx]["lock"]: + self._windows[display_idx]["image"] = pil_img + except: + # If lock fails, window might be dead, recreate it + del self._windows[display_idx] + self._show_monitor_preview(images, display_idx) + + def _hide_monitor_preview(self, display_idx): + """Hide monitor preview""" + if display_idx in self._windows: + self._windows[display_idx]["running"] = False + + def _prepare_preview(self, images): + """Prepare images for ComfyUI's preview system (enables floating preview)""" + results = [] + for i, tensor in enumerate(images): + # Convert tensor to numpy + array = 255.0 * tensor.cpu().numpy() + img = Image.fromarray(np.clip(array, 0, 255).astype(np.uint8)) + + # Save to temp directory with unique timestamp + output_dir = folder_paths.get_temp_directory() + + # Generate unique filename with timestamp to avoid caching issues + import time + timestamp = int(time.time() * 1000) # milliseconds + filename = f"watchpoint_{timestamp:013d}_{i:02d}.png" + + file_path = os.path.join(output_dir, filename) + img.save(file_path, compress_level=4) + + results.append({ + "filename": filename, + "subfolder": "", + "type": "temp" + }) + + return results + + def _window_loop(self, display_idx): + """Main window loop with Tkinter""" + + # Load settings + settings = self._load_settings() + + # Create Tkinter window + root = tk.Tk() + root.title(f"Watch Point - Monitor {display_idx}") + + # Configure window icon if exists + if ICON_BASE64: + try: + icon_img = tk.PhotoImage(data=ICON_BASE64) + root.iconphoto(True, icon_img) + except Exception as e: + print(f"Could not set window icon: {e}") + + # Set window size from settings + window_size_mode = settings.get("window_size_mode", "fixed") + + if window_size_mode in ["Half Vertical", "Half Horizontal", "Quarter"]: + # Get screen dimensions + screen_w = root.winfo_screenwidth() + screen_h = root.winfo_screenheight() + + # Calculate dynamic size + if window_size_mode == "Half Vertical": + window_width = screen_w // 2 + window_height = screen_h - 100 + elif window_size_mode == "Half Horizontal": + window_width = screen_w - 100 + window_height = screen_h // 2 + elif window_size_mode == "Quarter": + window_width = screen_w // 2 + window_height = screen_h // 2 + else: + # Use fixed size from settings + window_width = settings.get("window_width", 800) + window_height = settings.get("window_height", 600) + + root.geometry(f"{window_width}x{window_height}") + + # Store root reference + self._windows[display_idx]["root"] = root + + # Create preview window instance + preview = WatchPointWindow(root, display_idx, self._windows, settings) + + # Position window based on settings or monitor position + window_x = settings.get("window_x", None) + window_y = settings.get("window_y", None) + + if window_x is not None and window_y is not None: + # Use saved position from settings + root.geometry(f"+{window_x}+{window_y}") + elif SCREENINFO_AVAILABLE: + # Fallback to monitor position + try: + monitor_info = get_monitors()[display_idx] + x = monitor_info.x + 50 + y = monitor_info.y + 50 + root.geometry(f"+{x}+{y}") + except Exception: + pass + + # Start Tkinter main loop + try: + root.mainloop() + except Exception as e: + print(f"Watch Point window error: {e}") + finally: + self._windows[display_idx]["running"] = False + + def _load_settings(self): + """Load settings from JSON file""" + settings_file = os.path.join(os.path.dirname(__file__), "watchpoint_settings.json") + + default_settings = { + "window_width": 800, + "window_height": 600, + "window_x": None, + "window_y": None, + "show_toolbar": True, + "save_format": "png", + "jpeg_quality": 90, + } + + try: + if os.path.exists(settings_file): + with open(settings_file, 'r') as f: + user_settings = json.load(f) + return {**default_settings, **user_settings} + except Exception as e: + print(f"Could not load settings: {e}") + + return default_settings + + +class WatchPointWindow: + """Tkinter-based preview window with zoom/pan""" + + def __init__(self, root, display_idx, windows_dict, settings): + self.root = root + self.display_idx = display_idx + self.windows = windows_dict + self.settings = settings + + # Zoom/Pan state + self.zoom_level = 1.0 + self.pan_x = 0 + self.pan_y = 0 + self.is_dragging = False + self.drag_start_x = 0 + self.drag_start_y = 0 + self.zoom_1to1_active = False + + # Current image + self.current_pil_image = None + self.photo_image = None + + # Setup UI + self._create_ui() + self._bind_events() + + # Start update loop + self._update_image() + + def _create_ui(self): + """Create UI elements""" + + # Main container + self.main_frame = tk.Frame(self.root, bg='#1a1a1a') + self.main_frame.pack(fill=tk.BOTH, expand=True) + + # Toolbar + self.toolbar_visible = self.settings.get("show_toolbar", True) + self._create_toolbar() + + # Canvas for image display + self.canvas = tk.Canvas(self.main_frame, bg='#000000', highlightthickness=0) + self.canvas.pack(fill=tk.BOTH, expand=True) + + # Create context menu + self._create_context_menu() + + def _create_toolbar(self): + """Create toolbar with buttons""" + self.toolbar = tk.Frame(self.main_frame, bg='#2a2a2a', height=40) + + # Buttons + btn_style = {'bg': '#3a3a3a', 'fg': 'white', 'relief': tk.FLAT, 'padx': 8, 'pady': 4} + + tk.Button(self.toolbar, text="↻ Reset", command=self._reset_zoom, **btn_style).pack(side=tk.LEFT, padx=2) + tk.Button(self.toolbar, text="⊕ Zoom In", command=self._zoom_in, **btn_style).pack(side=tk.LEFT, padx=2) + tk.Button(self.toolbar, text="⊖ Zoom Out", command=self._zoom_out, **btn_style).pack(side=tk.LEFT, padx=2) + tk.Button(self.toolbar, text="1:1 (100%)", command=self._zoom_1to1, **btn_style).pack(side=tk.LEFT, padx=2) + + # Window size dropdown + self.size_var = tk.StringVar(value="Current") + size_options = [ + "800x600", + "1024x768", + "1280x720", + "1920x1080", + "---", + "Half Vertical", + "Half Horizontal", + "Quarter" + ] + + size_menu = tk.OptionMenu(self.toolbar, self.size_var, *size_options, + command=self._change_window_size) + size_menu.config(bg='#3a3a3a', fg='white', relief=tk.FLAT) + size_menu.pack(side=tk.LEFT, padx=2) + + tk.Button(self.toolbar, text="⛶ Fullscreen", command=self._toggle_fullscreen, **btn_style).pack(side=tk.LEFT, padx=2) + tk.Button(self.toolbar, text="⚙ Settings", command=self._open_settings, **btn_style).pack(side=tk.LEFT, padx=2) + + if self.toolbar_visible: + self.toolbar.pack(side=tk.TOP, fill=tk.X) + + def _create_context_menu(self): + """Create right-click context menu""" + self.context_menu = tk.Menu(self.root, tearoff=0) + self.context_menu.add_command(label="Save Image As...", command=self._save_image) + self.context_menu.add_command(label="Copy to Clipboard", command=self._copy_to_clipboard) + self.context_menu.add_separator() + self.context_menu.add_command(label="Zoom 1:1 (100%)", command=self._zoom_1to1) + self.context_menu.add_command(label="Reset Zoom", command=self._reset_zoom) + + def _bind_events(self): + """Bind keyboard and mouse events""" + + # Mouse events + self.canvas.bind("", self._on_mouse_down) + self.canvas.bind("", self._on_mouse_drag) + self.canvas.bind("", self._on_mouse_up) + self.canvas.bind("", self._on_mouse_wheel) + self.canvas.bind("", self._show_context_menu) + + # Keyboard events + self.root.bind("", lambda e: self._reset_zoom()) + self.root.bind("", lambda e: self._reset_zoom()) + self.root.bind("", lambda e: self._toggle_toolbar()) + self.root.bind("", lambda e: self._toggle_toolbar()) + self.root.bind("", lambda e: self._zoom_1to1()) + self.root.bind("", lambda e: self._close_window()) + + # Window close event + self.root.protocol("WM_DELETE_WINDOW", self._close_window) + + def _update_image(self): + """Update displayed image from shared state""" + try: + if not self.windows[self.display_idx]["running"]: + # Window is closing, stop the loop gracefully + try: + self.root.quit() + except: + pass + return + + with self.windows[self.display_idx]["lock"]: + pil_img = self.windows[self.display_idx]["image"] + + if pil_img is not None: + if pil_img != self.current_pil_image: + self.current_pil_image = pil_img + self._render_image() + + self.root.after(33, self._update_image) # ~30 FPS + except Exception as e: + print(f"Update image error: {e}") + # If error, mark as not running and stop + self.windows[self.display_idx]["running"] = False + try: + self.root.quit() + except: + pass + + def _render_image(self): + """Render image on canvas with zoom/pan""" + if not self.current_pil_image: + return + + self.canvas.update_idletasks() + + canvas_width = self.canvas.winfo_width() + canvas_height = self.canvas.winfo_height() + + if canvas_width <= 1 or canvas_height <= 1: + self.root.after(100, self._render_image) + return + + try: + img_w, img_h = self.current_pil_image.size + + if self.zoom_1to1_active: + scale = 1.0 + else: + scale = min(canvas_width / img_w, canvas_height / img_h) * self.zoom_level + + new_w = max(1, int(img_w * scale)) + new_h = max(1, int(img_h * scale)) + + resized = self.current_pil_image.resize((new_w, new_h), Image.LANCZOS) + + x_pos = (canvas_width - new_w) // 2 + int(self.pan_x) + y_pos = (canvas_height - new_h) // 2 + int(self.pan_y) + + self.photo_image = ImageTk.PhotoImage(resized) + + self.canvas.delete("all") + self.canvas.create_image(x_pos, y_pos, anchor=tk.NW, image=self.photo_image) + except Exception as e: + print(f"Render error: {e}") + + def _on_mouse_wheel(self, event): + """Handle mouse wheel for zoom""" + if not self.zoom_1to1_active: + if event.delta > 0: + self._zoom_in() + else: + self._zoom_out() + + def _on_mouse_down(self, event): + """Start dragging""" + self.is_dragging = True + self.drag_start_x = event.x + self.drag_start_y = event.y + + def _on_mouse_drag(self, event): + """Handle dragging for pan""" + if self.is_dragging: + dx = event.x - self.drag_start_x + dy = event.y - self.drag_start_y + self.pan_x += dx + self.pan_y += dy + self.drag_start_x = event.x + self.drag_start_y = event.y + self._render_image() + + def _on_mouse_up(self, event): + """Stop dragging""" + self.is_dragging = False + + def _show_context_menu(self, event): + """Show context menu on right click""" + try: + self.context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + + def _zoom_in(self): + """Zoom in""" + self.zoom_1to1_active = False + self.zoom_level = min(10.0, self.zoom_level * 1.2) + self._render_image() + + def _zoom_out(self): + """Zoom out""" + self.zoom_1to1_active = False + self.zoom_level = max(0.1, self.zoom_level / 1.2) + self._render_image() + + def _zoom_1to1(self): + """Zoom to 1:1 (100% actual size)""" + self.zoom_1to1_active = True + self.pan_x = 0 + self.pan_y = 0 + self._render_image() + + def _reset_zoom(self): + """Reset zoom and pan""" + self.zoom_1to1_active = False + self.zoom_level = 1.0 + self.pan_x = 0 + self.pan_y = 0 + self._render_image() + + def _toggle_toolbar(self): + """Toggle toolbar visibility""" + if self.toolbar_visible: + self.toolbar.pack_forget() + self.toolbar_visible = False + else: + self.toolbar.pack(side=tk.TOP, fill=tk.X, before=self.canvas) + self.toolbar_visible = True + + def _change_window_size(self, size_str): + """Change window size""" + if size_str == "---": + return + + try: + screen_w = self.root.winfo_screenwidth() + screen_h = self.root.winfo_screenheight() + + if size_str == "Half Vertical": + w = screen_w // 2 + h = screen_h - 100 + self.root.geometry(f"{w}x{h}") + elif size_str == "Half Horizontal": + w = screen_w - 100 + h = screen_h // 2 + self.root.geometry(f"{w}x{h}") + elif size_str == "Quarter": + w = screen_w // 2 + h = screen_h // 2 + self.root.geometry(f"{w}x{h}") + else: + w, h = map(int, size_str.split('x')) + self.root.geometry(f"{w}x{h}") + except Exception as e: + print(f"Error changing window size: {e}") + + def _toggle_fullscreen(self): + """Toggle fullscreen mode""" + current_state = self.root.attributes('-fullscreen') + self.root.attributes('-fullscreen', not current_state) + + def _save_image(self): + """Save current image to file""" + if not self.current_pil_image: + messagebox.showwarning("No Image", "No image to save") + return + + save_format = self.settings.get("save_format", "png").upper() + + filetypes = [ + ("PNG Image", "*.png"), + ("JPEG Image", "*.jpg"), + ("WebP Image", "*.webp"), + ("All Files", "*.*") + ] + + filename = filedialog.asksaveasfilename( + defaultextension=f".{save_format.lower()}", + filetypes=filetypes, + title="Save Image As" + ) + + if filename: + try: + if filename.lower().endswith('.jpg') or filename.lower().endswith('.jpeg'): + quality = self.settings.get("jpeg_quality", 90) + self.current_pil_image.save(filename, "JPEG", quality=quality) + else: + self.current_pil_image.save(filename) + messagebox.showinfo("Success", f"Image saved to:\n{filename}") + except Exception as e: + messagebox.showerror("Error", f"Failed to save image:\n{str(e)}") + + def _copy_to_clipboard(self): + """Copy image to clipboard""" + if not self.current_pil_image: + messagebox.showwarning("No Image", "No image to copy") + return + + try: + output = io.BytesIO() + self.current_pil_image.convert('RGB').save(output, 'BMP') + data = output.getvalue()[14:] + output.close() + + import win32clipboard + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data) + win32clipboard.CloseClipboard() + + messagebox.showinfo("Success", "Image copied to clipboard") + except ImportError: + messagebox.showerror("Error", "pywin32 not installed. Install with: pip install pywin32") + except Exception as e: + messagebox.showerror("Error", f"Failed to copy:\n{str(e)}") + + def _open_settings(self): + """Open settings dialog""" + WatchPointSettingsDialog(self.root, self.settings) + + def _close_window(self): + """Close window properly""" + self.windows[self.display_idx]["running"] = False + try: + self.root.destroy() + except: + pass + + +class WatchPointSettingsDialog: + """Settings dialog window""" + + def __init__(self, parent, settings): + self.settings = settings + self.dialog = tk.Toplevel(parent) + self.dialog.title("Watch Point Settings") + self.dialog.geometry("450x650") + self.dialog.resizable(True, True) + + self._create_ui() + + def _create_ui(self): + """Create settings UI""" + + # Window Size section + size_frame = tk.LabelFrame(self.dialog, text="Default Window Size", padx=10, pady=10) + size_frame.pack(fill=tk.X, padx=10, pady=10) + + current_mode = self.settings.get("window_size_mode", "fixed") + if current_mode in ["Half Vertical", "Half Horizontal", "Quarter"]: + initial_value = current_mode + else: + initial_value = f"{self.settings.get('window_width', 800)}x{self.settings.get('window_height', 600)}" + + self.size_var = tk.StringVar(value=initial_value) + + sizes = [ + "800x600", + "1024x768", + "1280x720", + "1920x1080", + "Half Vertical", + "Half Horizontal", + "Quarter" + ] + + for size in sizes: + tk.Radiobutton(size_frame, text=size, variable=self.size_var, value=size).pack(anchor=tk.W) + + # Window Position section + position_frame = tk.LabelFrame(self.dialog, text="Window Position", padx=10, pady=10) + position_frame.pack(fill=tk.X, padx=10, pady=10) + + tk.Label(position_frame, text="X coordinate:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.pos_x_var = tk.StringVar(value=str(self.settings.get("window_x", ""))) + tk.Entry(position_frame, textvariable=self.pos_x_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=5) + tk.Label(position_frame, text="(leave empty for auto)").grid(row=0, column=2, sticky=tk.W) + + tk.Label(position_frame, text="Y coordinate:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.pos_y_var = tk.StringVar(value=str(self.settings.get("window_y", ""))) + tk.Entry(position_frame, textvariable=self.pos_y_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=5) + tk.Label(position_frame, text="(leave empty for auto)").grid(row=1, column=2, sticky=tk.W) + + # Save Format section + format_frame = tk.LabelFrame(self.dialog, text="Save Settings", padx=10, pady=10) + format_frame.pack(fill=tk.X, padx=10, pady=10) + + tk.Label(format_frame, text="Default Format:").grid(row=0, column=0, sticky=tk.W) + self.format_var = tk.StringVar(value=self.settings.get("save_format", "png").upper()) + format_menu = tk.OptionMenu(format_frame, self.format_var, "PNG", "JPEG", "WEBP") + format_menu.grid(row=0, column=1, sticky=tk.W, padx=5) + + tk.Label(format_frame, text="JPEG Quality:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.quality_var = tk.IntVar(value=self.settings.get("jpeg_quality", 90)) + quality_scale = tk.Scale(format_frame, from_=10, to=100, orient=tk.HORIZONTAL, + variable=self.quality_var, length=200) + quality_scale.grid(row=1, column=1, sticky=tk.W, padx=5) + + # UI Options + ui_frame = tk.LabelFrame(self.dialog, text="UI Options", padx=10, pady=10) + ui_frame.pack(fill=tk.X, padx=10, pady=10) + + self.toolbar_var = tk.BooleanVar(value=self.settings.get("show_toolbar", True)) + tk.Checkbutton(ui_frame, text="Show toolbar by default", variable=self.toolbar_var).pack(anchor=tk.W) + + # Buttons at bottom + btn_frame = tk.Frame(self.dialog, pady=10) + btn_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10) + + tk.Button(btn_frame, text="Cancel", command=self.dialog.destroy, width=10, + bg='#666', fg='white').pack(side=tk.RIGHT, padx=5) + tk.Button(btn_frame, text="Save", command=self._save, width=10, + bg='#4a9eff', fg='white').pack(side=tk.RIGHT, padx=5) + + def _save(self): + """Save settings""" + size_value = self.size_var.get() + + if size_value in ["Half Vertical", "Half Horizontal", "Quarter"]: + self.settings["window_size_mode"] = size_value + else: + try: + w, h = map(int, size_value.split('x')) + self.settings["window_width"] = w + self.settings["window_height"] = h + self.settings["window_size_mode"] = "fixed" + except: + pass + + try: + x_str = self.pos_x_var.get().strip() + y_str = self.pos_y_var.get().strip() + + if x_str and y_str: + self.settings["window_x"] = int(x_str) + self.settings["window_y"] = int(y_str) + else: + self.settings["window_x"] = None + self.settings["window_y"] = None + except ValueError: + messagebox.showwarning("Invalid Input", "X and Y coordinates must be valid numbers") + return + + self.settings["save_format"] = self.format_var.get().lower() + self.settings["jpeg_quality"] = self.quality_var.get() + self.settings["show_toolbar"] = self.toolbar_var.get() + + settings_file = os.path.join(os.path.dirname(__file__), "watchpoint_settings.json") + try: + with open(settings_file, 'w') as f: + json.dump(self.settings, f, indent=2) + messagebox.showinfo("Success", "Settings saved!") + self.dialog.destroy() + except Exception as e: + messagebox.showerror("Error", f"Could not save settings:\n{str(e)}") + + +# Node registration +NODE_CLASS_MAPPINGS = { + "WatchPoint": WatchPoint +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WatchPoint": "👁️ Watch Point" +} \ No newline at end of file diff --git a/watchpoint_settings.json b/watchpoint_settings.json new file mode 100644 index 0000000..5f03380 --- /dev/null +++ b/watchpoint_settings.json @@ -0,0 +1,10 @@ +{ + "window_width": 800, + "window_height": 600, + "window_x": 0, + "window_y": 0, + "show_toolbar": true, + "save_format": "png", + "jpeg_quality": 90, + "window_size_mode": "Half Vertical" +} \ No newline at end of file