From 6a841a86df73f8bdd2ed97f61a103a9bc9e1acd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez?= Date: Thu, 17 Jul 2014 10:23:22 +0000 Subject: [PATCH] Updated to Guacamole 0.9.1, now closes the session correctly --- guacamole-tunnel/pom.xml | 4 +- .../org/openuds/guacamole/TunnelServlet.java | 2 +- .../webapp/images/action-icons/guac-back.png | Bin 0 -> 586 bytes .../webapp/images/action-icons/guac-close.png | Bin 704 -> 512 bytes .../images/action-icons/guac-config.png | Bin 0 -> 1230 bytes .../images/action-icons/guac-delete.png | Bin 0 -> 611 bytes .../images/action-icons/guac-first-page.png | Bin 0 -> 690 bytes .../images/action-icons/guac-group-add.png | Bin 0 -> 525 bytes .../images/action-icons/guac-last-page.png | Bin 0 -> 707 bytes .../images/action-icons/guac-logout.png | Bin 0 -> 1024 bytes .../images/action-icons/guac-monitor-add.png | Bin 0 -> 560 bytes .../images/action-icons/guac-next-page.png | Bin 0 -> 626 bytes .../images/action-icons/guac-prev-page.png | Bin 0 -> 648 bytes .../images/action-icons/guac-user-add.png | Bin 0 -> 810 bytes .../webapp/images/group-icons/guac-closed.png | Bin 0 -> 843 bytes .../webapp/images/group-icons/guac-open.png | Bin 0 -> 717 bytes .../src/main/webapp/images/guac-mono-192.png | Bin 0 -> 6041 bytes .../main/webapp/images/guacamole-logo-144.png | Bin 0 -> 9167 bytes .../main/webapp/images/guacamole-logo-24.png | Bin 0 -> 1520 bytes .../src/main/webapp/images/mouse/blank.cur | Bin 584 -> 326 bytes .../webapp/images/noguacamole-logo-24.png | Bin 0 -> 1245 bytes .../images/protocol-icons/guac-monitor.png | Bin 0 -> 691 bytes .../images/protocol-icons/guac-plug.png | Bin 0 -> 727 bytes .../images/protocol-icons/guac-text.png | Bin 0 -> 792 bytes .../webapp/images/settings/tablet-keys.png | Bin 0 -> 3175 bytes .../main/webapp/images/settings/touchpad.png | Bin 0 -> 38013 bytes .../webapp/images/settings/touchscreen.png | Bin 0 -> 24025 bytes .../main/webapp/images/settings/zoom-in.png | Bin 0 -> 1553 bytes .../main/webapp/images/settings/zoom-out.png | Bin 0 -> 1521 bytes .../webapp/images/user-icons/guac-user.png | Bin 0 -> 1049 bytes guacamole-tunnel/src/main/webapp/index.xhtml | 263 +- .../src/main/webapp/scripts/admin-ui.js | 99 +- .../src/main/webapp/scripts/client-ui.js | 2539 +++++++++++++---- .../src/main/webapp/scripts/guac-ui.js | 402 ++- .../src/main/webapp/scripts/history.js | 212 ++ .../src/main/webapp/scripts/service.js | 84 +- .../src/main/webapp/scripts/session.js | 207 +- .../src/main/webapp/styles/admin.css | 65 + .../src/main/webapp/styles/animation.css | 53 + .../src/main/webapp/styles/client.css | 469 ++- .../src/main/webapp/styles/keyboard.css | 153 + .../src/main/webapp/styles/login.css | 364 +++ .../src/main/webapp/styles/ui.css | 626 ++++ 43 files changed, 4408 insertions(+), 1134 deletions(-) create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-back.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-config.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-delete.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-first-page.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-group-add.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-last-page.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-logout.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-monitor-add.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-next-page.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-prev-page.png create mode 100644 guacamole-tunnel/src/main/webapp/images/action-icons/guac-user-add.png create mode 100644 guacamole-tunnel/src/main/webapp/images/group-icons/guac-closed.png create mode 100644 guacamole-tunnel/src/main/webapp/images/group-icons/guac-open.png create mode 100644 guacamole-tunnel/src/main/webapp/images/guac-mono-192.png create mode 100644 guacamole-tunnel/src/main/webapp/images/guacamole-logo-144.png create mode 100644 guacamole-tunnel/src/main/webapp/images/guacamole-logo-24.png create mode 100644 guacamole-tunnel/src/main/webapp/images/noguacamole-logo-24.png create mode 100644 guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-monitor.png create mode 100644 guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-plug.png create mode 100644 guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-text.png create mode 100644 guacamole-tunnel/src/main/webapp/images/settings/tablet-keys.png create mode 100644 guacamole-tunnel/src/main/webapp/images/settings/touchpad.png create mode 100644 guacamole-tunnel/src/main/webapp/images/settings/touchscreen.png create mode 100644 guacamole-tunnel/src/main/webapp/images/settings/zoom-in.png create mode 100644 guacamole-tunnel/src/main/webapp/images/settings/zoom-out.png create mode 100644 guacamole-tunnel/src/main/webapp/images/user-icons/guac-user.png create mode 100644 guacamole-tunnel/src/main/webapp/scripts/history.js create mode 100644 guacamole-tunnel/src/main/webapp/styles/admin.css create mode 100644 guacamole-tunnel/src/main/webapp/styles/animation.css create mode 100644 guacamole-tunnel/src/main/webapp/styles/keyboard.css create mode 100644 guacamole-tunnel/src/main/webapp/styles/login.css create mode 100644 guacamole-tunnel/src/main/webapp/styles/ui.css diff --git a/guacamole-tunnel/pom.xml b/guacamole-tunnel/pom.xml index fcc5a2f8..300f4c5d 100644 --- a/guacamole-tunnel/pom.xml +++ b/guacamole-tunnel/pom.xml @@ -7,7 +7,7 @@ org.openuds.server transport war - 1.2.1 + 1.5.0 Guacamole Transport http://openuds.org/ @@ -70,7 +70,7 @@ org.glyptodon.guacamole guacamole-common-js - 0.9.0 + 0.9.1 zip runtime diff --git a/guacamole-tunnel/src/main/java/org/openuds/guacamole/TunnelServlet.java b/guacamole-tunnel/src/main/java/org/openuds/guacamole/TunnelServlet.java index b5364649..8c110bfa 100644 --- a/guacamole-tunnel/src/main/java/org/openuds/guacamole/TunnelServlet.java +++ b/guacamole-tunnel/src/main/java/org/openuds/guacamole/TunnelServlet.java @@ -78,7 +78,7 @@ public class TunnelServlet throw new GuacamoleException("Can't access required user credentials"); } - System.out.println("Got parameters from remote server"); + System.out.println("Got parameters from remote server: " + data + ", " + width + "x" + height); GuacamoleClientInformation info = new GuacamoleClientInformation(); info.setOptimalScreenWidth(Integer.parseInt(width)); diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-back.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-back.png new file mode 100644 index 0000000000000000000000000000000000000000..6435f4a7329dd7b19538fc571fbee90b695dc4b7 GIT binary patch literal 586 zcmV-Q0=4~#P)&G`ggbBv!mp#@Q>LNdd^Gd{gv2M@g$s~~B2p+=ODGaSo1L*| zVviI>DV*$k@s8K_xLPikzTm>}1q3I@ysZKTYv5HR)W+Z^|GxaVEM zO9)s1a~wxs0dwFAc-|oSiY{-b%mJFoNO-j>A;y}ZX$i4bg=bbJ#M~D!DIxaGkU0q{Hb;YmHwzL{?GDd? z3kwob)(4o7kh(%7xdbakQoH;VNu73V*Sko{N}1dYI0vR3+H6IU)TR=)qDX38PMF4k zwL zh<7H0e}CdV3gN7H3WOc;AqW}bFNN?%d^dzF@s~h2D836qrud-{I>jG`kS%@)grN9C z5Jrekg>X{*TL^oTt|uoQ$^XxHko)p1?*njiBKp4+B2k$+4){Auj z*4hDB2Vkuofa2T{fMW%skVA+?ChxuE`1C=)-g_q;gK@YRgO4#~kD)Q2WCh{cd{Q<9 zt$8a5kIai8FwED6FlRm+0?T|Y2y4t|L13CM4I#&T(jo-5`6M<3iTR{i2qnxXu^?#7 zCrv__nsii7n17^&z%ZXQKAK}H6Z`^j7H3!g8UTDlmNB|`?m&p+jNs{BmFVYw6ROSK zOdwWsHgz>B&$Svrt-wrva#nZb+W^h(HwW*tilXd0^ILq+9`m&Jgj@gs7s4gJz7odD zHV?=E09*(RiOd7?lGrIu$Vis75}OBfHNh4jsLTWUs(-L01hsj9Qx>;?Krs*S`T{i} z(98qe#z+kaRPzA8GgJ}+-8>-A1D50f{D70-03l&If@~hxdxJ#)q%4tj^%2LCR+eVP z22e6Z)|A&K#RO1eeL#o60#I{tM1{ZrV6if!LC^xQTozLxXaKZO6Hr5t0%*Z}k_v(Z z0J|i1S%3W0vcwQ_0kDgr$9UMa;ntRq3zx3V%Hl1UZ@w|ob!)8a=4jXLaZB@6W0?9I ze1}o5?n=z^+JNBcUNcvq>rGNx2%Fd@Cxx)7jgq+#Hn~-HErd;PmR^F;#di5A2wgoe zaSP$;ADPj##zutgr}TDJrW`_&^C3C8RB!NUv?2Eftpl*u4!}A9YyANN&{!FBajrQ4 O0000h%jm+`gqV>W7jpu`aNpb?@7?5OX1;J2^WM4Vo^#&4 z_x!kbdPIayMu84T*8rVvfG-YV%n_g`;hg}h?QDQD@W?`Q8F<{TOW;;UG?}620Nr3e z@EZ8aLiYhU0c^F~47lc?JW6w|9hbmlB-%qWKnWNJiWdGAU=bMe;60{&Di#@wDnlt) z3lRxvB@tPZI{p`t1reDPks*CQC?ZoL@>0^{rSck7-a|Upf{raJODRN`kWH`zECJsp z`0;1JH^7$}&(FmvMu@S2|flmpL69Kjc`83 z5-0-iG@wa?x4?5?LmR+J4Svo7$5Zm(0tSJPS?2;Dfrr44flAK-ziFJm*J4JNb&Q*V z#(3zee25#I^mJj{A~GW)0|72-K>KccjydTwj;~NdBC_Ww$P*Dc6&PnUwo}T>1$zy# zh*R^81piY%Z^OK%wXZC6$F#M=E9EusFzz#zFfdaa^`^>3GYs8=z0mb2n_4< z7r+x>MML1f6Z+o=u3LhouXVGIvIk5#O#41Vzg`iU7Ll66G_EOauYpgW!*ZC4wFeX| zvHf?h6K(&C)*YQ(0FnS?7U^Mo~WO|VQ^ zMDB^mrsG!{ZR&s8al>;(O?ftsG3wZNQ*|l3E`bqG;i`sB=eA^z+lEcK>d9loWeN0n zPSNLv*#1(4EU%g89`KiM6+A{!bi?rMVgv)2c)rduKBZZ&o;41gv!S;NHhL<|G?*2&Kp_^d=Qz=}ckv%be5E(TcD{QGC> z2bDG)*D_Wk7J*M0D*s7&HlplUGW(#qT*h?wz!EF>i^znCEL#GMSwr9H41=I)1OHh| zpJkQ3znctYR1&*uSq68Sp6yb<>`+TF^=rywx9RyE%k;k+qaxJt1(CKaiFJ#j0d@MP z4qx$p!c1sx$F$k#`jn>)Pkr_29*lVI%2z$=1s8xdUi_@% literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-delete.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..925e958319ca538f8466ed012f93fc42957c3b23 GIT binary patch literal 611 zcmV-p0-XJcP)&i(c77RiG&1 z#-cv;CCdbK3PkZeCxJ~%$;>cwJgEy%$0G^iB_RTyDxnhop@gkP*UAMc5=ihrV=WQDT-YU{RSoh2)N6NB zh-%Hgu7*N*FJDOo{B8l!1*+hS1Vk08gfA8lAmK9)3A{@{fCL%5TR>+L>h3nZ6FZSm z{k%~nKrDgtuvMEtB!Tw0O%c#o0=L(|O4LXKz1u_;5G*04)y7WvO9JZ#$8xTaX6y9t z2G5_sAn^VF2{wQakuVxOUBZa)R0*TP(e6<8M{8%k9GW=Lg(a1uT66Ek| z30=S|C3FL?TCq(j2-w#pVrIPPZY9TJYeUH%^l^BkDCBVqU>Gbtx3j(kw*)f_iD x%*Y3~p3UsVHQy`XC^7xJ!Bc>`0Ch|+;0yH|yZa)&SRw!b002ovPDHLkV1oYC{7wJ> literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-first-page.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-first-page.png new file mode 100644 index 0000000000000000000000000000000000000000..86c7a97cc721070ce77696df0f7948b778ec545f GIT binary patch literal 690 zcmV;j0!{siP)9vxQ1whgj zunYW79Gd(`M{2Tt22=>235JmMTR_q!!4R^a2Sh_~ITLIk%o2w#9Kr2cphDQi8Nu~x zFv5}G{(FF=3mg&L0l*g_1OWIUgaH5_girwRh_FW{gm3@=lFrG55E20LA%q2hCkTHM zhrXUgQez{82LO6CVa z@)t?xi=aHM;Lv;Y7A07*qoM6N<$f(yJK00000 literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-group-add.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-group-add.png new file mode 100644 index 0000000000000000000000000000000000000000..be9b63014f651c3f582633dd2204eac9e3e70f8f GIT binary patch literal 525 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHjR^4?CmUMD5Ol3=E93JY5_^DsH{KxzUT+Q0Dl@_nlp3 z8=72YuX+j`JbN@yMrO+cZVRt?hdDAaHH>$UUR|iStCRnNW4OAytkuf`U0Iv5*RAOM{Sa2)VX3@TL%%<)+410P%E%$WE+`#+B zI_dm@wq*?rEDAs+3hzvQOc9P|des-fUvYBf?$^KkHn0VRq)%%M_hn?{aA06E*eAR1 zY-cW0ci#>64>N+zua-pw1BC^E1_+c&nOHI{Ys_Bo{$VBitc|@5$q()%*V{F=%1_X6 zJFxBfzcUQhsc%@r8krLw&HuWc>&0cSj0WWgLLNP9yo(o#ADp&ux;|^d-H5bRFP%LY z9&T<}y~Kfi|MRb31Nrrxn3wS6C+}k2a+^tSjenz`qdtFu{9&npCwiLQj1G}dCxH9_ zWUf$iSbU&NI*j4dIp%8(#t-;@e7f;WrywbH`HYlrdhF{PoYyikaj;)7zqULsqhXt2 z!|V%@4@71#ee;?p6x&ulg@57Z1MlCToW#gB@ABLEr_9gj#B?*A|BwTWI0jEwKbLh* G2~7Z>!p}JX literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-last-page.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-last-page.png new file mode 100644 index 0000000000000000000000000000000000000000..b03932c82d59c02391ba205a9eb9e7faac32201e GIT binary patch literal 707 zcmV;!0zCbRP)LaHoqODK58-CZ@)Q|okL#6XeP$}W32MJ4m z25XlPbs|)=gy>UY0|`5c>jp{E-{Bf1B=-Xp65caH30V!1nj~a*gbXC?=03M&R4p;3 zgdby&pzaBJmtfKq^(?`xE9_N*XCdYfJZDB<^qf`=Xi7{LFvuBj`g0=&V4p#TN^BZc6> zNPrT4uM|8O2=MyKho}S?!2i)azeTbI)Bq2e0&0PW3;{L4L!tmB{K44ZAwhrv{O`nd zgG4hV6_ZiJgMLV=D^H9Z9&CoB7R3cT*bPa&PJq8q)HPTvhorz6JlGCNxq}D0At{&e zU^6831RQ#V2mO##fIqh`Y^Z34q};U zK~#9!?VDMsO<@$rf9tx&2Qr0R59EPlDqdu&OBpg0%9G?tcrcW?lxqqvlov&!3=bY| zS6oAea49c_gp@+aEF#nO_prYswtcwHw9nq3f1Pgaz3=+Y`JZpC^3l{|Sh= z_QrPt1_777CBV$?10#SffV_+WX7<(0mI4!j7v2UzGdl(h1kQPz1+y&p19;a2I}?D% zNh4qtuP& zD&YJ21k7~7t^5d(G}1-pj0;T$4ixykeFGLyW59rjYhwr+riI3Of==|G(%>cF zSrcr1C=>zN%HE+x`l5CN&w)cg>xlbnbL~as8Us!OpR?5F#X_CiWv^n70cQ5pu^vW4 zkz-Z^!9dr;a!7+4fVsdu;EEG0*GrJOZn>i9W;$1W$4}!V78gP{1+YIgh~eQfTIzQz6bVF4#QVY5X{aZR31tio`(>+ zorlmV$tx2C_W)BPe6k8SD`};qinIp73~F6He<`BETMyJr>XP;}_y8CQZ07ed*|ES? zN!6*=0R)p@%Lon;o`>#r1x`q+k<=`$1Y}U}T;g2B|EvbiNUBOR0T3J`WgC+n;ap;3 znhD4h8*5w)2#?8j1nMMhmDD_~1Y~g9xx|UEm;#m%99#EH8UdC=6Ucz&&Mm_IgwOnX z0C@-Kla?LuL+}9`fzkZloNSS17YO5`bBVe?Jo7fq1VGTEpA0M}dQA3c8e>2<40Yx0 u&%+7>fE&IL0MLeDimMyYmf#oSWsUEe0 zf|81fcxOz^@@rZeUlv{Ic%jPG+vl}DP_xhJr06N_2^l8KEzi0-7F8FuhQ_SB!<$}K zruKV(*5v6oXZ|g__xt%-9|2dv%58QL*pH|K-cGl-?kH`_X1FAEhUvp9sG5R3GW)zftYx@t zxQ~Cqqzh&tmTSN=4J@J&EM|?3Jja9B^%|DtGFTsw6tMW<+}FtTA#bxx9a9(I*;BO- zuB7p4Scy$jojuo+bqUi+F{aQn@2>PPp0;D)c=_hAY0!)E^J$A-o3gCZthG6?mETg~ ztdZd-#)c3F1{P$bFrPs!=uP>h_P-6P4>)!(?&JMt$tSaeNulG8wX7}swyDf42jm*` zj~~0TiFd&pi3ac6S_K?DjISlzU7F8HK3$tD`ofMsjb}k_MBaZMh3VgBM<+!eShw(M zw5-KO+2{XfGH!O@-F*G!tOGnBULLku(C~A2i}Hav`rHext;#?6M6p(UQ)SsTA&mD~ fL$!>R)Ne*6v)GuP)a|0cxMlEk^>bP0l+XkK6Ij({ literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-next-page.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-next-page.png new file mode 100644 index 0000000000000000000000000000000000000000..d4022a452849b563cdac824157b623e6fa6b10d0 GIT binary patch literal 626 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4u?*!==k7JrvHL(6rt9@{5HH#^u_k-Cok3*X}Qp^(=YM3@H>TR6oHa}{u z+8%FC&FAL>yk2rtu$5igQ^GW->qPeJb5kd6W$QS>UBFngwlZ_G+LxkV;hs-T6E^x9 z)X99|Vx77mT4w>v-=?>!Hc=iL!6p-0idvgJG@@<%4>?va3aVV0nFf@aIxWDpA)WIo zOU2ZH&<1zTtE?516OO?htG3oBm1iL zjQkcezU`lVYT1g87{=#+?1kM9%$#j)-+yb}2QE(DKeNN?qXO@W#VaND1Xn1$4^POv zIDhiv;56=e-W3`u3~BSvFa)3f5D?%n1?c!`bNw3Nv9phH(PXRhnSC|!!1Tf3>FVdQ I&MBb@01Q|4MF0Q* literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/action-icons/guac-prev-page.png b/guacamole-tunnel/src/main/webapp/images/action-icons/guac-prev-page.png new file mode 100644 index 0000000000000000000000000000000000000000..6f6819dee966231443fcfde4c9127610446290d1 GIT binary patch literal 648 zcmV;30(bq1P)3hz{mVFF{qq9Ka%Q3FtS+Gq7tirZ@C2;7uh+gfBxPvPiSweb)A0M z;SYhe2`?Q1`+eX6-$#I^?XUQG+y9+4AOgIbfGF@T0$TX%2=LVfM1!v?AR>G<0l-p3 z_$mS-!xtCO!rw%NFDf7dd@%u8;EM>z1aB@N8@#E27XCI9yqSQk@FoI)<*e|%1!TVS zuSefsg-5_+&lo zsd<`BQkxTA+Iy2!h>en}`T%SL7ZY5Tf0NXh$KjlcYe0w&cum_0JAB;phH0yJ2>i4OX(nWD!2-c z4kGH(r4XTocB!?L7D=TsS|Pze25Bh-L`^i-@EwP|p#;8<^!whI_g=qy;7;e=chCKE z?z!jOd%Ge+Crh^umjoaINB}xpwpGa19q6f}yNBD&{Ish=+u#)5sH6K4ucRfA03-kj zKmw2eRC^noX`maz<8^fZV7@`$iF$cL*#NZ5n4MTmOCSMA01|)%pycK7Hw|>-c&d)> z4_t20_wQ5=rzNnpOJE427{E=;V)bDMFo9#)#QQhs)^H0KaT+V-$vq=lZQCQ_^VqyB zM8tt6#+hpH&zWZJZ#4$&!_Rp40s1_XMeI+|RpmUOA7?SxOcjdbc(2Ok@EN?-;`l2V z!9vwM;7AUWhhBV9d9phD60wZ`NXFJBK1Yoocc2o+$ zU>odsx~R&u82~@E!H!?Dbh#1$erSUo(^Xmmb6D&4W!v1e^)tb1N+j*N7Z|{v49YBi!@o)>t>)j)S--3p z#S?ha`h&*)4e@0azu(v9vjBPlcdOxkI3l|gW`AD)PaKVxfTDIzAyvgWhPQkpKVy07*qoM6N<$g3YUL2mk;8 literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/group-icons/guac-closed.png b/guacamole-tunnel/src/main/webapp/images/group-icons/guac-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa036be87c80d87bffb75ce8d79f642acc2f018 GIT binary patch literal 843 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4u{Xvq=_r8M-E{|I$ z8~Xa%giLtQc=*eL+c}!`X)~v8-}PB#vOwCix%Yo3x81!dyL7AC$(jjb3cbb-Q4J>N z7%~|zSuihQ^_X12Y%+!t_{Y`JE?Mk#acbgcu51#tpObac=0e-ZXr(sY-3j(zLDIlYi`Ozzo4P@|}!x=FcxKDM`7q?fUDpYj*G1lail*Uc+y1 z-0|Az+?OEt$oH)y*-b4+MQp2JfO>%4l%6LTw&U32d$|}(@#roefzbl z$8GV-QfX=F6eG#UhL07*m}~Cy%em?K`gS6)6nZL?&%O0v*?mN|@ zd2mlSCaIw7AeG*bxZ8gA``fo~hfhj6-(t3yoo!ip)7%HnyYKcXyo}Gg#b!Pk zug_$Ot9@L5ym#6G?*-WnA1i#eMs22?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4nJ>sXJITnD{l6Fl5eQc*$^yC+@)W zZLCv*;u|dunD=e*OF1TagPA|-wftJG%(eMh2V^HOo^KG#IzOGK-2D6Fw!Z$x>Vtmq z+20S`*k*s~S)_xeDVtnF{?+Pv&jnc+14Rty`t@dSRXkt3?Y3^@w7_Ype;2DKtlRo8 zFXqgrH~-6?J+`>!8knKkwdg{j%>R$R1~0v@<{bZXy77|9+pU}bEBRht#_~A3skdQG z-Lv~rz4=bdcJE<}S++}gfzA2H1r`r}@B3)*y7}?P6DdY3*KtaH*m_VTvr5^7UBp3v zC4b##hJEjaSscFxeboM3ZzyW8+H8@=Chu#?y*Zn+e_u5e{ZQ0?{d!0_cW;hc`O@Zk zu>z(g%pQ+fJlH23!FY*rNg+t{<&!!FQoRv6Vi36!B?TVmYp+YimsX{Ja6SMsv{zUG5$F%= zo@Xha{~X?RX`b7*&s+b#n!$A=T>AFgYf3@;O*Xw<|2F*iU)#K(KS3`97wC9TQpueB zHPmC$l6`S6E-v}p5UphV!!;;x=A7QO-D~)_23(4A@IBHVp_OgQ^6yx6yw2UI1J%cE gxnbTxve}-I$t7=TtHqHQz@*3E>FVdQ&MBb@0NsExrvLx| literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/guac-mono-192.png b/guacamole-tunnel/src/main/webapp/images/guac-mono-192.png new file mode 100644 index 0000000000000000000000000000000000000000..428396041273fc733b16fa87b1912b13c72b524c GIT binary patch literal 6041 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 TUTKO|00000NkvXXu0mjf&YTfe literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/guacamole-logo-144.png b/guacamole-tunnel/src/main/webapp/images/guacamole-logo-144.png new file mode 100644 index 0000000000000000000000000000000000000000..e2cb830e93ff0c7bd7dc9a1cd10402456af34c76 GIT binary patch literal 9167 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 Wa+i#07o_+GN=8CKyjs*S=zjomwz@h1 literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/guacamole-logo-24.png b/guacamole-tunnel/src/main/webapp/images/guacamole-logo-24.png new file mode 100644 index 0000000000000000000000000000000000000000..3652598c59bbea5fcbe6334ae356ab2b13c60715 GIT binary patch literal 1520 zcmV*t*FFzN-@o=++W%G}!aKQ~ca#ozZ})>z%I#j; ze($?_GM~t=%G_o}8gtXN|CFW{uSTJ~GCf^>L8>yTl=2>U;L}g*MtuI+Kc0PZVq#_r zK)qFqZtd%SxUjZ;Alsf?d;5o0_2fF!juwlymZa-pw6Hi=XX5hg%s-zSKYHit+a{kl z{Ix?t;B)T$Xz91Vd;aK;4*&97Z!%Cy71nR<`PD}U*KMw6Lnq^>Y4_523kK8f15nou z6p*kk($Lh6Av@Wn_kH$W&vO*YaR_{mA3XlEr`~8$CX=~k* zTGIHgkE0yOCrBtT0*+&`VsQc=@m*?{t2}uB$GluNO{G#Lo%Z?Hg)v9P4)Hl9dLSj zde&qz8C(MLvks9Fs#*|bxh2_(h!E-slLT#J=As3R*5C^%$Iup}P)QdNp?h@_fH*Yd zvN;NcLbp??RLU(aExfm5Bb64gV!2)~Q&TZH&mdOg0>)}0-5}-oSRK<4!(^?BryNu% zLh71YYzgZPy8JsR6bjVq^^$Yt%9R%Z9y#!B-d+0$uV0Gj3|jD%qBK=UTTNG>5gVg* zgX`5}JMCFHe@kJ72Fv zqF|UnX{x>w!^y=u#Ke>4BZ#(Yz;P#Hma>&#iBs_xor3MBDemyia^r=)TO|@YO zV@srkC>A1ZF*gcCHDR7}V-GwN+}=#bnn`=>*(O% z;O74R{>;r48Y_Upz3Ck^#xOKAG=1X4iIc;_!w&;=a;aD>&K8TsiShCAlZ%UsFE>2}FhCWU6%n(1 z1}+dZPmuuJrko|iW&09LniZR-VxR^zL`1I`^3tH25XDNUyG%5px(QtZEE$R{gZ>x9 W%JZkg-)<@Z0000UVdY~hP-098rGzgzpiSKL zKmti`go6T+fQe~OCYmP3gYjf~QDYNB%o>BlSP$rl_QaQs;6@T?(@Ix&7mya%rPF11 z+1bv#^LSupv%AH5Fm;kA&m89c|NhVW%=^5Ii10ig{#@1n4}kn{A27yB0C*c38eTee z>QsvmBDJ}>d2erTZyUfa5oMmazOV<3vC{VT_Ti?cri(;$uDG~ZsjI6i^>{o*E|&|s zt|O61;Najui^XDEI-NdbjI9QP!P&L7weN^1aUwublo$H?`mVWLuGgBIn`;{y8ko=L zgWK(fX_`2}2sr16$K#lro5R-D7J|XxZZsNQx_R^Fx_Kx~|z7Oi_>tF8e z?Zw%%XCZ`u5CW!YJ`=z>ha^ehoFfzpVPRnb`}_NU+_-V$i>0Nd&sbYq+u+r!SN)+- zNEsg=2NA(AjI6kwZ2%F0G4>Qdk|fBo49+)s>!@m>7|}y1Fj4w6rLirol8# z7>1Dr?S zRTKhj_qGX5)1W8{yk2jO9FNCUx7*#ANF)%AMxiK59^|Ax*nmR!L}dSAsIIO?I-Pzb zhr{7J0NRI#hcP`p4MkDV)6)Z8*Pp5)7r;4(5F*e0%E}57Ag>AlS#epG(b?IFj*gC}PVn{X*VUn+p*9c^ z-g*Dy+YeQZgfPi8|9c!|47&8*N2^41Jd2{+!TS$Xt#v=1L6zb_r6R*2v6F|(GoFlL z;!)B-G@)ZFbpIY;4ZBG2?pMFB{``7#$E%eOR1FUw14S}Nv4f-7!BFBnez&2*BbyY}hlr>|6aUUWQ82^?h%92Lw-i33D{`44xL zEHV6fClH_iY3j<(&Q1u(8Fq{@*5BX%qu=kpSW;4o)Dsh$ZX#n4GCCn+m=MPX!7MSE zA7`h`P+lw{6_1%?V`E<~E-qdsOPE=+g#jo6P%|(v@b%c(*egDtPbqXLOaj0l6AW}y_uPr`Q_#1cL6*Gpb?R217rZz07?MxnwpyKo}Qk!2L}f`FI>1#?sPg&+>%r( zg@uKM!>OsMUvJ&I^+`IN-U46($N<U`SbbptP*qiR&g=E|xm+%f+wCr| zsHmuLI2=w@RUgG-v4dDF_Sg3I_Ixs#ylXKDfB=vJa6m*Nw+9jcr&UD}0Lcn&19P%1 zMwVk?g%02dz!M@e@|tWn+7e;N7nO1XmZ)Kg+5$F^C`a^fq|4YveA>Nn00000NkvXX Hu0mjf3AZ|= literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-monitor.png b/guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..6608c293598845e3e757fce3f4776745293b1f63 GIT binary patch literal 691 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4E^09>yiE(HUQk;6fuBS7&d!{-`Wefs|2Nkka_v~#T(FS) zVCw7@3-n3~CyLDazGcR0&%8O3_V=@m`Okdfd0)Bz{hWNev}1Q<8H}Zw=P~jB*#DE| z&CcKY3~X#V%(e%(C2bRK-Zgw+`9Ut@>5VGxm(1MC2iQJD=(sg98ne|fTO}E4?Q&bu znWVY)oqiy10Czwzh`8+6^P4M&QT_S~)pqLx-U(K!7G;t<`L^7hn<&-NzEf_t{~N0Z z95YJ49~N0Y``DRxjPn~iS06j_t)V&5S1w_}TLracKhz@N#(~Wwn%Kj*(c0Ob)7p7& z+n1EYwC_OH_mxU@#YX4#Wpy&O8=(gAlOf-D-9cNk z|HzZ~LbEz|&5oDJHqSh8N z>1u literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-plug.png b/guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-plug.png new file mode 100644 index 0000000000000000000000000000000000000000..d54ee53d4752eb663e5a5972d921341ec74221d2 GIT binary patch literal 727 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4-$0ahv=5ehYlS(=e8kK;1VmV;sm8{ zV&BpeC#>%Ay&!QW@kI7U=>UD{^tAtfIL!HdOs(JBd``}vanrawecH@m?iD8)SFm1r z!f=HttB4_s(bSS*Yr`y=hA0PLJ_p?eYU~Td0wyyDK$V@TVQ`gpJ(I(5hhg3PfZ|8* z)-%7p%Xscei9CaRyKQmNvHTc(yWT zm;PHCi@s?rdZ)Kx&aS>Cwg=ASF=Zd9IrU@OO!jM9%QJ$^52hb5D9jP((VV+z9%J@PbEC-KLqxBn4WVZ&)GM5zV~n3z5n&aC-K`Y^LOvg%9pysI%~^Y%Y?1HBL14I zTX)|#-n_kKKFfJ#e_=K2j~a6V-dnt5wr)H>&3^Zh)gRbCu8$7C{&4ZOi?>T9DmdP# z_PuSg^;(`WiK*Z_{}l1rf2VxZ-ORrx#P6cYeC9i~lRrKB$YtTP_Hx7FtanFGsdm12 z&v;zS(AxWF;1{j0>D}+&?URdmZ&t8w*>8p)E<2b1=_#E%YjrP!pRsYh$T`wta6e=8 XUw?(DAg{^5M9ARj>gTe~DWM4fTdzIi literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-text.png b/guacamole-tunnel/src/main/webapp/images/protocol-icons/guac-text.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5eff62a323dda0cc593dd21fddd634454f2f0d GIT binary patch literal 792 zcmV+z1LypSP))f2 z;r|E1AZ#}U!u*5b5UyGW3S|NJF5p^F!4P=uQCFLs00f5L0agyu_y-37 z6@(wD>I;wBdKGEvh~mhs`{>~e}2j9@`KA{@O;i2+I|BUAX^%X9zF-K zZWMmn-T*u?3V+f#yaK-fzIlcZ+9*Lb=B)u}0({3!t=k}R-=Tl2!cS$gNeVb!;+*xj z+)VUsa=;;A(-^?|tfA-|C4q;=0PYqIrNAoiGw(147V8L~pYJH}2-u1od=t0_9Nu$X zKtu+>3C*_8)GiS@LUvDx^(Z`6)os9iVWe&9*9EK%SgQwMZNOSR0BZx*>H%0Au-0F@ W{$6AhTJBQ-0000@5@9T8M3k^4fus;2CNYJOKnTnY?HSviPG|mrdFQ;7bMMW!+~4>6 zmY4UDzt>iMGkpkxwt62!2SSieBzR3X>4Kfo7XMA)XJd+o_lZs5$=VbRgZ1X5V<9OJ z^p_s(tFt``;{i5zrh1-C4I;#*5+jmhAR>{t|4d?BN>oHr%zi>LzC!9|20>eX^hSSp zBE3?=WyehrNqusq()G5N<-RK?J~4wan@VazisQUHAGBXS;=S3|cidN&yK2|5Z{dj) zciBTfQ-037*reU|P+LTt&$s@9uUOvj_6H7T-t(G;%h+-&v(e$_Vx^Zuk=_oIxP>Q| z6fY|ud>MP)wua?~8FEC(t6^0SVP&bRgAaXxW*tXoSs<7&Oeqaq6}E0UxI6dGneqtq z%0A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 literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/settings/touchpad.png b/guacamole-tunnel/src/main/webapp/images/settings/touchpad.png new file mode 100644 index 0000000000000000000000000000000000000000..3facb5d3822a60361d89a8a6791921f6f1f549b8 GIT binary patch literal 38013 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%0D+T$#)8#g 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 literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/settings/touchscreen.png b/guacamole-tunnel/src/main/webapp/images/settings/touchscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..11ad40d9e0290e9ca49e674226891c10a91d6c09 GIT binary patch literal 24025 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 H_euE=jm-W( literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/images/settings/zoom-in.png b/guacamole-tunnel/src/main/webapp/images/settings/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..ef36301b48ee14dfe7376a949244e4a4fba54d28 GIT binary patch literal 1553 zcmeAS@N?(olHy`uVBq!ia0y~yU~B+k4mP03lH*(V11XkbC(jTLAgJL;=>YOM3p^r= z85l$kgD|6Lfv*!#P_o1|q9iy!t)x7$D3zhSyj(9cFS|H7u^?41zbJk7I~yqm2G%}L z7srr_Id88Y%v5m@U^)2O`QPrCO$lEwCJRU=Iv(UvslEM=@xtf-Ul|zg2{1HhFfw!q zjZzc_!LKS?hPtBi>b(!mx7~fs`=OJYLBWfGVG+wnqh4*>dv8Knxm?4V-0xM+GlxPj zfSk-AfA{YEe+zEE`^xs?2rGktDg#3x(@3LAZ^z{`JYW=OaG1it;KDgdQ62YOM3p^r= z85l$kgD|6Lfv*!#P_o1|q9iy!t)x7$D3zhSyj(9cFS|H7u^?41zbJk7I~yqm2G%@J z7srr_Id89T6*1Hku#YyqllF_Wf4|hCB`ihbasUE}Wy(Xb_N{ z3Tm%2ze-D6Z+>9v-MO!MQ{{$0V!HC?-B&gSz6u70KqdwO)lq6R2q;Pg_KS^qen*O> RUj!D<44$rjF6*2UngIQH~s~KzZ7LYgU2IMUuZ>+0iVD^1^{iq zv}5v1F%7hN>y;?l1tPK^7y-7pk^YX)K)b3=dJ(!!gCf!h43S;(MDC&3mJ2-B0fRun zkuM2_*p?5Rp8GERpZR}9S5=GwBC-aU0FIi7O2-r6xT-E%NIV{aZYrNVvCVEPiH89f z95`}o+w=&Ch!EECJFG&=#5=+Qsq7@IJ>UTOB~NVofQ7(nz%FBb512Gq4cKX{?*Wqr zs{zf%`W`T8up02mSlqg(=04~Pxl zw{RG7-;zA|EQRknaOBpu$J^j$Ub#Z6WlJmy=^>2DS>rp<c#?g^^uW1x?)U6mCS=!uI$ZoyQWG!$8=m1Uw?f4y(E&!9j zc#MatI_^R2@iKG*xCC6Ja+{=!ghBEya8p&M%4RZ*?!CZ0!T?t3gVE*j1GoVktjIJ7 z9KmOP_l_^XHDGHd1`ysGt^i-FAlbB-1dh2HK)6phSOv+gG~NImE(Q>;yWOgSq)Lrh z;FN~}*Q+3za^r!?0K$OxtqKycM6bnw+f|T&H73$>Ok4m&GG0lKHufBL=ds0D~0mTQ0+C*Y5X$F`sAXXU? zb6NHOGgR*ZDV6GDKn6ncLcIqh71RwVr&0}~Za`8&+S}ks!ruP>fDu)FRIK0^?->wD TbN@Ht00000NkvXXu0mjf^)beV literal 0 HcmV?d00001 diff --git a/guacamole-tunnel/src/main/webapp/index.xhtml b/guacamole-tunnel/src/main/webapp/index.xhtml index 9575d056..18f93bd3 100644 --- a/guacamole-tunnel/src/main/webapp/index.xhtml +++ b/guacamole-tunnel/src/main/webapp/index.xhtml @@ -1,160 +1,189 @@ - + - + + - - - - UDS - + + + + Guacamole ${project.version} - -
-
-
+
+ + +
+
+
+
+
+ + +
-
+ +
+ + +
- + - + - - + + + - // Instantiate client - var guac = new Guacamole.Client(tunnel); - - // Add client to UI - guac.getDisplay().className = "software-cursor"; - GuacUI.Client.display.appendChild(guac.getDisplay()); - - // Tie UI to client - GuacUI.Client.attach(guac); - - try { - - // Calculate optimal width/height for display - var optimal_width = window.innerWidth; - var optimal_height = window.innerHeight; - - // Scale width/height to be at least 600x600 - if (optimal_width < 600 || optimal_height < 600) { - var scale = Math.max(600 / optimal_width, 600 / optimal_height); - optimal_width = Math.floor(optimal_width * scale); - optimal_height = Math.floor(optimal_height * scale); - } - - // Get entire query string, and pass to connect(). - // Normally, only the "id" parameter is required, but - // all parameters should be preserved and passed on for - // the sake of authentication. - - var connect_string = "data=" + data + "&width=" + optimal_width + "&height=" + optimal_height; - - // Add audio mimetypes to connect_string - GuacUI.Audio.supported.forEach(function(mimetype) { - connect_string += "&audio=" + encodeURIComponent(mimetype); - }); - - // Add video mimetypes to connect_string - GuacUI.Video.supported.forEach(function(mimetype) { - connect_string += "&video=" + encodeURIComponent(mimetype); - }); - - guac.connect(connect_string); - - } - catch (e) { - GuacUI.Client.showError(e.message); - } - - }, 0); -}; - /* ]]> */ - - - \ No newline at end of file + + diff --git a/guacamole-tunnel/src/main/webapp/scripts/admin-ui.js b/guacamole-tunnel/src/main/webapp/scripts/admin-ui.js index 4370cf18..0bf2296a 100644 --- a/guacamole-tunnel/src/main/webapp/scripts/admin-ui.js +++ b/guacamole-tunnel/src/main/webapp/scripts/admin-ui.js @@ -1,19 +1,23 @@ /* - * Guacamole - Clientless Remote Desktop - * Copyright (C) 2010 Michael Jumper + * Copyright (C) 2013 Glyptodon LLC * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ /** @@ -76,7 +80,6 @@ GuacAdmin.Field = function() { }; - /** * Simple HTML input field. * @@ -108,7 +111,6 @@ GuacAdmin.Field._HTML_INPUT = function(type) { GuacAdmin.Field._HTML_INPUT.prototype = new GuacAdmin.Field(); - /** * A basic text field. * @@ -120,6 +122,34 @@ GuacAdmin.Field.TEXT = function() { GuacAdmin.Field.TEXT.prototype = new GuacAdmin.Field._HTML_INPUT(); +/** + * A basic multiline text field. + * + * @augments GuacAdmin.Field + */ +GuacAdmin.Field.MULTILINE = function() { + + // Call parent constructor + GuacAdmin.Field.apply(this); + + // Create backing element + var element = GuacUI.createElement("textarea"); + + this.getValue = function() { + return element.value; + }; + + this.getElement = function() { + return element; + }; + + this.setValue = function(value) { + element.value = value; + }; + +}; + +GuacAdmin.Field.MULTILINE.prototype = new GuacAdmin.Field(); /** * A basic password field. @@ -132,7 +162,6 @@ GuacAdmin.Field.PASSWORD = function() { GuacAdmin.Field.PASSWORD.prototype = new GuacAdmin.Field._HTML_INPUT(); - /** * A basic numeric field, leveraging the new HTML5 field types. * @@ -144,7 +173,6 @@ GuacAdmin.Field.NUMERIC = function() { GuacAdmin.Field.NUMERIC.prototype = new GuacAdmin.Field._HTML_INPUT(); - /** * Simple checkbox. * @@ -216,7 +244,6 @@ GuacAdmin.Field.ENUM = function(values) { GuacAdmin.Field.ENUM.prototype = new GuacAdmin.Field(); - /** * An arbitrary button. * @@ -363,7 +390,6 @@ GuacAdmin.addUser = function(name, parameters) { }; - /** * User edit dialog which allows editing of the user's password and connection * access level. @@ -619,8 +645,8 @@ GuacAdmin.UserEditor = function(name, parameters) { GuacAdmin.reset(); } - catch (e) { - alert(e.message); + catch (status) { + alert(status.message); } }; @@ -658,8 +684,8 @@ GuacAdmin.UserEditor = function(name, parameters) { } // Alert on failure - catch (e) { - alert(e.message); + catch (status) { + alert(status.message); } } @@ -816,8 +842,10 @@ GuacAdmin.ConnectionEditor = function(connection, parameters) { start.textContent = GuacAdmin.formatDate(record.start); if (record.duration !== null) duration.textContent = GuacAdmin.formatSeconds(record.duration); - else + else if (record.active) duration.textContent = "Active now"; + else + duration.textContent = "-"; // Add record to pager history_pager.addElement(row); @@ -882,6 +910,11 @@ GuacAdmin.ConnectionEditor = function(connection, parameters) { field = new GuacAdmin.Field.ENUM(parameter.options); break; + // Multiline text field + case GuacamoleService.Protocol.Parameter.MULTILINE: + field = new GuacAdmin.Field.MULTILINE(); + break; + default: continue; @@ -952,8 +985,8 @@ GuacAdmin.ConnectionEditor = function(connection, parameters) { GuacAdmin.reset(); } - catch (e) { - alert(e.message); + catch (status) { + alert(status.message); } }; @@ -991,8 +1024,8 @@ GuacAdmin.ConnectionEditor = function(connection, parameters) { } // Alert on failure - catch (e) { - alert(e.message); + catch (status) { + alert(status.message); } } @@ -1145,8 +1178,8 @@ GuacAdmin.ConnectionGroupEditor = function(group, parameters) { GuacAdmin.reset(); } - catch (e) { - alert(e.message); + catch (status) { + alert(status.message); } }; @@ -1184,8 +1217,8 @@ GuacAdmin.ConnectionGroupEditor = function(group, parameters) { } // Alert on failure - catch (e) { - alert(e.message); + catch (status) { + alert(status.message); } } @@ -1375,8 +1408,8 @@ GuacAdmin.reset = function() { } // Alert on failure - catch (e) { - alert(e.message); + catch (status) { + alert(tatusmessage); } }; diff --git a/guacamole-tunnel/src/main/webapp/scripts/client-ui.js b/guacamole-tunnel/src/main/webapp/scripts/client-ui.js index b2dfcc58..286dbb54 100644 --- a/guacamole-tunnel/src/main/webapp/scripts/client-ui.js +++ b/guacamole-tunnel/src/main/webapp/scripts/client-ui.js @@ -1,3 +1,24 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ /** * Client UI root object. @@ -5,435 +26,239 @@ GuacUI.Client = { /** - * Collection of all Guacamole client UI states. + * Enumeration of all tunnel-specific error messages for each applicable + * error code. */ - "states": { + "tunnel_errors": { - /** - * The normal default Guacamole client UI mode - */ - "INTERACTIVE" : 0, + 0x0201: "The Guacamole server has rejected this connection attempt \ + because there are too many active connections. Please wait \ + a few minutes and try again.", - /** - * Same as INTERACTIVE except with visible on-screen keyboard. - */ - "OSK" : 1, + 0x0202: "The connection has been closed because the server is taking \ + too long to respond. This is usually caused by network \ + problems, such as a spotty wireless signal, or slow network \ + speeds. Please check your network connection and try again \ + or contact your system administrator.", - /** - * No on-screen keyboard, but a visible magnifier. - */ - "MAGNIFIER" : 2, + 0x0203: "The server encountered an error and has closed the \ + connection. Please try again or contact your \ + system administrator.", - /** - * Arrows and a draggable view. - */ - "PAN" : 3, + 0x0204: "The requested connection does not exist. Please check the \ + connection name and try again.", - /** - * Same as PAN, but with visible native OSK. - */ - "PAN_TYPING" : 4, + 0x0205: "This connection is currently in use, and concurrent access to \ + this connection is not allowed. Please try again later.", - /** - * Precursor to PAN_TYPING, like PAN, except does not pan the - * screen, but rather hints at how to start typing. - */ - "WAIT_TYPING" : 5 + 0x0301: "You do not have permission to access this connection because \ + you are not logged in. Please log in and try again.", + 0x0303: "You do not have permission to access this connection. If you \ + require access, please ask your system administrator to add \ + you the list of allowed users, or check your system settings.", + + 0x0308: "The Guacamole server has closed the connection because there \ + has been no response from your browser for long enough that \ + it appeared to be disconnected. This is commonly caused by \ + network problems, such as spotty wireless signal, or simply \ + very slow network speeds. Please check your network and try \ + again.", + + 0x031D: "The Guacamole server is denying access to this connection \ + because you have exhausted the limit for simultaneous \ + connection use by an individual user. Please close one or \ + more connections and try again.", + + "DEFAULT": "An internal error has occurred within the Guacamole \ + server, and the connection has been terminated. If \ + the problem persists, please notify your system \ + administrator, or check your system logs." + + }, + + /** + * Enumeration of all client-specific error messages for each applicable + * error code. + */ + "client_errors": { + + 0x0201: "This connection has been closed because the server is busy. \ + Please wait a few minutes and try again.", + + 0x0202: "The Guacamole server has closed the connection because the \ + remote desktop is taking too long to respond. Please try \ + again or contact your system administrator.", + + 0x0203: "The remote desktop server encountered an error and has closed \ + the connection. Please try again or contact your system \ + administrator.", + + 0x0205: "This connection has been closed because it conflicts with \ + another connection. Please try again later.", + + 0x0301: "Log in failed. Please reconnect and try again.", + + 0x0303: "You do not have permission to access this connection. If you \ + require access, please ask your system administrator to add \ + you the list of allowed users, or check your system settings.", + + 0x0308: "The Guacamole server has closed the connection because there \ + has been no response from your browser for long enough that \ + it appeared to be disconnected. This is commonly caused by \ + network problems, such as spotty wireless signal, or simply \ + very slow network speeds. Please check your network and try \ + again.", + + 0x031D: "The Guacamole server is denying access to this connection \ + because you have exhausted the limit for simultaneous \ + connection use by an individual user. Please close one or \ + more connections and try again.", + + "DEFAULT": "An internal error has occurred within the Guacamole \ + server, and the connection has been terminated. If \ + the problem persists, please notify your system \ + administrator, or check your system logs." + + }, + + /** + * Enumeration of all error messages for each applicable error code. This + * list is specific to file uploads. + */ + "upload_errors": { + + 0x0100: "File transfer is either not supported or not enabled. Please \ + contact your system administrator, or check your system logs.", + + 0x0201: "Too many files are currently being transferred. Please wait \ + for existing transfers to complete, and then try again.", + + 0x0202: "The file cannot be transferred because the remote desktop \ + server is taking too long to respond. Please try again or \ + or contact your system administrator.", + + 0x0203: "The remote desktop server encountered an error during \ + transfer. Please try again or contact your system \ + administrator.", + + 0x0204: "The destination for the file transfer does not exist. Please \ + check that the destionation exists and try again.", + + 0x0205: "The destination for the file transfer is currently locked. \ + Please wait for any in-progress tasks to complete and try \ + again.", + + 0x0301: "You do not have permission to upload this file because you \ + are not logged in. Please log in and try again.", + + 0x0303: "You do not have permission to upload this file. If you \ + require access, please check your system settings, or \ + check with your system administrator.", + + 0x0308: "The file transfer has stalled. This is commonly caused by \ + network problems, such as spotty wireless signal, or \ + simply very slow network speeds. Please check your \ + network and try again.", + + 0x031D: "Too many files are currently being transferred. Please wait \ + for existing transfers to complete, and then try again.", + + "DEFAULT": "An internal error has occurred within the Guacamole \ + server, and the connection has been terminated. If \ + the problem persists, please notify your system \ + administrator, or check your system logs.", + + }, + + /** + * All error codes for which automatic reconnection is appropriate when a + * tunnel error occurs. + */ + "tunnel_auto_reconnect": { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0308: true + }, + + /** + * All error codes for which automatic reconnection is appropriate when a + * client error occurs. + */ + "client_auto_reconnect": { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0301: true, + 0x0308: true }, /* Constants */ - "LONG_PRESS_DETECT_TIMEOUT" : 800, /* milliseconds */ - "LONG_PRESS_MOVEMENT_THRESHOLD" : 10, /* pixels */ "KEYBOARD_AUTO_RESIZE_INTERVAL" : 30, /* milliseconds */ + "RECONNECT_PERIOD" : 15, /* seconds */ + "TEXT_INPUT_PADDING" : 128, /* characters */ + "TEXT_INPUT_PADDING_CODEPOINT" : 0x200B, - /* UI Components */ + /* Main application area */ "viewport" : document.getElementById("viewportClone"), + "main" : document.getElementById("main"), "display" : document.getElementById("display"), "notification_area" : document.getElementById("notificationArea"), - /* Expected Input Rectangle */ + /* Text input */ - "expected_input_x" : 0, - "expected_input_y" : 0, - "expected_input_width" : 1, - "expected_input_height" : 1, + "text_input" : { + "container" : document.getElementById("text-input"), + "sent" : document.getElementById("sent-history"), + "target" : document.getElementById("target"), + "enabled" : false + }, + + /* Menu */ + + "menu" : document.getElementById("menu"), + "menu_title" : document.getElementById("menu-title"), + "clipboard" : document.getElementById("clipboard"), + "relative_radio" : document.getElementById("relative"), + "absolute_radio" : document.getElementById("absolute"), + "ime_none_radio" : document.getElementById("ime-none"), + "ime_text_radio" : document.getElementById("ime-text"), + "ime_osk_radio" : document.getElementById("ime-osk"), + "zoom_state" : document.getElementById("zoom-state"), + "zoom_out" : document.getElementById("zoom-out"), + "zoom_in" : document.getElementById("zoom-in"), + "auto_fit" : document.getElementById("auto-fit"), + + "min_zoom" : 1, + "max_zoom" : 3, "connectionName" : "Guacamole", - "overrideAutoFit" : false, - "attachedClient" : null + "attachedClient" : null, + + /* Mouse emulation */ + + "emulate_absolute" : true, + "touch" : null, + "touch_screen" : null, + "touch_pad" : null, + + /* Clipboard */ + + "remote_clipboard" : "", + "clipboard_integration_enabled" : undefined }; -/** - * Component which displays a magnified (100% zoomed) client display. - * - * @constructor - * @augments GuacUI.DraggableComponent - */ -GuacUI.Client.Magnifier = function() { - - /** - * Reference to this magnifier. - * @private - */ - var guac_magnifier = this; - - /** - * Large background div which will block touch events from reaching the - * client while also providing a click target to deactivate the - * magnifier. - * @private - */ - var magnifier_background = GuacUI.createElement("div", "magnifier-background"); - - /** - * Container div for the magnifier, providing a clipping rectangle. - * @private - */ - var magnifier = GuacUI.createChildElement(magnifier_background, - "div", "magnifier"); - - /** - * Canvas which will contain the static image copy of the display at time - * of show. - * @private - */ - var magnifier_display = GuacUI.createChildElement(magnifier, "canvas"); - - /** - * Context of magnifier display. - * @private - */ - var magnifier_context = magnifier_display.getContext("2d"); - - /* - * This component is draggable. - */ - GuacUI.DraggableComponent.apply(this, [magnifier]); - - // Ensure transformations on display originate at 0,0 - magnifier.style.transformOrigin = - magnifier.style.webkitTransformOrigin = - magnifier.style.MozTransformOrigin = - magnifier.style.OTransformOrigin = - magnifier.style.msTransformOrigin = - "0 0"; - - /* - * Reposition magnifier display relative to own position on screen. - */ - - this.onmove = function(x, y) { - - var width = magnifier.offsetWidth; - var height = magnifier.offsetHeight; - - // Update contents relative to new position - var clip_x = x - / (window.innerWidth - width) * (GuacUI.Client.attachedClient.getWidth() - width); - var clip_y = y - / (window.innerHeight - height) * (GuacUI.Client.attachedClient.getHeight() - height); - - magnifier_display.style.WebkitTransform = - magnifier_display.style.MozTransform = - magnifier_display.style.OTransform = - magnifier_display.style.msTransform = - magnifier_display.style.transform = "translate(" - + (-clip_x) + "px, " + (-clip_y) + "px)"; - - /* Update expected input rectangle */ - GuacUI.Client.expected_input_x = clip_x; - GuacUI.Client.expected_input_y = clip_y; - GuacUI.Client.expected_input_width = width; - GuacUI.Client.expected_input_height = height; - - }; - - /* - * Copy display and add self to body on show. - */ - - this.show = function() { - - // Copy displayed image - magnifier_display.width = GuacUI.Client.attachedClient.getWidth(); - magnifier_display.height = GuacUI.Client.attachedClient.getHeight(); - magnifier_context.drawImage(GuacUI.Client.attachedClient.flatten(), 0, 0); - - // Show magnifier container - document.body.appendChild(magnifier_background); - - }; - - /* - * Remove self from body on hide. - */ - - this.hide = function() { - - // Hide magnifier container - document.body.removeChild(magnifier_background); - - }; - - /* - * If the user clicks on the background, switch to INTERACTIVE mode. - */ - - magnifier_background.addEventListener("click", function() { - GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); - }, true); - - /* - * If the user clicks on the magnifier, switch to PAN_TYPING mode. - */ - - magnifier.addEventListener("click", function(e) { - GuacUI.StateManager.setState(GuacUI.Client.states.PAN_TYPING); - e.stopPropagation(); - }, true); - -}; - -/* - * We inherit from GuacUI.DraggableComponent. - */ -GuacUI.Client.Magnifier.prototype = new GuacUI.DraggableComponent(); - -GuacUI.StateManager.registerComponent( - new GuacUI.Client.Magnifier(), - GuacUI.Client.states.MAGNIFIER -); - -/** - * Zoomed Display, a pseudo-component. - * - * @constructor - * @augments GuacUI.Component - */ -GuacUI.Client.ZoomedDisplay = function() { - - this.show = function() { - GuacUI.Client.overrideAutoFit = true; - GuacUI.Client.updateDisplayScale(); - }; - - this.hide = function() { - GuacUI.Client.overrideAutoFit = false; - GuacUI.Client.updateDisplayScale(); - }; - -}; - -GuacUI.Client.ZoomedDisplay.prototype = new GuacUI.Component(); - -/* - * Zoom the main display during PAN and PAN_TYPING modes. - */ - -GuacUI.StateManager.registerComponent( - new GuacUI.Client.ZoomedDisplay(), - GuacUI.Client.states.PAN, - GuacUI.Client.states.PAN_TYPING -); - -/** - * Type overlay UI. This component functions to provide a means of activating - * the keyboard, when neither panning nor magnification make sense. - * - * @constructor - * @augments GuacUI.Component - */ -GuacUI.Client.TypeOverlay = function() { - - /** - * Overlay which will provide the means of scrolling the screen. - */ - var type_overlay = GuacUI.createElement("div", "type-overlay"); - - /* - * Add exit button - */ - - var start = GuacUI.createChildElement(type_overlay, "p", "hint"); - start.textContent = "Tap here to type, or tap the screen to cancel."; - - // Begin typing when user clicks hint - start.addEventListener("click", function(e) { - GuacUI.StateManager.setState(GuacUI.Client.states.PAN_TYPING); - e.stopPropagation(); - }, false); - - this.show = function() { - document.body.appendChild(type_overlay); - }; - - this.hide = function() { - document.body.removeChild(type_overlay); - }; - - /* - * Cancel when user taps screen - */ - - type_overlay.addEventListener("click", function(e) { - GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); - e.stopPropagation(); - }, false); - -}; - -GuacUI.Client.TypeOverlay.prototype = new GuacUI.Component(); - -/* - * Show the type overlay during WAIT_TYPING mode only - */ - -GuacUI.StateManager.registerComponent( - new GuacUI.Client.TypeOverlay(), - GuacUI.Client.states.WAIT_TYPING -); - -/** - * Pan overlay UI. This component functions to receive touch events and - * translate them into scrolling of the main UI. - * - * @constructor - * @augments GuacUI.Component - */ -GuacUI.Client.PanOverlay = function() { - - /** - * Overlay which will provide the means of scrolling the screen. - */ - var pan_overlay = GuacUI.createElement("div", "pan-overlay"); - - /* - * Add arrows - */ - - GuacUI.createChildElement(pan_overlay, "div", "indicator up"); - GuacUI.createChildElement(pan_overlay, "div", "indicator down"); - GuacUI.createChildElement(pan_overlay, "div", "indicator right"); - GuacUI.createChildElement(pan_overlay, "div", "indicator left"); - - /* - * Add exit button - */ - - var back = GuacUI.createChildElement(pan_overlay, "p", "hint"); - back.textContent = "Tap here to exit panning mode"; - - // Return to interactive when back is clicked - back.addEventListener("click", function() { - GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); - }, false); - - this.show = function() { - document.body.appendChild(pan_overlay); - }; - - this.hide = function() { - document.body.removeChild(pan_overlay); - }; - - /* - * Transition to PAN_TYPING when the user taps on the overlay. - */ - - pan_overlay.addEventListener("click", function(e) { - GuacUI.StateManager.setState(GuacUI.Client.states.PAN_TYPING); - e.stopPropagation(); - }, true); - -}; - -GuacUI.Client.PanOverlay.prototype = new GuacUI.Component(); - -/* - * Show the pan overlay during PAN or PAN_TYPING modes. - */ - -GuacUI.StateManager.registerComponent( - new GuacUI.Client.PanOverlay(), - GuacUI.Client.states.PAN, - GuacUI.Client.states.PAN_TYPING -); - -/** - * Native Keyboard. This component uses a hidden textarea field to show the - * platforms native on-screen keyboard (if any) or otherwise enable typing, - * should the platform require a text field with focus for keyboard events to - * register. - * - * @constructor - * @augments GuacUI.Component - */ -GuacUI.Client.NativeKeyboard = function() { - - /** - * Event target. This is a hidden textarea element which will receive - * key events. - * @private - */ - var eventTarget = GuacUI.createElement("textarea", "event-target"); - eventTarget.setAttribute("autocorrect", "off"); - eventTarget.setAttribute("autocapitalize", "off"); - - this.show = function() { - - // Move to location of expected input - eventTarget.style.left = GuacUI.Client.expected_input_x + "px"; - eventTarget.style.top = GuacUI.Client.expected_input_y + "px"; - eventTarget.style.width = GuacUI.Client.expected_input_width + "px"; - eventTarget.style.height = GuacUI.Client.expected_input_height + "px"; - - // Show and focus target - document.body.appendChild(eventTarget); - eventTarget.focus(); - - }; - - this.hide = function() { - - // Hide and blur target - eventTarget.blur(); - document.body.removeChild(eventTarget); - - }; - - /* - * Automatically switch to INTERACTIVE mode after target loses focus - */ - - eventTarget.addEventListener("blur", function() { - GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); - }, false); - -}; - -GuacUI.Client.NativeKeyboard.prototype = new GuacUI.Component(); - -/* - * Show native keyboard during PAN_TYPING mode only. - */ - -GuacUI.StateManager.registerComponent( - new GuacUI.Client.NativeKeyboard(), - GuacUI.Client.states.PAN_TYPING -); - /** * On-screen Keyboard. This component provides a clickable/touchable keyboard * which sends key events to the Guacamole client. * * @constructor - * @augments GuacUI.Component */ -GuacUI.Client.OnScreenKeyboard = function() { +GuacUI.Client.OnScreenKeyboard = new (function() { /** * Event target. This is a hidden textarea element which will receive @@ -460,16 +285,21 @@ GuacUI.Client.OnScreenKeyboard = function() { } keyboard.onkeydown = function(keysym) { - GuacUI.Client.attachedClient.sendKeyEvent(1, keysym); + if (GuacUI.Client.attachedClient) + GuacUI.Client.attachedClient.sendKeyEvent(1, keysym); }; keyboard.onkeyup = function(keysym) { - GuacUI.Client.attachedClient.sendKeyEvent(0, keysym); + if (GuacUI.Client.attachedClient) + GuacUI.Client.attachedClient.sendKeyEvent(0, keysym); }; - this.show = function() { + // Only add if not already present + if (keyboard_container.parentNode === document.body) + return; + // Show keyboard document.body.appendChild(keyboard_container); @@ -488,6 +318,10 @@ GuacUI.Client.OnScreenKeyboard = function() { this.hide = function() { + // Only remove if present + if (keyboard_container.parentNode !== document.body) + return; + // Hide keyboard document.body.removeChild(keyboard_container); window.clearInterval(keyboard_resize_interval); @@ -495,25 +329,7 @@ GuacUI.Client.OnScreenKeyboard = function() { }; -}; - -GuacUI.Client.OnScreenKeyboard.prototype = new GuacUI.Component(); - -/* - * Show on-screen keyboard during OSK mode only. - */ - -GuacUI.StateManager.registerComponent( - new GuacUI.Client.OnScreenKeyboard(), - GuacUI.Client.states.OSK -); - - -/* - * Set initial state - */ - -GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); +})(); /** * Modal status display. Displays a message to the user, covering the entire @@ -523,34 +339,486 @@ GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); * components is impossible. * * @constructor - * @augments GuacUI.Component */ -GuacUI.Client.ModalStatus = function(text, classname) { +GuacUI.Client.ModalStatus = function(title_text, text, classname, reconnect) { // Create element hierarchy var outer = GuacUI.createElement("div", "dialogOuter"); var middle = GuacUI.createChildElement(outer, "div", "dialogMiddle"); var dialog = GuacUI.createChildElement(middle, "div", "dialog"); + + // Add title if given + if (title_text) { + var title = GuacUI.createChildElement(dialog, "p", "title"); + title.textContent = title_text; + } + var status = GuacUI.createChildElement(dialog, "p", "status"); + status.textContent = text; // Set classname if given if (classname) GuacUI.addClass(outer, classname); - // Set status text - status.textContent = text; + // Automatically reconnect after the given time period + var reconnect_interval = null; + var reconnect_forced = false; + + /** + * Stops the reconnect countdown and forces a client reconnect. + */ + function force_reconnect() { + if (!reconnect_forced) { + reconnect_forced = true; + window.clearInterval(reconnect_interval); + GuacUI.Client.connect(); + } + } + + if (reconnect) { + + var countdown = GuacUI.createChildElement(dialog, "p", "countdown"); + + function update_status() { + + // Use appropriate description of time remaining + if (reconnect === 0) + countdown.textContent = "Reconnecting..."; + if (reconnect === 1) + countdown.textContent = "Reconnecting in 1 second..."; + else + countdown.textContent = "Reconnecting in " + reconnect + " seconds..."; + + // Reconnect if countdown complete + if (reconnect === 0) + force_reconnect(); + + } + + // Update counter every second + reconnect_interval = window.setInterval(function update_countdown() { + reconnect--; + update_status(); + }, 1000); + + // Init status + update_status(); + + } + + // Reconnect button + var reconnect_section = GuacUI.createChildElement(dialog, "div", "reconnect"); + var reconnect_button = GuacUI.createChildElement(reconnect_section, "button"); + reconnect_button.textContent = "Reconnect"; + + // Reconnect if button clicked + reconnect_button.onclick = force_reconnect; + + // Reconnect if button tapped + reconnect_button.addEventListener("touchend", function(e) { + if (e.touches.length === 0) + force_reconnect(); + }, true); this.show = function() { document.body.appendChild(outer); }; this.hide = function() { + window.clearInterval(reconnect_interval); document.body.removeChild(outer); }; }; -GuacUI.Client.ModalStatus.prototype = new GuacUI.Component(); +/** + * Monitors a given element for touch events, firing drag-specific events + * based on pre-defined gestures. + * + * @constructor + * @param {Element} element The element to monitor for touch events. + */ +GuacUI.Client.Drag = function(element) { + + /** + * Reference to this drag instance. + * @private + */ + var guac_drag = this; + + /** + * Whether a drag gestures is in progress. + */ + var in_progress = false; + + /** + * The starting X location of the drag gesture. + */ + this.start_x = null; + + /** + * The starting Y location of the drag gesture. + */ + this.start_y = null; + + /** + * The change in X relative to drag start. + */ + this.delta_x = 0; + + /** + * The change in X relative to drag start. + */ + this.delta_y = 0; + + /** + * Called when a drag gesture begins. + * + * @event + * @param {Number} x The relative change in X location relative to + * drag start. For drag start, this will ALWAYS be 0. + * @param {Number} y The relative change in Y location relative to + * drag start. For drag start, this will ALWAYS be 0. + */ + this.ondragstart = null; + + /** + * Called when the drag amount changes. + * + * @event + * @param {Number} x The relative change in X location relative to + * drag start. + * @param {Number} y The relative change in Y location relative to + * drag start. + */ + this.ondragchange = null; + + /** + * Called when a drag gesture ends. + * + * @event + * @param {Number} x The relative change in X location relative to + * drag start. + * @param {Number} y The relative change in Y location relative to + * drag start. + */ + this.ondragend = null; + + /** + * Cancels the current drag gesture, if any. Drag events will cease to fire + * until a new gesture begins. + */ + this.cancel = function() { + in_progress = false; + }; + + // When there is exactly one touch, monitor the change in location + element.addEventListener("touchmove", function(e) { + if (e.touches.length === 1) { + + e.preventDefault(); + e.stopPropagation(); + + // Get touch location + var x = e.touches[0].clientX; + var y = e.touches[0].clientY; + + // If gesture just starting, fire zoom start + if (!guac_drag.start_x || !guac_drag.start_y) { + guac_drag.start_x = x; + guac_drag.start_y = y; + guac_drag.delta_x = 0; + guac_drag.delta_y = 0; + in_progress = true; + if (guac_drag.ondragstart) + guac_drag.ondragstart(guac_drag.delta_x, guac_drag.delta_y); + } + + // Otherwise, notify of zoom change + else if (guac_drag.ondragchange) { + guac_drag.delta_x = x - guac_drag.start_x; + guac_drag.delta_y = y - guac_drag.start_y; + + if (in_progress) + guac_drag.ondragchange(guac_drag.delta_x, guac_drag.delta_y); + } + + } + }, false); + + // Reset monitoring and fire end event when done + element.addEventListener("touchend", function(e) { + + if (guac_drag.start_x && guac_drag.start_y && e.touches.length === 0) { + + e.preventDefault(); + e.stopPropagation(); + + if (in_progress && guac_drag.ondragend) + guac_drag.ondragend(); + + guac_drag.start_x = null; + guac_drag.start_y = null; + guac_drag.delta_x = 0; + guac_drag.delta_y = 0; + in_progress = false; + + } + + }, false); + +}; + +/** + * Monitors a given element for touch events, firing zoom-specific events + * based on pre-defined gestures. + * + * @constructor + * @param {Element} element The element to monitor for touch events. + */ +GuacUI.Client.Pinch = function(element) { + + /** + * Reference to this zoom instance. + * @private + */ + var guac_zoom = this; + + /** + * The current pinch distance, or null if the gesture has not yet started. + * @private + */ + var start_length = null; + + /** + * The current zoom ratio. + * @type Number + */ + this.ratio = 1; + + /** + * The X-coordinate of the current center of the pinch gesture. + * @type Number + */ + this.centerX = 0; + + /** + * The Y-coordinate of the current center of the pinch gesture. + * @type Number + */ + this.centerY = 0; + + /** + * Called when a zoom gesture begins. + * + * @event + * @param {Number} ratio The relative value of the starting zoom. This will + * ALWAYS be 1. + * @param {Number} x The X-coordinate of the center of the pinch gesture. + * @param {Number} y The Y-coordinate of the center of the pinch gesture. + */ + this.onzoomstart = null; + + /** + * Called when the amount of zoom changes. + * + * @event + * @param {Number} ratio The relative value of the changed zoom, with 1 + * being no change. + * @param {Number} x The X-coordinate of the center of the pinch gesture. + * @param {Number} y The Y-coordinate of the center of the pinch gesture. + */ + this.onzoomchange = null; + + /** + * Called when a zoom gesture ends. + * + * @event + * @param {Number} ratio The relative value of the final zoom, with 1 + * being no change. + * @param {Number} x The X-coordinate of the center of the pinch gesture. + * @param {Number} y The Y-coordinate of the center of the pinch gesture. + */ + this.onzoomend = null; + + /** + * Given a touch event, calculates the distance between the first two + * touches in pixels. + * + * @param {TouchEvent} e The touch event to use when performing distance + * calculation. + * @return {Number} The distance in pixels between the first two touches. + */ + function pinch_distance(e) { + + var touch_a = e.touches[0]; + var touch_b = e.touches[1]; + + var delta_x = touch_a.clientX - touch_b.clientX; + var delta_y = touch_a.clientY - touch_b.clientY; + + return Math.sqrt(delta_x*delta_x + delta_y*delta_y); + + } + + /** + * Given a touch event, calculates the center between the first two + * touches in pixels, returning the X coordinate of this center. + * + * @param {TouchEvent} e The touch event to use when performing center + * calculation. + * @return {Number} The X-coordinate of the center of the first two touches. + */ + function pinch_center_x(e) { + + var touch_a = e.touches[0]; + var touch_b = e.touches[1]; + + return (touch_a.clientX + touch_b.clientX) / 2; + + } + + /** + * Given a touch event, calculates the center between the first two + * touches in pixels, returning the Y coordinate of this center. + * + * @param {TouchEvent} e The touch event to use when performing center + * calculation. + * @return {Number} The Y-coordinate of the center of the first two touches. + */ + function pinch_center_y(e) { + + var touch_a = e.touches[0]; + var touch_b = e.touches[1]; + + return (touch_a.clientY + touch_b.clientY) / 2; + + } + + // When there are exactly two touches, monitor the distance between + // them, firing zoom events as appropriate + element.addEventListener("touchmove", function(e) { + if (e.touches.length === 2) { + + e.preventDefault(); + e.stopPropagation(); + + // Calculate current zoom level + var current = pinch_distance(e); + + // Calculate center + guac_zoom.centerX = pinch_center_x(e); + guac_zoom.centerY = pinch_center_y(e); + + // If gesture just starting, fire zoom start + if (!start_length) { + start_length = current; + guac_zoom.ratio = 1; + if (guac_zoom.onzoomstart) + guac_zoom.onzoomstart(guac_zoom.ratio, guac_zoom.centerX, guac_zoom.centerY); + } + + // Otherwise, notify of zoom change + else { + guac_zoom.ratio = current / start_length; + if (guac_zoom.onzoomchange) + guac_zoom.onzoomchange(guac_zoom.ratio, guac_zoom.centerX, guac_zoom.centerY); + } + + } + }, false); + + // Reset monitoring and fire end event when done + element.addEventListener("touchend", function(e) { + + if (start_length && e.touches.length < 2) { + + e.preventDefault(); + e.stopPropagation(); + + start_length = null; + if (guac_zoom.onzoomend) + guac_zoom.onzoomend(guac_zoom.ratio, guac_zoom.centerX, guac_zoom.centerY); + guac_zoom.ratio = 1; + } + + }, false); + +}; + +/** + * Flattens the attached Guacamole.Client, storing the result within the + * connection history. + */ +GuacUI.Client.updateThumbnail = function() { + + var guac = GuacUI.Client.attachedClient; + if (!guac) + return; + + // Do not create empty thumbnails + if (guac.getDisplay().getWidth() <= 0 || guac.getDisplay().getHeight() <= 0) + return; + + // Get screenshot + var canvas = guac.getDisplay().flatten(); + + // Calculate scale of thumbnail (max 320x240, max zoom 100%) + var scale = Math.min( + 320 / canvas.width, + 240 / canvas.height, + 1 + ); + + // Create thumbnail canvas + var thumbnail = document.createElement("canvas"); + thumbnail.width = canvas.width*scale; + thumbnail.height = canvas.height*scale; + + // Scale screenshot to thumbnail + var context = thumbnail.getContext("2d"); + context.drawImage(canvas, + 0, 0, canvas.width, canvas.height, + 0, 0, thumbnail.width, thumbnail.height + ); + + // Save thumbnail to history + var id = decodeURIComponent(window.location.search.substring(4)); + GuacamoleHistory.update(id, thumbnail.toDataURL()); + +}; + +/** + * Sets the current display scale to the given value, where 1 is 100% (1:1 + * pixel ratio). Out-of-range values will be clamped in-range. + * + * @param {Number} new_scale The new scale to apply + */ +GuacUI.Client.setScale = function(new_scale) { + + new_scale = Math.max(new_scale, GuacUI.Client.min_zoom); + new_scale = Math.min(new_scale, GuacUI.Client.max_zoom); + + if (GuacUI.Client.attachedClient) + GuacUI.Client.attachedClient.getDisplay().scale(new_scale); + + GuacUI.Client.zoom_state.textContent = Math.round(new_scale * 100) + "%"; + + // If at minimum zoom level, auto fit is ON + if (new_scale === GuacUI.Client.min_zoom) { + GuacUI.Client.main.style.overflow = "hidden"; + GuacUI.Client.auto_fit.checked = true; + GuacUI.Client.auto_fit.disabled = (GuacUI.Client.min_zoom >= 1); + } + + // If at minimum zoom level, auto fit is OFF + else { + GuacUI.Client.main.style.overflow = "auto"; + GuacUI.Client.auto_fit.checked = false; + GuacUI.Client.auto_fit.disabled = false; + } + +}; /** * Updates the scale of the attached Guacamole.Client based on current window @@ -558,28 +826,28 @@ GuacUI.Client.ModalStatus.prototype = new GuacUI.Component(); */ GuacUI.Client.updateDisplayScale = function() { - // Currently attacched client var guac = GuacUI.Client.attachedClient; + if (!guac) + return; - // If auto-fit is enabled, scale display - if (!GuacUI.Client.overrideAutoFit - && GuacUI.sessionState.getProperty("auto-fit")) { + // Determine whether display is currently fit to the screen + var auto_fit = (guac.getDisplay().getScale() === GuacUI.Client.min_zoom); - // Calculate scale to fit screen - var fit_scale = Math.min( - window.innerWidth / guac.getWidth(), - window.innerHeight / guac.getHeight() - ); - - // Scale client - if (fit_scale != guac.getScale()) - guac.scale(fit_scale); + // Calculate scale to fit screen + GuacUI.Client.min_zoom = Math.min( + GuacUI.Client.main.offsetWidth / Math.max(guac.getDisplay().getWidth(), 1), + GuacUI.Client.main.offsetHeight / Math.max(guac.getDisplay().getHeight(), 1) + ); - } + // Calculate appropriate maximum zoom level + GuacUI.Client.max_zoom = Math.max(GuacUI.Client.min_zoom, 3); - // Otherwise, scale to 100% - else if (guac.getScale() != 1.0) - guac.scale(1.0); + // Clamp zoom level, maintain auto-fit + if (guac.getDisplay().getScale() < GuacUI.Client.min_zoom || auto_fit) + GuacUI.Client.setScale(GuacUI.Client.min_zoom); + + else if (guac.getDisplay().getScale() > GuacUI.Client.max_zoom) + GuacUI.Client.setScale(GuacUI.Client.max_zoom); }; @@ -593,6 +861,51 @@ GuacUI.Client.updateTitle = function () { else document.title = GuacUI.Client.connectionName; + GuacUI.Client.menu_title.textContent = GuacUI.Client.connectionName; + +}; + +/** + * Sets whether the menu is currently visible. Keyboard is disabled while the + * menu is shown. + * + * @param {Boolean} [shown] Whether the menu should be shown. If omitted, this + * function will cause the menu to be shown by default. + */ +GuacUI.Client.showMenu = function(shown) { + if (shown === false) { + GuacUI.Client.menu.className = "closed"; + GuacUI.Client.commitClipboard(); + } + else + GuacUI.Client.menu.className = "open"; +}; + +/** + * Sets whether the text input box is currently visible. + * + * @param {Boolean} [shown] Whether the text input box should be shown. If + * omitted, this function will cause the menu to be + * shown by default. + */ +GuacUI.Client.showTextInput = function(shown) { + if (shown === false) { + GuacUI.Client.text_input.container.className = "closed"; + GuacUI.Client.text_input.target.blur(); + } + else { + GuacUI.Client.text_input.container.className = "open"; + GuacUI.Client.text_input.target.focus(); + } +}; + +/** + * Returns whether the menu is currently shown. + * + * @returns {Boolean} true if the menu is shown, false otherwise. + */ +GuacUI.Client.isMenuShown = function() { + return GuacUI.Client.menu.className === "open"; }; /** @@ -607,45 +920,283 @@ GuacUI.Client.hideStatus = function() { /** * Displays a status overlay with the given text. */ -GuacUI.Client.showStatus = function(status) { +GuacUI.Client.showStatus = function(title, status) { GuacUI.Client.hideStatus(); - GuacUI.Client.visibleStatus = new GuacUI.Client.ModalStatus(status); + GuacUI.Client.visibleStatus = new GuacUI.Client.ModalStatus(title, status); GuacUI.Client.visibleStatus.show(); }; /** * Displays an error status overlay with the given text. */ -GuacUI.Client.showError = function(status) { +GuacUI.Client.showError = function(title, status, reconnect) { GuacUI.Client.hideStatus(); GuacUI.Client.visibleStatus = - new GuacUI.Client.ModalStatus(status, "guac-error"); + new GuacUI.Client.ModalStatus(title, status, "guac-error", reconnect); GuacUI.Client.visibleStatus.show(); -} +}; + +GuacUI.Client.showNotification = function(message) { + + // Create notification + var element = GuacUI.createElement("div", "message notification"); + GuacUI.createChildElement(element, "div", "caption").textContent = message; + + // Add to DOM + GuacUI.Client.notification_area.appendChild(element); + + // Remove from DOM after around 5 seconds + window.setTimeout(function() { + GuacUI.Client.notification_area.removeChild(element); + }, 5000); + +}; + +/** + * Connects to the current Guacamole connection, attaching a new Guacamole + * client to the user interface. If a Guacamole client is already attached, + * it is replaced. + */ +GuacUI.Client.connect = function() { + + var tunnel; + + // If WebSocket available, try to use it. + /*if (window.WebSocket) + tunnel = new Guacamole.ChainedTunnel( + new Guacamole.WebSocketTunnel("websocket-tunnel"), + new Guacamole.HTTPTunnel("tunnel") + ) + + // If no WebSocket, then use HTTP. + else*/ + tunnel = new Guacamole.HTTPTunnel("tunnel"); + + // Instantiate client + var guac = new Guacamole.Client(tunnel); + + // Tie UI to client + GuacUI.Client.attach(guac); + + // Calculate optimal width/height for display + var pixel_density = window.devicePixelRatio || 1; + var optimal_dpi = pixel_density * 96; + var optimal_width = window.innerWidth * pixel_density; + var optimal_height = window.innerHeight * pixel_density; + + // Scale width/height to be at least 600x600 + if (optimal_width < 600 || optimal_height < 600) { + var scale = Math.max(600 / optimal_width, 600 / optimal_height); + optimal_width = optimal_width * scale; + optimal_height = optimal_height * scale; + } + + // Get entire query string, and pass to connect(). + // Normally, only the "id" parameter is required, but + // all parameters should be preserved and passed on for + // the sake of authentication. + + var connect_string = + window.location.search.substring(1) + + "&width=" + Math.floor(optimal_width) + + "&height=" + Math.floor(optimal_height) + + "&dpi=" + Math.floor(optimal_dpi); + + // Add audio mimetypes to connect_string + GuacUI.Audio.supported.forEach(function(mimetype) { + connect_string += "&audio=" + encodeURIComponent(mimetype); + }); + + // Add video mimetypes to connect_string + GuacUI.Video.supported.forEach(function(mimetype) { + connect_string += "&video=" + encodeURIComponent(mimetype); + }); + + // Show connection errors from tunnel + tunnel.onerror = function(status) { + var message = GuacUI.Client.tunnel_errors[status.code] || GuacUI.Client.tunnel_errors.DEFAULT; + GuacUI.Client.showError("Connection Error", message, + GuacUI.Client.tunnel_auto_reconnect[status.code] && GuacUI.Client.RECONNECT_PERIOD); + }; + + // Notify of disconnections (if not already notified of something else) + tunnel.onstatechange = function(state) { + if (state === Guacamole.Tunnel.State.CLOSED && !GuacUI.Client.visibleStatus) + //GuacUI.Client.showStatus("Disconnected", "You have been disconnected. Reload the page to reconnect."); + window.location = window.query.exit; + }; + + // Connect + guac.connect(connect_string); + + +}; + +/** + * Represents a number of bytes as a human-readable size string, including + * units. + * + * @param {Number} bytes The number of bytes. + * @returns {String} A human-readable string containing the size given. + */ +GuacUI.Client.getSizeString = function(bytes) { + + if (bytes > 1000000000) + return (bytes / 1000000000).toFixed(1) + " GB"; + + else if (bytes > 1000000) + return (bytes / 1000000).toFixed(1) + " MB"; + + else if (bytes > 1000) + return (bytes / 1000).toFixed(1) + " KB"; + + else + return bytes + " B"; + +}; + +/** + * Commits the current contents of the clipboard textarea to session storage, + * and thus to the remote clipboard if the client is connected. + */ +GuacUI.Client.commitClipboard = function() { + var new_value = GuacUI.Client.clipboard.value; + GuacamoleSessionStorage.setItem("clipboard", new_value); +}; + +/** + * Sets the contents of the remote clipboard, if the contents given are + * different. + * + * @param {String} data The data to assign to the clipboard. + */ +GuacUI.Client.setClipboard = function(data) { + + if (data !== GuacUI.Client.remote_clipboard && GuacUI.Client.attachedClient) { + GuacUI.Client.remote_clipboard = data; + GuacUI.Client.attachedClient.setClipboard(data); + } + +}; + +/** + * Sets the mouse emulation mode to absolute or relative. + * + * @param {Boolean} absolute Whether mouse emulation should use absolute + * (touchscreen) mode. + */ +GuacUI.Client.setMouseEmulationAbsolute = function(absolute) { + + function __handle_mouse_state(mouseState) { + + // Get client - do nothing if not attached + var guac = GuacUI.Client.attachedClient; + if (!guac) return; + + // Determine mouse position within view + var guac_display = guac.getDisplay().getElement(); + var mouse_view_x = mouseState.x + guac_display.offsetLeft - GuacUI.Client.main.scrollLeft; + var mouse_view_y = mouseState.y + guac_display.offsetTop - GuacUI.Client.main.scrollTop; + + // Determine viewport dimensioins + var view_width = GuacUI.Client.main.offsetWidth; + var view_height = GuacUI.Client.main.offsetHeight; + + // Determine scroll amounts based on mouse position relative to document + + var scroll_amount_x; + if (mouse_view_x > view_width) + scroll_amount_x = mouse_view_x - view_width; + else if (mouse_view_x < 0) + scroll_amount_x = mouse_view_x; + else + scroll_amount_x = 0; + + var scroll_amount_y; + if (mouse_view_y > view_height) + scroll_amount_y = mouse_view_y - view_height; + else if (mouse_view_y < 0) + scroll_amount_y = mouse_view_y; + else + scroll_amount_y = 0; + + // Scroll (if necessary) to keep mouse on screen. + GuacUI.Client.main.scrollLeft += scroll_amount_x; + GuacUI.Client.main.scrollTop += scroll_amount_y; + + // Scale event by current scale + var scaledState = new Guacamole.Mouse.State( + mouseState.x / guac.getDisplay().getScale(), + mouseState.y / guac.getDisplay().getScale(), + mouseState.left, + mouseState.middle, + mouseState.right, + mouseState.up, + mouseState.down); + + // Send mouse event + guac.sendMouseState(scaledState); + + }; + + var new_mode, old_mode; + GuacUI.Client.emulate_absolute = absolute; + + // Switch to touchscreen if absolute + if (absolute) { + new_mode = GuacUI.Client.touch_screen; + old_mode = GuacUI.Client.touch; + } + + // Switch to touchpad if not absolute (relative) + else { + new_mode = GuacUI.Client.touch_pad; + old_mode = GuacUI.Client.touch; + } + + // Perform switch + if (new_mode) { + + if (old_mode) { + old_mode.onmousedown = old_mode.onmouseup = old_mode.onmousemove = null; + new_mode.currentState.x = old_mode.currentState.x; + new_mode.currentState.y = old_mode.currentState.y; + } + + new_mode.onmousedown = new_mode.onmouseup = new_mode.onmousemove = __handle_mouse_state; + GuacUI.Client.touch = new_mode; + } + +}; /** * Attaches a Guacamole.Client to the client UI, such that Guacamole events - * affect the UI, and local events affect the Guacamole.Client. + * affect the UI, and local events affect the Guacamole.Client. If a client + * is already attached, it is replaced. * * @param {Guacamole.Client} guac The Guacamole.Client to attach to the UI. */ GuacUI.Client.attach = function(guac) { + // If a client is already attached, ensure it is disconnected + if (GuacUI.Client.attachedClient) + GuacUI.Client.attachedClient.disconnect(); + // Store attached client GuacUI.Client.attachedClient = guac; // Get display element - var guac_display = guac.getDisplay(); + var guac_display = guac.getDisplay().getElement(); /* * Update the scale of the display when the client display size changes. */ - guac.onresize = function(width, height) { + guac.getDisplay().onresize = function(width, height) { GuacUI.Client.updateDisplayScale(); - } + }; /* * Update UI when the state of the Guacamole.Client changes. @@ -657,19 +1208,19 @@ GuacUI.Client.attach = function(guac) { // Idle case 0: - GuacUI.Client.showStatus("Idle."); + GuacUI.Client.showStatus(null, "Idle."); GuacUI.Client.titlePrefix = "[Idle]"; break; // Connecting case 1: - GuacUI.Client.showStatus("Connecting..."); + GuacUI.Client.showStatus("Connecting", "Connecting to Guacamole..."); GuacUI.Client.titlePrefix = "[Connecting...]"; break; // Connected + waiting case 2: - GuacUI.Client.showStatus("Connected, waiting for first update..."); + GuacUI.Client.showStatus("Connecting", "Connected to Guacamole. Waiting for response..."); GuacUI.Client.titlePrefix = "[Waiting...]"; break; @@ -680,27 +1231,20 @@ GuacUI.Client.attach = function(guac) { GuacUI.Client.titlePrefix = null; // Update clipboard with current data - if (GuacUI.sessionState.getProperty("clipboard")) - guac.setClipboard(GuacUI.sessionState.getProperty("clipboard")); + var clipboard = GuacamoleSessionStorage.getItem("clipboard"); + if (clipboard) + GuacUI.Client.setClipboard(clipboard); break; - // Disconnecting + // Disconnecting / disconnected are handled by tunnel instead case 4: - GuacUI.Client.showStatus("Disconnecting..."); - GuacUI.Client.titlePrefix = "[Disconnecting...]"; - break; - - // Disconnected case 5: - GuacUI.Client.showStatus("Disconnected."); - GuacUI.Client.titlePrefix = "[Disconnected]"; - window.location.assign(GuacUI.sessionState.getProperty("exitUrl")); break; // Unknown status code default: - GuacUI.Client.showStatus("[UNKNOWN STATUS]"); + GuacUI.Client.showStatus("Unknown Status", "An unknown status code was received. This is most likely a bug."); } @@ -722,59 +1266,68 @@ GuacUI.Client.attach = function(guac) { * receives an error. */ - guac.onerror = function(error) { + guac.onerror = function(status) { // Disconnect, if connected guac.disconnect(); - + // Display error message - GuacUI.Client.showError(error); + var message = GuacUI.Client.client_errors[status.code] || GuacUI.Client.client_errors.DEFAULT; + GuacUI.Client.showError("Connection Error", message, + GuacUI.Client.client_auto_reconnect[status.code] && GuacUI.Client.RECONNECT_PERIOD); - window.location.assign(GuacUI.sessionState.getProperty("exitUrl")); }; // Server copy handler - guac.onclipboard = function(data) { - GuacUI.sessionState.setProperty("clipboard", data); + guac.onclipboard = function(stream, mimetype) { + + // Only text/plain is supported for now + if (mimetype !== "text/plain") { + stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); + return; + } + + var reader = new Guacamole.StringReader(stream); + var data = ""; + + // Append any received data to buffer + reader.ontext = function clipboard_text_received(text) { + data += text; + stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); + }; + + // Set contents when done + reader.onend = function clipboard_text_end() { + GuacUI.Client.remote_clipboard = data; + GuacamoleSessionStorage.setItem("clipboard", data); + }; + }; /* * Prompt to download file when file received. */ - function getSizeString(bytes) { + guac.onfile = function(stream, mimetype, filename) { - if (bytes > 1000000000) - return (bytes / 1000000000).toFixed(1) + " GB"; + var download = new GuacUI.Download(filename); + download.updateProgress(GuacUI.Client.getSizeString(0)); - else if (bytes > 1000000) - return (bytes / 1000000).toFixed(1) + " MB"; - - else if (bytes > 1000) - return (bytes / 1000).toFixed(1) + " KB"; - - else - return bytes + " B"; - - } - - guac.onblob = function(blob) { - - var download = new GuacUI.Download(blob.name); - download.updateProgress(getSizeString(0)); + var blob_reader = new Guacamole.BlobReader(stream, mimetype); GuacUI.Client.notification_area.appendChild(download.getElement()); // Update progress as data is received - blob.ondata = function() { - download.updateProgress(getSizeString(blob.getLength())); + blob_reader.onprogress = function() { + download.updateProgress(GuacUI.Client.getSizeString(blob_reader.getLength())); + stream.sendAck("Received", 0x0000); }; // When complete, prompt for download - blob.oncomplete = function() { + blob_reader.onend = function() { download.ondownload = function() { - saveAs(blob.getBlob(), blob.name.replace(/[\u0080-\uffff]/g, "_").replace(/\s/g, "_")); + saveAs(blob_reader.getBlob(), filename); }; download.complete(); @@ -786,6 +1339,8 @@ GuacUI.Client.attach = function(guac) { GuacUI.Client.notification_area.removeChild(download.getElement()); }; + stream.sendAck("Ready", 0x0000); + }; /* @@ -801,91 +1356,158 @@ GuacUI.Client.attach = function(guac) { * Handle mouse and touch events relative to the display element. */ + // Touchscreen + var touch_screen = new Guacamole.Mouse.Touchscreen(guac_display); + GuacUI.Client.touch_screen = touch_screen; + + // Touchpad + var touch_pad = new Guacamole.Mouse.Touchpad(guac_display); + GuacUI.Client.touch_pad = touch_pad; + + // Init emulation mode for client + GuacUI.Client.setMouseEmulationAbsolute(GuacUI.Client.absolute_radio.checked); + // Mouse var mouse = new Guacamole.Mouse(guac_display); - var touch = new Guacamole.Mouse.Touchpad(guac_display); - touch.onmousedown = touch.onmouseup = touch.onmousemove = - mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = - function(mouseState) { - - // Determine mouse position within view - var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset; - var mouse_view_y = mouseState.y + guac_display.offsetTop - window.pageYOffset; + mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = function(mouseState) { - // Determine viewport dimensioins - var view_width = GuacUI.Client.viewport.offsetWidth; - var view_height = GuacUI.Client.viewport.offsetHeight; + // Scale event by current scale + var scaledState = new Guacamole.Mouse.State( + mouseState.x / guac.getDisplay().getScale(), + mouseState.y / guac.getDisplay().getScale(), + mouseState.left, + mouseState.middle, + mouseState.right, + mouseState.up, + mouseState.down); - // Determine scroll amounts based on mouse position relative to document + // Send mouse event + guac.sendMouseState(scaledState); + + }; - var scroll_amount_x; - if (mouse_view_x > view_width) - scroll_amount_x = mouse_view_x - view_width; - else if (mouse_view_x < 0) - scroll_amount_x = mouse_view_x; - else - scroll_amount_x = 0; - var scroll_amount_y; - if (mouse_view_y > view_height) - scroll_amount_y = mouse_view_y - view_height; - else if (mouse_view_y < 0) - scroll_amount_y = mouse_view_y; - else - scroll_amount_y = 0; + // Hide any existing status notifications + GuacUI.Client.hideStatus(); - // Scroll (if necessary) to keep mouse on screen. - window.scrollBy(scroll_amount_x, scroll_amount_y); + // Remove old client from UI, if any + GuacUI.Client.display.innerHTML = ""; - // Scale event by current scale - var scaledState = new Guacamole.Mouse.State( - mouseState.x / guac.getScale(), - mouseState.y / guac.getScale(), - mouseState.left, - mouseState.middle, - mouseState.right, - mouseState.up, - mouseState.down); + // Add client to UI + guac.getDisplay().getElement().className = "software-cursor"; + GuacUI.Client.display.appendChild(guac.getDisplay().getElement()); - // Send mouse event - guac.sendMouseState(scaledState); - - }; +}; + +// One-time UI initialization +(function() { + + var i; + + /** + * Keys which should be allowed through to the client when in text input + * mode, providing corresponding key events are received. Keys in this + * set will be allowed through to the server. + */ + var IME_ALLOWED_KEYS = { + 0xFF08: true, /* Backspace */ + 0xFF09: true, /* Tab */ + 0xFF0D: true, /* Enter */ + 0xFF1B: true, /* Escape */ + 0xFF50: true, /* Home */ + 0xFF51: true, /* Left */ + 0xFF52: true, /* Up */ + 0xFF53: true, /* Right */ + 0xFF54: true, /* Down */ + 0xFF57: true, /* End */ + 0xFF64: true, /* Insert */ + 0xFFBE: true, /* F1 */ + 0xFFBF: true, /* F2 */ + 0xFFC0: true, /* F3 */ + 0xFFC1: true, /* F4 */ + 0xFFC2: true, /* F5 */ + 0xFFC3: true, /* F6 */ + 0xFFC4: true, /* F7 */ + 0xFFC5: true, /* F8 */ + 0xFFC6: true, /* F9 */ + 0xFFC7: true, /* F10 */ + 0xFFC8: true, /* F11 */ + 0xFFC9: true, /* F12 */ + 0xFFE1: true, /* Left shift */ + 0xFFE2: true, /* Right shift */ + 0xFFFF: true /* Delete */ + }; /* * Route document-level keyboard events to the client. */ - var keyboard = new Guacamole.Keyboard(document); var show_keyboard_gesture_possible = true; + function __send_key(pressed, keysym) { + + // Do not send key if menu shown + if (GuacUI.Client.isMenuShown()) + return true; + + // Allow all but specific keys through to browser when in IME mode + if (GuacUI.Client.text_input.enabled && !IME_ALLOWED_KEYS[keysym]) + return true; + + GuacUI.Client.attachedClient.sendKeyEvent(pressed, keysym); + return false; + + } + keyboard.onkeydown = function (keysym) { - guac.sendKeyEvent(1, keysym); + + // Only handle key events if client is attached + var guac = GuacUI.Client.attachedClient; + if (!guac) return true; + + // Handle Ctrl-shortcuts specifically + if (keyboard.modifiers.ctrl && !keyboard.modifiers.alt && !keyboard.modifiers.shift) { + + // Allow event through if Ctrl+C or Ctrl+X + if (keyboard.pressed[0x63] || keyboard.pressed[0x78]) { + __send_key(1, keysym); + return true; + } + + // If Ctrl+V, wait until after paste event (next event loop) + if (keyboard.pressed[0x76]) { + window.setTimeout(function after_paste() { + __send_key(1, keysym); + }, 10); + return true; + } + + } // If key is NOT one of the expected keys, gesture not possible - if (keysym != 0xFFE3 && keysym != 0xFFE9 && keysym != 0xFFE1) + if (keysym !== 0xFFE3 && keysym !== 0xFFE9 && keysym !== 0xFFE1) show_keyboard_gesture_possible = false; + // Send key event + return __send_key(1, keysym); + }; keyboard.onkeyup = function (keysym) { - guac.sendKeyEvent(0, keysym); - // If lifting up on shift, toggle keyboard if rest of gesture + // Only handle key events if client is attached + var guac = GuacUI.Client.attachedClient; + if (!guac) return true; + + // If lifting up on shift, toggle menu visibility if rest of gesture // conditions satisfied - if (show_keyboard_gesture_possible && keysym == 0xFFE1) { - if (keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) { - - // If in INTERACTIVE mode, switch to OSK - if (GuacUI.StateManager.getState() == GuacUI.Client.states.INTERACTIVE) - GuacUI.StateManager.setState(GuacUI.Client.states.OSK); - - // If in OSK mode, switch to INTERACTIVE - else if (GuacUI.StateManager.getState() == GuacUI.Client.states.OSK) - GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE); - - } + if (show_keyboard_gesture_possible && keysym === 0xFFE1 + && keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) { + __send_key(0, 0xFFE1); + __send_key(0, 0xFFE9); + __send_key(0, 0xFFE3); + GuacUI.Client.showMenu(!GuacUI.Client.isMenuShown()); } // Detect if no keys are pressed @@ -899,97 +1521,792 @@ GuacUI.Client.attach = function(guac) { if (reset_gesture) show_keyboard_gesture_possible = true; + // Send key event + return __send_key(0, keysym); + }; + /** + * Returns the contents of the remote clipboard if clipboard integration is + * enabled, and null otherwise. + */ + function get_clipboard_data() { + + // If integration not enabled, do not attempt retrieval + if (GuacUI.Client.clipboard_integration_enabled === false) + return null; + + // Otherwise, attempt retrieval and update integration status + try { + var data = GuacamoleService.Clipboard.get(); + GuacUI.Client.clipboard_integration_enabled = true; + return data; + } + catch (status) { + GuacUI.Client.clipboard_integration_enabled = false; + return null; + } + + } + + // Set local clipboard contents on cut + document.body.addEventListener("cut", function handle_cut(e) { + var data = get_clipboard_data(); + if (data !== null) { + e.preventDefault(); + e.clipboardData.setData("text/plain", data); + } + }, false); + + // Set local clipboard contents on copy + document.body.addEventListener("copy", function handle_copy(e) { + var data = get_clipboard_data(); + if (data !== null) { + e.preventDefault(); + e.clipboardData.setData("text/plain", data); + } + }, false); + + // Set remote clipboard contents on paste + document.body.addEventListener("paste", function handle_paste(e) { + + // If status of clipboard integration is unknown, attempt to define it + if (GuacUI.Client.clipboard_integration_enabled === undefined) + get_clipboard_data(); + + // Override and handle paste only if integration is enabled + if (GuacUI.Client.clipboard_integration_enabled) { + e.preventDefault(); + GuacUI.Client.setClipboard(e.clipboardData.getData("text/plain")); + } + + }, false); /* * Disconnect and update thumbnail on close */ window.onunload = function() { - guac.disconnect(); + + GuacUI.Client.updateThumbnail(); + + if (GuacUI.Client.attachedClient) + GuacUI.Client.attachedClient.disconnect(); }; /* - * Send size events on resize + * Reflow layout and send size events on resize/scroll */ - window.onresize = function() { - guac.sendSize(window.innerWidth, window.innerHeight); - GuacUI.Client.updateDisplayScale(); + var last_scroll_left = 0; + var last_scroll_top = 0; + var last_scroll_width = 0; + var last_scroll_height = 0; + var last_window_width = 0; + var last_window_height = 0; - }; + function __update_layout() { - GuacUI.sessionState.onchange = function(old_state, new_state, name) { - if (name == "clipboard") - guac.setClipboard(new_state[name]); - else if (name == "auto-fit") + // Only reflow if size or scroll have changed + if (document.body.scrollLeft !== last_scroll_left + || document.body.scrollTop !== last_scroll_top + || document.body.scrollWidth !== last_scroll_width + || document.body.scrollHeight !== last_scroll_height + || window.innerWidth !== last_window_width + || window.innerHeight !== last_window_height) { + + last_scroll_top = document.body.scrollTop; + last_scroll_left = document.body.scrollLeft; + last_scroll_width = document.body.scrollWidth; + last_scroll_height = document.body.scrollHeight; + last_window_width = window.innerWidth; + last_window_height = window.innerHeight; + + // Reset scroll and reposition document such that it's on-screen + window.scrollTo(document.body.scrollWidth, document.body.scrollHeight); + + // Determine height of bottom section (currently only text input) + var bottom = GuacUI.Client.text_input.container; + var bottom_height = (bottom && bottom.offsetHeight) | 0; + + // Calculate correct height of main section (display) + var main_width = window.innerWidth; + var main_height = window.innerHeight - bottom_height; + + // Anchor main to top-left of viewport, sized to fit above bottom + var main = GuacUI.Client.main; + main.style.top = document.body.scrollTop + "px"; + main.style.left = document.body.scrollLeft + "px"; + main.style.width = main_width + "px"; + main.style.height = main_height + "px"; + + // Anchor bottom to bottom of viewport + if (bottom) { + bottom.style.top = (document.body.scrollTop + main_height) + "px"; + bottom.style.left = document.body.scrollLeft + "px"; + bottom.style.width = window.innerWidth + "px"; + } + + // Send new size + if (GuacUI.Client.attachedClient) { + var pixel_density = window.devicePixelRatio || 1; + var width = main_width * pixel_density; + var height = main_height * pixel_density; + GuacUI.Client.attachedClient.sendSize(width, height); + } + + // Rescale display appropriately GuacUI.Client.updateDisplayScale(); - }; - var long_press_start_x = 0; - var long_press_start_y = 0; - var longPressTimeout = null; + } - GuacUI.Client.startLongPressDetect = function() { + } - if (!longPressTimeout) { + window.onresize = __update_layout; + window.onscroll = __update_layout; + window.setInterval(__update_layout, 10); - longPressTimeout = window.setTimeout(function() { - longPressTimeout = null; + GuacamoleSessionStorage.addChangeListener(function(name, value) { + if (name === "clipboard") { + GuacUI.Client.clipboard.value = value; + GuacUI.Client.setClipboard(value); + } + }); - // If screen shrunken, show magnifier - if (GuacUI.Client.attachedClient.getScale() < 1.0) - GuacUI.StateManager.setState(GuacUI.Client.states.MAGNIFIER); + /** + * Ignores the given event. + * + * @private + * @param {Event} e The event to ignore. + */ + function _ignore(e) { + e.preventDefault(); + e.stopPropagation(); + } - // Otherwise, if screen too big to fit, use panning mode - else if ( - GuacUI.Client.attachedClient.getWidth() > window.innerWidth - || GuacUI.Client.attachedClient.getHeight() > window.innerHeight - ) - GuacUI.StateManager.setState(GuacUI.Client.states.PAN); + /** + * Converts the given bytes to a base64-encoded string. + * + * @private + * @param {Uint8Array} bytes A Uint8Array which contains the data to be + * encoded as base64. + * @return {String} The base64-encoded string. + */ + function _get_base64(bytes) { - // Otherwise, just show a hint + var data = ""; + + // Produce binary string from bytes in buffer + for (var i=0; i= bytes.length) { + stream.sendEnd(); + GuacUI.Client.notification_area.removeChild(upload.getElement()); + GuacUI.Client.showNotification("Upload of \"" + file.name + "\" complete."); + } + + // Otherwise, update progress else - GuacUI.StateManager.setState(GuacUI.Client.states.WAIT_TYPING); - }, GuacUI.Client.LONG_PRESS_DETECT_TIMEOUT); + upload.updateProgress(GuacUI.Client.getSizeString(offset), offset / bytes.length * 100); + + }; + + // Close dialog and abort when close is clicked + upload.onclose = function() { + GuacUI.Client.notification_area.removeChild(upload.getElement()); + // TODO: Abort transfer + }; + + }; + reader.readAsArrayBuffer(file); + + } + + // Handle and ignore dragenter/dragover + GuacUI.Client.display.addEventListener("dragenter", _ignore, false); + GuacUI.Client.display.addEventListener("dragover", _ignore, false); + + // File drop event handler + GuacUI.Client.display.addEventListener("drop", function(e) { + + e.preventDefault(); + e.stopPropagation(); + + // Ignore file drops if no attached client + if (!GuacUI.Client.attachedClient) return; + + // Upload each file + var files = e.dataTransfer.files; + for (var i=0; i= 64 && Math.abs(dy) < 32 && duration < 250) { + GuacUI.Client.showMenu(); + guac_drag.cancel(); + } + } + + // Hide menu if swiping left + else if (GuacUI.Client.isMenuShown()) { + + GuacUI.Client.menu.scrollLeft -= change_drag_dx; + GuacUI.Client.menu.scrollTop -= change_drag_dy; + + if (dx <= -64 && Math.abs(dy) < 32 && duration < 250) { + GuacUI.Client.showMenu(false); + guac_drag.cancel(); + } + + } + + // Otherwise, drag UI (if not relative emulation) + else if (GuacUI.Client.emulate_absolute) { + GuacUI.Client.main.scrollLeft -= change_drag_dx; + GuacUI.Client.main.scrollTop -= change_drag_dy; + } + + last_drag_dx = dx; + last_drag_dy = dy; } }; - GuacUI.Client.stopLongPressDetect = function() { - window.clearTimeout(longPressTimeout); - longPressTimeout = null; + /* + * Initialize clipboard with current data + */ + + GuacUI.Client.clipboard.value = GuacamoleSessionStorage.getItem("clipboard", ""); + + /* + * Update clipboard contents when changed + */ + + window.onblur = + GuacUI.Client.clipboard.onchange = function() { + GuacUI.Client.commitClipboard(); }; - // Detect long-press at bottom of screen - GuacUI.Client.display.addEventListener('touchstart', function(e) { - - // Record touch location - if (e.touches.length == 1) { - var touch = e.touches[0]; - long_press_start_x = touch.screenX; - long_press_start_y = touch.screenY; + /* + * Update emulation mode when changed + */ + + GuacUI.Client.absolute_radio.onclick = + GuacUI.Client.absolute_radio.onchange = function() { + if (!GuacUI.Client.emulate_absolute) { + GuacUI.Client.showNotification("Absolute mouse emulation selected"); + GuacUI.Client.setMouseEmulationAbsolute(GuacUI.Client.absolute_radio.checked); + GuacUI.Client.showMenu(false); } - - // Start detection - GuacUI.Client.startLongPressDetect(); - - }, true); + }; - // Stop detection if touch moves significantly - GuacUI.Client.display.addEventListener('touchmove', function(e) { - - // If touch distance from start exceeds threshold, cancel long press - var touch = e.touches[0]; - if (Math.abs(touch.screenX - long_press_start_x) >= GuacUI.Client.LONG_PRESS_MOVEMENT_THRESHOLD - || Math.abs(touch.screenY - long_press_start_y) >= GuacUI.Client.LONG_PRESS_MOVEMENT_THRESHOLD) - GuacUI.Client.stopLongPressDetect(); - - }, true); + GuacUI.Client.relative_radio.onclick = + GuacUI.Client.relative_radio.onchange = function() { + if (GuacUI.Client.emulate_absolute) { + GuacUI.Client.showNotification("Relative mouse emulation selected"); + GuacUI.Client.setMouseEmulationAbsolute(!GuacUI.Client.relative_radio.checked); + GuacUI.Client.showMenu(false); + } + }; - // Stop detection if press stops - GuacUI.Client.display.addEventListener('touchend', GuacUI.Client.stopLongPressDetect, true); + /* + * Update input method mode when changed + */ -}; + GuacUI.Client.ime_none_radio.onclick = + GuacUI.Client.ime_none_radio.onchange = function() { + GuacUI.Client.showTextInput(false); + GuacUI.Client.OnScreenKeyboard.hide(); + GuacUI.Client.showMenu(false); + }; + GuacUI.Client.ime_text_radio.onclick = + GuacUI.Client.ime_text_radio.onchange = function() { + GuacUI.Client.showTextInput(true); + GuacUI.Client.OnScreenKeyboard.hide(); + GuacUI.Client.showMenu(false); + }; + + GuacUI.Client.ime_osk_radio.onclick = + GuacUI.Client.ime_osk_radio.onchange = function() { + GuacUI.Client.showTextInput(false); + GuacUI.Client.OnScreenKeyboard.show(); + GuacUI.Client.showMenu(false); + }; + + /* + * Text input + */ + + // Disable automatic input features on platforms that support these attributes + GuacUI.Client.text_input.target.setAttribute("autocapitalize", "off"); + GuacUI.Client.text_input.target.setAttribute("autocorrect", "off"); + GuacUI.Client.text_input.target.setAttribute("autocomplete", "off"); + GuacUI.Client.text_input.target.setAttribute("spellcheck", "off"); + + function keysym_from_codepoint(codepoint) { + + // Keysyms for control characters + if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F)) + return 0xFF00 | codepoint; + + // Keysyms for ASCII chars + if (codepoint >= 0x0000 && codepoint <= 0x00FF) + return codepoint; + + // Keysyms for Unicode + if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) + return 0x01000000 | codepoint; + + return null; + + } + + /** + * Presses and releases the key corresponding to the given keysym, as if + * typed by the user. + * + * @param {Number} keysym The keysym of the key to send. + */ + function send_keysym(keysym) { + + var guac = GuacUI.Client.attachedClient; + if (!guac) + return; + + guac.sendKeyEvent(1, keysym); + guac.sendKeyEvent(0, keysym); + + } + + /** + * Presses and releases the key having the keysym corresponding to the + * Unicode codepoint given, as if typed by the user. + * + * @param {Number} codepoint The Unicode codepoint of the key to send. + */ + function send_codepoint(codepoint) { + + if (codepoint === 10) { + send_keysym(0xFF0D); + release_sticky_keys(); + return; + } + + var keysym = keysym_from_codepoint(codepoint); + if (keysym) { + send_keysym(keysym); + release_sticky_keys(); + } + + } + + /** + * Translates each character within the given string to keysyms and sends + * each, in order, as if typed by the user. + * + * @param {String} content The string to send. + */ + function send_string(content) { + + var sent_text = ""; + + for (var i=0; i + */ + var active_sticky_keys = {}; + + /** + * Presses/releases the keysym defined by the "data-keysym" attribute on + * the given element whenever the element is pressed. The "data-sticky" + * attribute, if present and set to "true", causes the key to remain + * pressed until text is sent. + * + * @param {Element} key The element which will control its associated key. + */ + function apply_key_behavior(key) { + + function __update_key(e) { + + var guac = GuacUI.Client.attachedClient; + if (!guac) + return; + + e.preventDefault(); + e.stopPropagation(); + + // Pull properties of key + var keysym = parseInt(key.getAttribute("data-keysym")); + var sticky = (key.getAttribute("data-sticky") === "true"); + var pressed = (key.className.indexOf("pressed") !== -1); + + // If sticky, toggle pressed state + if (sticky) { + if (pressed) { + GuacUI.removeClass(key, "pressed"); + guac.sendKeyEvent(0, keysym); + delete active_sticky_keys[keysym]; + } + else { + GuacUI.addClass(key, "pressed"); + guac.sendKeyEvent(1, keysym); + active_sticky_keys[keysym] = key; + } + } + + // For all non-sticky keys, press and release key immediately + else + send_keysym(keysym); + + } + + var ignore_mouse = false; + + // Press/release key when clicked + key.addEventListener("click", function __mouse_key(e) { + + // Ignore clicks which follow touches + if (ignore_mouse) + return; + + __update_key(e); + + }, false); + + // Press/release key when tapped + key.addEventListener("touchstart", function __touch_key(e) { + + // Ignore following clicks + ignore_mouse = true; + + __update_key(e); + + }, false); + + // Restore handling of mouse events when mouse is used + key.addEventListener("mousemove", function __reset_mouse() { + ignore_mouse = false; + }, false); + + } + + /** + * Releases all currently-held sticky keys within the text input UI. + */ + function release_sticky_keys() { + + var guac = GuacUI.Client.attachedClient; + if (!guac) + return; + + // Release all active sticky keys + for (var keysym in active_sticky_keys) { + var key = active_sticky_keys[keysym]; + GuacUI.removeClass(key, "pressed"); + guac.sendKeyEvent(0, keysym); + } + + // Reset set of active keys + active_sticky_keys = {}; + + } + + // Apply key behavior to all keys within the text input UI + var keys = GuacUI.Client.text_input.container.getElementsByClassName("key"); + for (i=0; i. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ /** @@ -22,11 +26,6 @@ */ var GuacUI = GuacUI || {}; -/** - * Current session state, including settings. - */ -GuacUI.sessionState = new GuacamoleSessionState(); - /** * Creates a new element having the given tagname and CSS class. */ @@ -108,6 +107,55 @@ GuacUI.removeClass = function(element, classname) { }; +/** + * Opens the connection group having the given ID in a new tab/window. + * + * @param {String} id The ID of the connection group to open. + * @param {String} parameters Any parameters that should be added to the URL, + * for sake of authentication. + */ +GuacUI.openConnectionGroup = function(id, parameters) { + GuacUI.openObject("g/" + id, parameters); +}; + +/** + * Opens the connection having the given ID in a new tab/window. + * + * @param {String} id The ID of the connection to open. + * @param {String} parameters Any parameters that should be added to the URL, + * for sake of authentication. + */ +GuacUI.openConnection = function(id, parameters) { + GuacUI.openObject("c/" + id, parameters); +}; + +/** + * Opens the object having the given ID in a new tab/window. The ID must + * include the relevant prefix. + * + * @param {String} id The ID of the object to open, including prefix. + * @param {String} parameters Any parameters that should be added to the URL, + * for sake of authentication. + */ +GuacUI.openObject = function(id, parameters) { + + // Get URL + var url = "client.xhtml?id=" + encodeURIComponent(id); + + // Add parameters, if given + if (parameters) + url += "&" + parameters; + + // Attempt to focus existing window + var current = window.open(null, id); + + // If window did not already exist, set up as + // Guacamole client + if (!current.GuacUI) + window.open(url, id); + +}; + /** * Object describing the UI's level of audio support. If the user has request * that audio be disabled, this object will pretend that audio is not @@ -133,7 +181,7 @@ GuacUI.Audio = new (function() { this.supported = []; // If sound disabled, we're done now. - if (GuacUI.sessionState.getProperty("disable-sound")) + if (GuacamoleSessionStorage.getItem("disable-sound", false)) return; // Build array of supported audio formats @@ -214,203 +262,6 @@ GuacUI.Video = new (function() { })(); - -/** - * Central registry of all components for all states. - */ -GuacUI.StateManager = new (function() { - - /** - * The current state. - */ - var current_state = null; - - /** - * Array of arrays of components, indexed by the states they are in. - */ - var components = []; - - /** - * Registers the given component with this state manager, to be shown - * during the given states. - * - * @param {GuacUI.Component} component The component to register. - * @param {Number} [...] The list of states this component should be - * visible during. - */ - this.registerComponent = function(component) { - - // For each state specified, add the given component - for (var i=1; i