diff --git a/guacamole-common-js/.gitignore b/guacamole-common-js/.gitignore new file mode 100644 index 000000000..42f4a1a64 --- /dev/null +++ b/guacamole-common-js/.gitignore @@ -0,0 +1,2 @@ +target/ +*~ diff --git a/guacamole-common-js/ChangeLog b/guacamole-common-js/ChangeLog new file mode 100644 index 000000000..c95084408 --- /dev/null +++ b/guacamole-common-js/ChangeLog @@ -0,0 +1,57 @@ +2013-05-29 Michael Jumper + + * Fix support for AltGr and modifiers + * Handle bad source rect sizes in copy + +2012-10-24 Michael Jumper + + * Implement base audio support + * Implement audio instruction + * Add width/height getters to client + * Add onresize event to client + +2012-10-16 Michael Jumper + + * Fix stream inefficiency bug (ticket #201) + +2012-09-27 Michael Jumper + + * Fix variable naming conflict (ticket #191) + +2012-08-11 Michael Jumper + + * Improve documentation + +2012-08-02 Michael Jumper + + * Fix keyboard event handling + +2012-08-01 Michael Jumper + + * Implement absolute touch device emulation + +2012-05-04 Michael Jumper + + * Improved iPad and touch device support + * Improved touch support + * Implemented new drawing instructions + * Nestable layers + +2011-12-11 Michael Jumper + + * Implemented improved instruction format + * Fixed errors in IE in on-screen keyboard + * Relicensed as Mozilla/LGPL/GPL + * Touch support (emulates touchpad control of mouse pointer) + * "rect" and "clip" instructions + +2011-07-13 Michael Jumper + + * "sync" instruction + * Channel masks (alpha compositing) + * Multiple tunnel support + +2011-03-02 Michael Jumper + + * Initial release + diff --git a/guacamole-common-js/LICENSE b/guacamole-common-js/LICENSE new file mode 100644 index 000000000..7714141d1 --- /dev/null +++ b/guacamole-common-js/LICENSE @@ -0,0 +1,470 @@ + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + +1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + +2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + +3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + +4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + +6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + +7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + +9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + +11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + +12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + +13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + +EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] + diff --git a/guacamole-common-js/README b/guacamole-common-js/README new file mode 100644 index 000000000..3929fe4bb --- /dev/null +++ b/guacamole-common-js/README @@ -0,0 +1,78 @@ + +------------------------------------------------------------ + About this README +------------------------------------------------------------ + +This README is intended to provide quick and to-the-point documentation for +technical users intending to compile parts of Guacamole themselves. + +Distribution-specific packages are available from the files section of the main +project page: + + http://sourceforge.net/projects/guacamole/files/ + +Distribution-specific documentation is provided on the Guacamole wiki: + + http://guac-dev.org/ + + +------------------------------------------------------------ + What is guacamole-common-js? +------------------------------------------------------------ + +guacamole-common-js is the core JavaScript library used by the Guacamole web +application. + +guacamole-common-js provides an efficient HTTP tunnel for transporting +protocol data between JavaScript and the web application, as well as an +implementation of a Guacamole protocol client and abstract synchronized +drawing layers. + + +------------------------------------------------------------ + Compiling and installing guacamole-common-js +------------------------------------------------------------ + +guacamole-common-js is built using Maven. Note that this is JavaScript code +and not actually compiled. "Building" guacamole-common-js actually simply +packages it into a redistributable .zip file. This .zip file can be easily +included and expanded into other Maven-based projects (like Guacamole). + +Note that prebuilt versions of guacamole-common-js are available from the +main guac-dev.org Maven repository which is referenced in all Maven +projects in Guacamole. Unless you want to make changes to guacamole-common-js +or you want to use a newer, unreleased version (such as the unstable branch), +you do not need to build this manually. You can let Maven download it for +you when you build Guacamole. + +1) Run mvn package + + $ mvn package + + Maven will download any needed dependencies for building the .zip file. + Once all dependencies have been downloaded, the .zip file will be + created in the target/ subdirectory of the current directory. + +2) Run mvn install + + $ mvn install + + DO NOT RUN THIS AS ROOT! + + Maven will install guacamole-common-js to your user's local Maven + repository where it can be used in future builds. It will not install + into a system-wide repository and does not require root privileges. + + Once installed, building other Maven projects that depend on + guacamole-common-js (such as Guacamole) will be possible. + + +------------------------------------------------------------ + Reporting problems +------------------------------------------------------------ + +Please report any bugs encountered by opening a new ticket at the Trac system +hosted at: + + http://guac-dev.org/trac/ + diff --git a/guacamole-common-js/doc/guacamole-osk.dtd b/guacamole-common-js/doc/guacamole-osk.dtd new file mode 100644 index 000000000..e3a468980 --- /dev/null +++ b/guacamole-common-js/doc/guacamole-osk.dtd @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml new file mode 100644 index 000000000..d79ba6436 --- /dev/null +++ b/guacamole-common-js/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + net.sourceforge.guacamole + guacamole-common-js + pom + 0.7.2 + guacamole-common-js + http://guac-dev.org/ + + + UTF-8 + + + + + + + maven-assembly-plugin + + + static.xml + + + + + make-zip + package + + attached + + + + + + + + + diff --git a/guacamole-common-js/src/main/resources/audio.js b/guacamole-common-js/src/main/resources/audio.js new file mode 100644 index 000000000..8a3243cdb --- /dev/null +++ b/guacamole-common-js/src/main/resources/audio.js @@ -0,0 +1,228 @@ + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is guacamole-common-js. + * + * The Initial Developer of the Original Code is + * Michael Jumper. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Namespace for all Guacamole JavaScript objects. + * @namespace + */ +var Guacamole = Guacamole || {}; + +/** + * Abstract audio channel which queues and plays arbitrary audio data. + * @constructor + */ +Guacamole.AudioChannel = function() { + + /** + * Reference to this AudioChannel. + * @private + */ + var channel = this; + + /** + * When the next packet should play. + * @private + */ + var next_packet_time = 0; + + /** + * Queues up the given data for playing by this channel once all previously + * queued data has been played. If no data has been queued, the data will + * play immediately. + * + * @param {String} mimetype The mimetype of the data provided. + * @param {Number} duration The duration of the data provided, in + * milliseconds. + * @param {String} data The base64-encoded data to play. + */ + this.play = function(mimetype, duration, data) { + + var packet = + new Guacamole.AudioChannel.Packet(mimetype, data); + + var now = Guacamole.AudioChannel.getTimestamp(); + + // If underflow is detected, reschedule new packets relative to now. + if (next_packet_time < now) + next_packet_time = now; + + // Schedule next packet + packet.play(next_packet_time); + next_packet_time += duration; + + }; + +}; + +// Define context if available +if (window.webkitAudioContext) { + Guacamole.AudioChannel.context = new webkitAudioContext(); +} + +/** + * Returns a base timestamp which can be used for scheduling future audio + * playback. Scheduling playback for the value returned by this function plus + * N will cause the associated audio to be played back N milliseconds after + * the function is called. + * + * @return {Number} An arbitrary channel-relative timestamp, in milliseconds. + */ +Guacamole.AudioChannel.getTimestamp = function() { + + // If we have an audio context, use its timestamp + if (Guacamole.AudioChannel.context) + return Guacamole.AudioChannel.context.currentTime * 1000; + + // If we have high-resolution timers, use those + if (window.performance) { + + if (window.performance.now) + return window.performance.now(); + + if (window.performance.webkitNow) + return window.performance.webkitNow(); + + } + + // Fallback to millisecond-resolution system time + return new Date().getTime(); + +}; + +/** + * Abstract representation of an audio packet. + * + * @constructor + * + * @param {String} mimetype The mimetype of the data contained by this packet. + * @param {String} data The base64-encoded sound data contained by this packet. + */ +Guacamole.AudioChannel.Packet = function(mimetype, data) { + + /** + * Schedules this packet for playback at the given time. + * + * @function + * @param {Number} when The time this packet should be played, in + * milliseconds. + */ + this.play = undefined; // Defined conditionally depending on support + + // If audio API available, use it. + if (Guacamole.AudioChannel.context) { + + var readyBuffer = null; + + // By default, when decoding finishes, store buffer for future + // playback + var handleReady = function(buffer) { + readyBuffer = buffer; + }; + + // Convert to ArrayBuffer + var binary = window.atob(data); + var arrayBuffer = new ArrayBuffer(binary.length); + var bufferView = new Uint8Array(arrayBuffer); + + for (var i=0; i 4096 && element_end >= start_index) { + + buffer = buffer.substring(start_index); + + // Reset parse relative to truncation + element_end -= start_index; + start_index = 0; + + } + + // Append data to buffer + buffer += packet; + + // While search is within currently received data + while (element_end < buffer.length) { + + // If we are waiting for element data + if (element_end >= start_index) { + + // We now have enough data for the element. Parse. + var element = buffer.substring(start_index, element_end); + var terminator = buffer.substring(element_end, element_end+1); + + // Add element to array + element_buffer.push(element); + + // If last element, handle instruction + if (terminator == ";") { + + // Get opcode + var opcode = element_buffer.shift(); + + // Call instruction handler. + if (parser.oninstruction != null) + parser.oninstruction(opcode, element_buffer); + + // Clear elements + element_buffer.length = 0; + + } + else if (terminator != ',') + throw new Error("Illegal terminator."); + + // Start searching for length at character after + // element terminator + start_index = element_end + 1; + + } + + // Search for end of length + var length_end = buffer.indexOf(".", start_index); + if (length_end != -1) { + + // Parse length + var length = parseInt(buffer.substring(element_end+1, length_end)); + if (length == NaN) + throw new Error("Non-numeric character in element length."); + + // Calculate start of element + start_index = length_end + 1; + + // Calculate location of element terminator + element_end = start_index + length; + + } + + // If no period yet, continue search when more data + // is received + else { + start_index = buffer.length; + break; + } + + } // end parse loop + + }; + + /** + * Fired once for every complete Guacamole instruction received, in order. + * + * @event + * @param {String} opcode The Guacamole instruction opcode. + * @param {Array} parameters The parameters provided for the instruction, + * if any. + */ + this.oninstruction = null; + +}; + + +/** + * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel}, + * automatically handles incoming and outgoing Guacamole instructions via the + * provided tunnel, updating the display using one or more canvas elements. + * + * @constructor + * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive + * Guacamole instructions. + */ +Guacamole.Client = function(tunnel) { + + var guac_client = this; + + var STATE_IDLE = 0; + var STATE_CONNECTING = 1; + var STATE_WAITING = 2; + var STATE_CONNECTED = 3; + var STATE_DISCONNECTING = 4; + var STATE_DISCONNECTED = 5; + + var currentState = STATE_IDLE; + + var currentTimestamp = 0; + var pingInterval = null; + + var displayWidth = 0; + var displayHeight = 0; + var displayScale = 1; + + /** + * Translation from Guacamole protocol line caps to Layer line caps. + * @private + */ + var lineCap = { + 0: "butt", + 1: "round", + 2: "square" + }; + + /** + * Translation from Guacamole protocol line caps to Layer line caps. + * @private + */ + var lineJoin = { + 0: "bevel", + 1: "miter", + 2: "round" + }; + + // Create bounding div + var bounds = document.createElement("div"); + bounds.style.position = "relative"; + bounds.style.width = (displayWidth*displayScale) + "px"; + bounds.style.height = (displayHeight*displayScale) + "px"; + + // Create display + var display = document.createElement("div"); + display.style.position = "relative"; + display.style.width = displayWidth + "px"; + display.style.height = displayHeight + "px"; + + // Ensure transformations on display originate at 0,0 + display.style.transformOrigin = + display.style.webkitTransformOrigin = + display.style.MozTransformOrigin = + display.style.OTransformOrigin = + display.style.msTransformOrigin = + "0 0"; + + // Create default layer + var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight); + + // Position default layer + var default_layer_container_element = default_layer_container.getElement(); + default_layer_container_element.style.position = "absolute"; + default_layer_container_element.style.left = "0px"; + default_layer_container_element.style.top = "0px"; + default_layer_container_element.style.overflow = "hidden"; + + // Create cursor layer + var cursor = new Guacamole.Client.LayerContainer(0, 0); + cursor.getLayer().setChannelMask(Guacamole.Layer.SRC); + + // Position cursor layer + var cursor_element = cursor.getElement(); + cursor_element.style.position = "absolute"; + cursor_element.style.left = "0px"; + cursor_element.style.top = "0px"; + + // Add default layer and cursor to display + display.appendChild(default_layer_container.getElement()); + display.appendChild(cursor.getElement()); + + // Add display to bounds + bounds.appendChild(display); + + // Initially, only default layer exists + var layers = [default_layer_container]; + + // No initial buffers + var buffers = []; + + // No initial parsers + var parsers = []; + + // No initial audio channels + var audio_channels = []; + + tunnel.onerror = function(message) { + if (guac_client.onerror) + guac_client.onerror(message); + }; + + function setState(state) { + if (state != currentState) { + currentState = state; + if (guac_client.onstatechange) + guac_client.onstatechange(currentState); + } + } + + function isConnected() { + return currentState == STATE_CONNECTED + || currentState == STATE_WAITING; + } + + var cursorHotspotX = 0; + var cursorHotspotY = 0; + + var cursorX = 0; + var cursorY = 0; + + function moveCursor(x, y) { + + // Move cursor layer + cursor.translate(x - cursorHotspotX, y - cursorHotspotY); + + // Update stored position + cursorX = x; + cursorY = y; + + } + + /** + * Returns an element containing the display of this Guacamole.Client. + * Adding the element returned by this function to an element in the body + * of a document will cause the client's display to be visible. + * + * @return {Element} An element containing ths display of this + * Guacamole.Client. + */ + this.getDisplay = function() { + return bounds; + }; + + /** + * Sends the current size of the screen. + * + * @param {Number} width The width of the screen. + * @param {Number} height The height of the screen. + */ + this.sendSize = function(width, height) { + + // Do not send requests if not connected + if (!isConnected()) + return; + + tunnel.sendMessage("size", width, height); + + }; + + /** + * Sends a key event having the given properties as if the user + * pressed or released a key. + * + * @param {Boolean} pressed Whether the key is pressed (true) or released + * (false). + * @param {Number} keysym The keysym of the key being pressed or released. + */ + this.sendKeyEvent = function(pressed, keysym) { + // Do not send requests if not connected + if (!isConnected()) + return; + + tunnel.sendMessage("key", keysym, pressed); + }; + + /** + * Sends a mouse event having the properties provided by the given mouse + * state. + * + * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send + * in the mouse event. + */ + this.sendMouseState = function(mouseState) { + + // Do not send requests if not connected + if (!isConnected()) + return; + + // Update client-side cursor + moveCursor( + Math.floor(mouseState.x), + Math.floor(mouseState.y) + ); + + // Build mask + var buttonMask = 0; + if (mouseState.left) buttonMask |= 1; + if (mouseState.middle) buttonMask |= 2; + if (mouseState.right) buttonMask |= 4; + if (mouseState.up) buttonMask |= 8; + if (mouseState.down) buttonMask |= 16; + + // Send message + tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask); + }; + + /** + * Sets the clipboard of the remote client to the given text data. + * + * @param {String} data The data to send as the clipboard contents. + */ + this.setClipboard = function(data) { + + // Do not send requests if not connected + if (!isConnected()) + return; + + tunnel.sendMessage("clipboard", data); + }; + + /** + * Fired whenever the state of this Guacamole.Client changes. + * + * @event + * @param {Number} state The new state of the client. + */ + this.onstatechange = null; + + /** + * Fired when the remote client sends a name update. + * + * @event + * @param {String} name The new name of this client. + */ + this.onname = null; + + /** + * Fired when an error is reported by the remote client, and the connection + * is being closed. + * + * @event + * @param {String} error A human-readable description of the error. + */ + this.onerror = null; + + /** + * Fired when the clipboard of the remote client is changing. + * + * @event + * @param {String} data The new text data of the remote clipboard. + */ + this.onclipboard = null; + + /** + * Fired when the default layer (and thus the entire Guacamole display) + * is resized. + * + * @event + * @param {Number} width The new width of the Guacamole display. + * @param {Number} height The new height of the Guacamole display. + */ + this.onresize = null; + + /** + * Fired when a file is received. Note that this will contain the entire + * data of the file. + * + * @event + * @param {String} name A human-readable name describing the file. + * @param {String} mimetype The mimetype of the file received. + * @param {String} data The actual entire contents of the file, + * base64-encoded. + */ + this.onfile = null; + + // Layers + function getBufferLayer(index) { + + index = -1 - index; + var buffer = buffers[index]; + + // Create buffer if necessary + if (buffer == null) { + buffer = new Guacamole.Layer(0, 0); + buffer.autosize = 1; + buffers[index] = buffer; + } + + return buffer; + + } + + function getLayerContainer(index) { + + var layer = layers[index]; + if (layer == null) { + + // Add new layer + layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight); + layers[index] = layer; + + // Get and position layer + var layer_element = layer.getElement(); + layer_element.style.position = "absolute"; + layer_element.style.left = "0px"; + layer_element.style.top = "0px"; + layer_element.style.overflow = "hidden"; + + // Add to default layer container + default_layer_container.getElement().appendChild(layer_element); + + } + + return layer; + + } + + function getLayer(index) { + + // If buffer, just get layer + if (index < 0) + return getBufferLayer(index); + + // Otherwise, retrieve layer from layer container + return getLayerContainer(index).getLayer(); + + } + + function getParser(index) { + + var parser = parsers[index]; + + // If parser not yet created, create it, and tie to the + // oninstruction handler of the tunnel. + if (parser == null) { + parser = parsers[index] = new Guacamole.Parser(); + parser.oninstruction = tunnel.oninstruction; + } + + return parser; + + } + + function getAudioChannel(index) { + + var audio_channel = audio_channels[index]; + + // If audio channel not yet created, create it + if (audio_channel == null) + audio_channel = audio_channels[index] = new Guacamole.AudioChannel(); + + return audio_channel; + + } + + /** + * Handlers for all defined layer properties. + * @private + */ + var layerPropertyHandlers = { + + "miter-limit": function(layer, value) { + layer.setMiterLimit(parseFloat(value)); + } + + }; + + /** + * Handlers for all instruction opcodes receivable by a Guacamole protocol + * client. + * @private + */ + var instructionHandlers = { + + "arc": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var x = parseInt(parameters[1]); + var y = parseInt(parameters[2]); + var radius = parseInt(parameters[3]); + var startAngle = parseFloat(parameters[4]); + var endAngle = parseFloat(parameters[5]); + var negative = parseInt(parameters[6]); + + layer.arc(x, y, radius, startAngle, endAngle, negative != 0); + + }, + + "audio": function(parameters) { + + var channel = getAudioChannel(parseInt(parameters[0])); + var mimetype = parameters[1]; + var duration = parseFloat(parameters[2]); + var data = parameters[3]; + + channel.play(mimetype, duration, data); + + }, + + "cfill": function(parameters) { + + var channelMask = parseInt(parameters[0]); + var layer = getLayer(parseInt(parameters[1])); + var r = parseInt(parameters[2]); + var g = parseInt(parameters[3]); + var b = parseInt(parameters[4]); + var a = parseInt(parameters[5]); + + layer.setChannelMask(channelMask); + + layer.fillColor(r, g, b, a); + + }, + + "clip": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + + layer.clip(); + + }, + + "clipboard": function(parameters) { + if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]); + }, + + "close": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + + layer.close(); + + }, + + "copy": function(parameters) { + + var srcL = getLayer(parseInt(parameters[0])); + var srcX = parseInt(parameters[1]); + var srcY = parseInt(parameters[2]); + var srcWidth = parseInt(parameters[3]); + var srcHeight = parseInt(parameters[4]); + var channelMask = parseInt(parameters[5]); + var dstL = getLayer(parseInt(parameters[6])); + var dstX = parseInt(parameters[7]); + var dstY = parseInt(parameters[8]); + + dstL.setChannelMask(channelMask); + + dstL.copy( + srcL, + srcX, + srcY, + srcWidth, + srcHeight, + dstX, + dstY + ); + + }, + + "cstroke": function(parameters) { + + var channelMask = parseInt(parameters[0]); + var layer = getLayer(parseInt(parameters[1])); + var cap = lineCap[parseInt(parameters[2])]; + var join = lineJoin[parseInt(parameters[3])]; + var thickness = parseInt(parameters[4]); + var r = parseInt(parameters[5]); + var g = parseInt(parameters[6]); + var b = parseInt(parameters[7]); + var a = parseInt(parameters[8]); + + layer.setChannelMask(channelMask); + + layer.strokeColor(cap, join, thickness, r, g, b, a); + + }, + + "cursor": function(parameters) { + + cursorHotspotX = parseInt(parameters[0]); + cursorHotspotY = parseInt(parameters[1]); + var srcL = getLayer(parseInt(parameters[2])); + var srcX = parseInt(parameters[3]); + var srcY = parseInt(parameters[4]); + var srcWidth = parseInt(parameters[5]); + var srcHeight = parseInt(parameters[6]); + + // Reset cursor size + cursor.resize(srcWidth, srcHeight); + + // Draw cursor to cursor layer + cursor.getLayer().copy( + srcL, + srcX, + srcY, + srcWidth, + srcHeight, + 0, + 0 + ); + + // Update cursor position (hotspot may have changed) + moveCursor(cursorX, cursorY); + + }, + + "curve": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var cp1x = parseInt(parameters[1]); + var cp1y = parseInt(parameters[2]); + var cp2x = parseInt(parameters[3]); + var cp2y = parseInt(parameters[4]); + var x = parseInt(parameters[5]); + var y = parseInt(parameters[6]); + + layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); + + }, + + "dispose": function(parameters) { + + var layer_index = parseInt(parameters[0]); + + // If visible layer, remove from parent + if (layer_index > 0) { + + // Get container element + var layer_container = getLayerContainer(layer_index).getElement(); + + // Remove from parent + layer_container.parentNode.removeChild(layer_container); + + // Delete reference + delete layers[layer_index]; + + } + + // If buffer, just delete reference + else if (layer_index < 0) + delete buffers[-1 - layer_index]; + + // Attempting to dispose the root layer currently has no effect. + + }, + + "distort": function(parameters) { + + var layer_index = parseInt(parameters[0]); + var a = parseFloat(parameters[1]); + var b = parseFloat(parameters[2]); + var c = parseFloat(parameters[3]); + var d = parseFloat(parameters[4]); + var e = parseFloat(parameters[5]); + var f = parseFloat(parameters[6]); + + // Only valid for visible layers (not buffers) + if (layer_index >= 0) { + + // Get container element + var layer_container = getLayerContainer(layer_index).getElement(); + + // Set layer transform + layer_container.transform(a, b, c, d, e, f); + + } + + }, + + "error": function(parameters) { + if (guac_client.onerror) guac_client.onerror(parameters[0]); + guac_client.disconnect(); + }, + + "file": function(parameters) { + if (guac_client.onfile) + guac_client.onfile( + parameters[0], // Name + parameters[1], // Mimetype + parameters[2] // Data + ); + }, + + "identity": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + + layer.setTransform(1, 0, 0, 1, 0, 0); + + }, + + "lfill": function(parameters) { + + var channelMask = parseInt(parameters[0]); + var layer = getLayer(parseInt(parameters[1])); + var srcLayer = getLayer(parseInt(parameters[2])); + + layer.setChannelMask(channelMask); + + layer.fillLayer(srcLayer); + + }, + + "line": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var x = parseInt(parameters[1]); + var y = parseInt(parameters[2]); + + layer.lineTo(x, y); + + }, + + "lstroke": function(parameters) { + + var channelMask = parseInt(parameters[0]); + var layer = getLayer(parseInt(parameters[1])); + var srcLayer = getLayer(parseInt(parameters[2])); + + layer.setChannelMask(channelMask); + + layer.strokeLayer(srcLayer); + + }, + + "move": function(parameters) { + + var layer_index = parseInt(parameters[0]); + var parent_index = parseInt(parameters[1]); + var x = parseInt(parameters[2]); + var y = parseInt(parameters[3]); + var z = parseInt(parameters[4]); + + // Only valid for non-default layers + if (layer_index > 0 && parent_index >= 0) { + + // Get container element + var layer_container = getLayerContainer(layer_index); + var layer_container_element = layer_container.getElement(); + var parent = getLayerContainer(parent_index).getElement(); + + // Set parent if necessary + if (!(layer_container_element.parentNode === parent)) + parent.appendChild(layer_container_element); + + // Move layer + layer_container.translate(x, y); + layer_container_element.style.zIndex = z; + + } + + }, + + "name": function(parameters) { + if (guac_client.onname) guac_client.onname(parameters[0]); + }, + + "nest": function(parameters) { + var parser = getParser(parseInt(parameters[0])); + parser.receive(parameters[1]); + }, + + "png": function(parameters) { + + var channelMask = parseInt(parameters[0]); + var layer = getLayer(parseInt(parameters[1])); + var x = parseInt(parameters[2]); + var y = parseInt(parameters[3]); + var data = parameters[4]; + + layer.setChannelMask(channelMask); + + layer.draw( + x, + y, + "data:image/png;base64," + data + ); + + }, + + "pop": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + + layer.pop(); + + }, + + "push": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + + layer.push(); + + }, + + "rect": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var x = parseInt(parameters[1]); + var y = parseInt(parameters[2]); + var w = parseInt(parameters[3]); + var h = parseInt(parameters[4]); + + layer.rect(x, y, w, h); + + }, + + "reset": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + + layer.reset(); + + }, + + "set": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var name = parameters[1]; + var value = parameters[2]; + + // Call property handler if defined + var handler = layerPropertyHandlers[name]; + if (handler) + handler(layer, value); + + }, + + "shade": function(parameters) { + + var layer_index = parseInt(parameters[0]); + var a = parseInt(parameters[1]); + + // Only valid for visible layers (not buffers) + if (layer_index >= 0) { + + // Get container element + var layer_container = getLayerContainer(layer_index).getElement(); + + // Set layer opacity + layer_container.style.opacity = a/255.0; + + } + + }, + + "size": function(parameters) { + + var layer_index = parseInt(parameters[0]); + var width = parseInt(parameters[1]); + var height = parseInt(parameters[2]); + + // If not buffer, resize layer and container + if (layer_index >= 0) { + + // Resize layer + var layer_container = getLayerContainer(layer_index); + layer_container.resize(width, height); + + // If layer is default, resize display + if (layer_index == 0) { + + displayWidth = width; + displayHeight = height; + + // Update (set) display size + display.style.width = displayWidth + "px"; + display.style.height = displayHeight + "px"; + + // Update bounds size + bounds.style.width = (displayWidth*displayScale) + "px"; + bounds.style.height = (displayHeight*displayScale) + "px"; + + // Call resize event handler if defined + if (guac_client.onresize) + guac_client.onresize(width, height); + + } + + } + + // If buffer, resize layer only + else { + var layer = getBufferLayer(parseInt(parameters[0])); + layer.resize(width, height); + } + + }, + + "start": function(parameters) { + + var layer = getLayer(parseInt(parameters[0])); + var x = parseInt(parameters[1]); + var y = parseInt(parameters[2]); + + layer.moveTo(x, y); + + }, + + "sync": function(parameters) { + + var timestamp = parameters[0]; + + // When all layers have finished rendering all instructions + // UP TO THIS POINT IN TIME, send sync response. + + var layersToSync = 0; + function syncLayer() { + + layersToSync--; + + // Send sync response when layers are finished + if (layersToSync == 0) { + if (timestamp != currentTimestamp) { + tunnel.sendMessage("sync", timestamp); + currentTimestamp = timestamp; + } + } + + } + + // Count active, not-ready layers and install sync tracking hooks + for (var i=0; i= 0) { + + var hex = keyIdentifier.substring(unicodePrefixLocation+2); + var codepoint = parseInt(hex, 16); + var typedCharacter; + + // Convert case if shifted + if (shifted == 0) + typedCharacter = String.fromCharCode(codepoint).toLowerCase(); + else + typedCharacter = String.fromCharCode(codepoint).toUpperCase(); + + // Get codepoint + codepoint = typedCharacter.charCodeAt(0); + + return keysym_from_charcode(codepoint); + + } + + return get_keysym(keyidentifier_keysym[keyIdentifier], location); + + } + + function isControlCharacter(codepoint) { + return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); + } + + function keysym_from_charcode(codepoint) { + + // Keysyms for control characters + if (isControlCharacter(codepoint)) 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; + + } + + function keysym_from_keycode(keyCode, location) { + + var keysyms; + + // If not shifted, just return unshifted keysym + if (!guac_keyboard.modifiers.shift) + keysyms = unshiftedKeysym[keyCode]; + + // Otherwise, return shifted keysym, if defined + else + keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode]; + + return get_keysym(keysyms, location); + + } + + + /** + * Marks a key as pressed, firing the keydown event if registered. Key + * repeat for the pressed key will start after a delay if that key is + * not a modifier. + * @private + */ + function press_key(keysym) { + + // Don't bother with pressing the key if the key is unknown + if (keysym == null) return; + + // Only press if released + if (!guac_keyboard.pressed[keysym]) { + + // Mark key as pressed + guac_keyboard.pressed[keysym] = true; + + // Send key event + if (guac_keyboard.onkeydown) { + guac_keyboard.onkeydown(keysym); + + // Stop any current repeat + window.clearTimeout(key_repeat_timeout); + window.clearInterval(key_repeat_interval); + + // Repeat after a delay as long as pressed + if (!no_repeat[keysym]) + key_repeat_timeout = window.setTimeout(function() { + key_repeat_interval = window.setInterval(function() { + guac_keyboard.onkeyup(keysym); + guac_keyboard.onkeydown(keysym); + }, 50); + }, 500); + + + } + } + + } + + /** + * Marks a key as released, firing the keyup event if registered. + * @private + */ + function release_key(keysym) { + + // Only release if pressed + if (guac_keyboard.pressed[keysym]) { + + // Mark key as released + delete guac_keyboard.pressed[keysym]; + + // Stop repeat + window.clearTimeout(key_repeat_timeout); + window.clearInterval(key_repeat_interval); + + // Send key event + if (keysym != null && guac_keyboard.onkeyup) + guac_keyboard.onkeyup(keysym); + + } + + } + + function isTypable(keyIdentifier) { + + // Find unicode prefix + var unicodePrefixLocation = keyIdentifier.indexOf("U+"); + if (unicodePrefixLocation == -1) + return false; + + // Parse codepoint value + var hex = keyIdentifier.substring(unicodePrefixLocation+2); + var codepoint = parseInt(hex, 16); + + // If control character, not typable + if (isControlCharacter(codepoint)) return false; + + // Otherwise, typable + return true; + + } + + /** + * Given a keyboard event, updates the local modifier state and remote + * key state based on the modifier flags within the event. This function + * pays no attention to keycodes. + * + * @param {KeyboardEvent} e The keyboard event containing the flags to update. + */ + function update_modifier_state(e) { + + // Release alt if implicitly released + if (guac_keyboard.modifiers.alt && e.altKey === false) { + release_key(0xFFE9); // Left alt + release_key(0xFFEA); // Right alt (or AltGr) + guac_keyboard.modifiers.alt = false; + } + + // Release shift if implicitly released + if (guac_keyboard.modifiers.shift && e.shiftKey === false) { + release_key(0xFFE1); // Left shift + release_key(0xFFE2); // Right shift + guac_keyboard.modifiers.shift = false; + } + + // Release ctrl if implicitly released + if (guac_keyboard.modifiers.ctrl && e.ctrlKey === false) { + release_key(0xFFE3); // Left ctrl + release_key(0xFFE4); // Right ctrl + guac_keyboard.modifiers.ctrl = false; + } + + } + + // When key pressed + element.addEventListener("keydown", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown) return; + + var keynum; + if (window.event) keynum = window.event.keyCode; + else if (e.which) keynum = e.which; + + // Get key location + var location = e.location || e.keyLocation || 0; + + // Ignore any unknown key events + if (keynum == 0 && !e.keyIdentifier) { + e.preventDefault(); + return; + } + + // Fix modifier states + update_modifier_state(e); + + // Ctrl/Alt/Shift/Meta + if (keynum == 16) guac_keyboard.modifiers.shift = true; + else if (keynum == 17) guac_keyboard.modifiers.ctrl = true; + else if (keynum == 18) guac_keyboard.modifiers.alt = true; + else if (keynum == 91) guac_keyboard.modifiers.meta = true; + + // Try to get keysym from keycode + var keysym = keysym_from_keycode(keynum, location); + + // By default, we expect a corresponding keypress event + var expect_keypress = true; + + // If key is known from keycode, prevent default + if (keysym) + expect_keypress = false; + + // Also try to get get keysym from keyIdentifier + if (e.keyIdentifier) { + + keysym = keysym || + keysym_from_key_identifier(guac_keyboard.modifiers.shift, + e.keyIdentifier, location); + + // Prevent default if non-typable character or if modifier combination + // likely to be eaten by browser otherwise (NOTE: We must not prevent + // default for Ctrl+Alt, as that combination is commonly used for + // AltGr. If we receive AltGr, we need to handle keypress, which + // means we cannot cancel keydown). + if (!isTypable(e.keyIdentifier) + || ( guac_keyboard.modifiers.ctrl && !guac_keyboard.modifiers.alt) + || (!guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) + || (guac_keyboard.modifiers.meta)) + expect_keypress = false; + + } + + // If we do not expect to handle via keypress, handle now + if (!expect_keypress) { + e.preventDefault(); + + // Press key if known + if (keysym != null) { + keydownChar[keynum] = keysym; + press_key(keysym); + + // If a key is pressed while meta is held down, the keyup will never be sent in Chrome, so send it now. (bug #108404) + if(guac_keyboard.modifiers.meta) { + release_key(keysym); + } + } + + } + + }, true); + + // When key pressed + element.addEventListener("keypress", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + e.preventDefault(); + + var keynum; + if (window.event) keynum = window.event.keyCode; + else if (e.which) keynum = e.which; + + var keysym = keysym_from_charcode(keynum); + + // Fix modifier states + update_modifier_state(e); + + // If event identified as a typable character, and we're holding Ctrl+Alt, + // assume Ctrl+Alt is actually AltGr, and release both. + if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) { + release_key(0xFFE3); // Left ctrl + release_key(0xFFE4); // Right ctrl + release_key(0xFFE9); // Left alt + release_key(0xFFEA); // Right alt + } + + // Send press + release if keysym known + if (keysym != null) { + press_key(keysym); + release_key(keysym); + } + + }, true); + + // When key released + element.addEventListener("keyup", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeyup) return; + + e.preventDefault(); + + var keynum; + if (window.event) keynum = window.event.keyCode; + else if (e.which) keynum = e.which; + + // Fix modifier states + update_modifier_state(e); + + // Ctrl/Alt/Shift/Meta + if (keynum == 16) guac_keyboard.modifiers.shift = false; + else if (keynum == 17) guac_keyboard.modifiers.ctrl = false; + else if (keynum == 18) guac_keyboard.modifiers.alt = false; + else if (keynum == 91) guac_keyboard.modifiers.meta = false; + + // Send release event if original key known + var keydown_keysym = keydownChar[keynum]; + if (keydown_keysym != null) + release_key(keydown_keysym); + + // Clear character record + keydownChar[keynum] = null; + + }, true); + +}; diff --git a/guacamole-common-js/src/main/resources/layer.js b/guacamole-common-js/src/main/resources/layer.js new file mode 100644 index 000000000..3b4343890 --- /dev/null +++ b/guacamole-common-js/src/main/resources/layer.js @@ -0,0 +1,1177 @@ + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is guacamole-common-js. + * + * The Initial Developer of the Original Code is + * Michael Jumper. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Namespace for all Guacamole JavaScript objects. + * @namespace + */ +var Guacamole = Guacamole || {}; + +/** + * Abstract ordered drawing surface. Each Layer contains a canvas element and + * provides simple drawing instructions for drawing to that canvas element, + * however unlike the canvas element itself, drawing operations on a Layer are + * guaranteed to run in order, even if such an operation must wait for an image + * to load before completing. + * + * @constructor + * + * @param {Number} width The width of the Layer, in pixels. The canvas element + * backing this Layer will be given this width. + * + * @param {Number} height The height of the Layer, in pixels. The canvas element + * backing this Layer will be given this height. + */ +Guacamole.Layer = function(width, height) { + + /** + * Reference to this Layer. + * @private + */ + var layer = this; + + /** + * The canvas element backing this Layer. + * @private + */ + var display = document.createElement("canvas"); + + /** + * The 2D display context of the canvas element backing this Layer. + * @private + */ + var displayContext = display.getContext("2d"); + displayContext.save(); + + /** + * The queue of all pending Tasks. Tasks will be run in order, with new + * tasks added at the end of the queue and old tasks removed from the + * front of the queue (FIFO). + * @private + */ + var tasks = new Array(); + + /** + * Whether a new path should be started with the next path drawing + * operations. + * @private + */ + var pathClosed = true; + + /** + * The number of states on the state stack. + * + * Note that there will ALWAYS be one element on the stack, but that + * element is not exposed. It is only used to reset the layer to its + * initial state. + * + * @private + */ + var stackSize = 0; + + /** + * Map of all Guacamole channel masks to HTML5 canvas composite operation + * names. Not all channel mask combinations are currently implemented. + * @private + */ + var compositeOperation = { + /* 0x0 NOT IMPLEMENTED */ + 0x1: "destination-in", + 0x2: "destination-out", + /* 0x3 NOT IMPLEMENTED */ + 0x4: "source-in", + /* 0x5 NOT IMPLEMENTED */ + 0x6: "source-atop", + /* 0x7 NOT IMPLEMENTED */ + 0x8: "source-out", + 0x9: "destination-atop", + 0xA: "xor", + 0xB: "destination-over", + 0xC: "copy", + /* 0xD NOT IMPLEMENTED */ + 0xE: "source-over", + 0xF: "lighter" + }; + + /** + * Resizes the canvas element backing this Layer without testing the + * new size. This function should only be used internally. + * + * @private + * @param {Number} newWidth The new width to assign to this Layer. + * @param {Number} newHeight The new height to assign to this Layer. + */ + function resize(newWidth, newHeight) { + + // Only preserve old data if width/height are both non-zero + var oldData = null; + if (width != 0 && height != 0) { + + // Create canvas and context for holding old data + oldData = document.createElement("canvas"); + oldData.width = width; + oldData.height = height; + + var oldDataContext = oldData.getContext("2d"); + + // Copy image data from current + oldDataContext.drawImage(display, + 0, 0, width, height, + 0, 0, width, height); + + } + + // Preserve composite operation + var oldCompositeOperation = displayContext.globalCompositeOperation; + + // Resize canvas + display.width = newWidth; + display.height = newHeight; + + // Redraw old data, if any + if (oldData) + displayContext.drawImage(oldData, + 0, 0, width, height, + 0, 0, width, height); + + // Restore composite operation + displayContext.globalCompositeOperation = oldCompositeOperation; + + width = newWidth; + height = newHeight; + + // Acknowledge reset of stack (happens on resize of canvas) + stackSize = 0; + displayContext.save(); + + } + + /** + * Given the X and Y coordinates of the upper-left corner of a rectangle + * and the rectangle's width and height, resize the backing canvas element + * as necessary to ensure that the rectangle fits within the canvas + * element's coordinate space. This function will only make the canvas + * larger. If the rectangle already fits within the canvas element's + * coordinate space, the canvas is left unchanged. + * + * @private + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to fit. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to fit. + * @param {Number} w The width of the the rectangle to fit. + * @param {Number} h The height of the the rectangle to fit. + */ + function fitRect(x, y, w, h) { + + // Calculate bounds + var opBoundX = w + x; + var opBoundY = h + y; + + // Determine max width + var resizeWidth; + if (opBoundX > width) + resizeWidth = opBoundX; + else + resizeWidth = width; + + // Determine max height + var resizeHeight; + if (opBoundY > height) + resizeHeight = opBoundY; + else + resizeHeight = height; + + // Resize if necessary + if (resizeWidth != width || resizeHeight != height) + resize(resizeWidth, resizeHeight); + + } + + /** + * A container for an task handler. Each operation which must be ordered + * is associated with a Task that goes into a task queue. Tasks in this + * queue are executed in order once their handlers are set, while Tasks + * without handlers block themselves and any following Tasks from running. + * + * @constructor + * @private + * @param {function} taskHandler The function to call when this task + * runs, if any. + * @param {boolean} blocked Whether this task should start blocked. + */ + function Task(taskHandler, blocked) { + + var task = this; + + /** + * Whether this Task is blocked. + * + * @type boolean + */ + this.blocked = blocked; + + /** + * The handler this Task is associated with, if any. + * + * @type function + */ + this.handler = taskHandler; + + /** + * Unblocks this Task, allowing it to run. + */ + this.unblock = function() { + if (task.blocked) { + task.blocked = false; + handlePendingTasks(); + } + } + + } + + /** + * If no tasks are pending or running, run the provided handler immediately, + * if any. Otherwise, schedule a task to run immediately after all currently + * running or pending tasks are complete. + * + * @private + * @param {function} handler The function to call when possible, if any. + * @param {boolean} blocked Whether the task should start blocked. + * @returns {Task} The Task created and added to the queue for future + * running, if any, or null if the handler was run + * immediately and no Task needed to be created. + */ + function scheduleTask(handler, blocked) { + + // If no pending tasks, just call (if available) and exit + if (layer.isReady() && !blocked) { + if (handler) handler(); + return null; + } + + // If tasks are pending/executing, schedule a pending task + // and return a reference to it. + var task = new Task(handler, blocked); + tasks.push(task); + return task; + + } + + var tasksInProgress = false; + + /** + * Run any Tasks which were pending but are now ready to run and are not + * blocked by other Tasks. + * @private + */ + function handlePendingTasks() { + + if (tasksInProgress) + return; + + tasksInProgress = true; + + // Draw all pending tasks. + var task; + while ((task = tasks[0]) != null && !task.blocked) { + tasks.shift(); + if (task.handler) task.handler(); + } + + tasksInProgress = false; + + } + + /** + * Schedules a task within the current layer just as scheduleTast() does, + * except that another specified layer will be blocked until this task + * completes, and this task will not start until the other layer is + * ready. + * + * Essentially, a task is scheduled in both layers, and the specified task + * will only be performed once both layers are ready, and neither layer may + * proceed until this task completes. + * + * Note that there is no way to specify whether the task starts blocked, + * as whether the task is blocked depends completely on whether the + * other layer is currently ready. + * + * @private + * @param {Guacamole.Layer} otherLayer The other layer which must be blocked + * until this task completes. + * @param {function} handler The function to call when possible. + */ + function scheduleTaskSynced(otherLayer, handler) { + + // If we ARE the other layer, no need to sync. + // Syncing would result in deadlock. + if (layer === otherLayer) + scheduleTask(handler); + + // Otherwise synchronize operation with other layer + else { + + var drawComplete = false; + var layerLock = null; + + function performTask() { + + // Perform task + handler(); + + // Unblock the other layer now that draw is complete + if (layerLock != null) + layerLock.unblock(); + + // Flag operation as done + drawComplete = true; + + } + + // Currently blocked draw task + var task = scheduleTask(performTask, true); + + // Unblock draw task once source layer is ready + otherLayer.sync(task.unblock); + + // Block other layer until draw completes + // Note that the draw MAY have already been performed at this point, + // in which case creating a lock on the other layer will lead to + // deadlock (the draw task has already run and will thus never + // clear the lock) + if (!drawComplete) + layerLock = otherLayer.sync(null, true); + + } + } + + /** + * Set to true if this Layer should resize itself to accomodate the + * dimensions of any drawing operation, and false (the default) otherwise. + * + * Note that setting this property takes effect immediately, and thus may + * take effect on operations that were started in the past but have not + * yet completed. If you wish the setting of this flag to only modify + * future operations, you will need to make the setting of this flag an + * operation with sync(). + * + * @example + * // Set autosize to true for all future operations + * layer.sync(function() { + * layer.autosize = true; + * }); + * + * @type Boolean + * @default false + */ + this.autosize = false; + + /** + * Returns the canvas element backing this Layer. + * @returns {Element} The canvas element backing this Layer. + */ + this.getCanvas = function() { + return display; + }; + + /** + * Returns whether this Layer is ready. A Layer is ready if it has no + * pending operations and no operations in-progress. + * + * @returns {Boolean} true if this Layer is ready, false otherwise. + */ + this.isReady = function() { + return tasks.length == 0; + }; + + /** + * Changes the size of this Layer to the given width and height. Resizing + * is only attempted if the new size provided is actually different from + * the current size. + * + * @param {Number} newWidth The new width to assign to this Layer. + * @param {Number} newHeight The new height to assign to this Layer. + */ + this.resize = function(newWidth, newHeight) { + scheduleTask(function() { + if (newWidth != width || newHeight != height) + resize(newWidth, newHeight); + }); + }; + + /** + * Draws the specified image at the given coordinates. The image specified + * must already be loaded. + * + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {Image} image The image to draw. Note that this is an Image + * object - not a URL. + */ + this.drawImage = function(x, y, image) { + scheduleTask(function() { + if (layer.autosize != 0) fitRect(x, y, image.width, image.height); + displayContext.drawImage(image, x, y); + }); + }; + + /** + * Draws the image at the specified URL at the given coordinates. The image + * will be loaded automatically, and this and any future operations will + * wait for the image to finish loading. + * + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {String} url The URL of the image to draw. + */ + this.draw = function(x, y, url) { + + var task = scheduleTask(function() { + if (layer.autosize != 0) fitRect(x, y, image.width, image.height); + displayContext.drawImage(image, x, y); + }, true); + + var image = new Image(); + image.onload = task.unblock; + image.src = url; + + }; + + /** + * Plays the video at the specified URL within this layer. The video + * will be loaded automatically, and this and any future operations will + * wait for the video to finish loading. Future operations will not be + * executed until the video finishes playing. + * + * @param {String} mimetype The mimetype of the video to play. + * @param {Number} duration The duration of the video in milliseconds. + * @param {String} url The URL of the video to play. + */ + this.play = function(mimetype, duration, url) { + + // Start loading the video + var video = document.createElement("video"); + video.type = mimetype; + video.src = url; + + // Main task - playing the video + var task = scheduleTask(function() { + video.play(); + }, true); + + // Lock which will be cleared after video ends + var lock = scheduleTask(null, true); + + // Start copying frames when playing + video.addEventListener("play", function() { + + function render_callback() { + displayContext.drawImage(video, 0, 0, width, height); + if (!video.ended) + window.setTimeout(render_callback, 20); + else + lock.unblock(); + } + + render_callback(); + + }, false); + + // Unblock future operations after an error + video.addEventListener("error", lock.unblock, false); + + // Play video as soon as current tasks are complete, now that the + // lock has been set up. + task.unblock(); + + }; + + /** + * Run an arbitrary function as soon as currently pending operations + * are complete. + * + * @param {function} handler The function to call once all currently + * pending operations are complete. + * @param {boolean} blocked Whether the task should start blocked. + */ + this.sync = scheduleTask; + + /** + * Transfer a rectangle of image data from one Layer to this Layer using the + * specified transfer function. + * + * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. + * @param {Number} srcx The X coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcy The Y coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcw The width of the rectangle within the source Layer's + * coordinate space to copy data from. + * @param {Number} srch The height of the rectangle within the source + * Layer's coordinate space to copy data from. + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {Function} transferFunction The transfer function to use to + * transfer data from source to + * destination. + */ + this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) { + scheduleTaskSynced(srcLayer, function() { + + var srcCanvas = srcLayer.getCanvas(); + + // If entire rectangle outside source canvas, stop + if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; + + // Otherwise, clip rectangle to area + if (srcx + srcw > srcCanvas.width) + srcw = srcCanvas.width - srcx; + + if (srcy + srch > srcCanvas.height) + srch = srcCanvas.height - srcy; + + // Stop if nothing to draw. + if (srcw == 0 || srch == 0) return; + + if (layer.autosize != 0) fitRect(x, y, srcw, srch); + + // Get image data from src and dst + var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); + var dst = displayContext.getImageData(x , y, srcw, srch); + + // Apply transfer for each pixel + for (var i=0; i= srcCanvas.width || srcy >= srcCanvas.height) return; + + // Otherwise, clip rectangle to area + if (srcx + srcw > srcCanvas.width) + srcw = srcCanvas.width - srcx; + + if (srcy + srch > srcCanvas.height) + srch = srcCanvas.height - srcy; + + // Stop if nothing to draw. + if (srcw == 0 || srch == 0) return; + + if (layer.autosize != 0) fitRect(x, y, srcw, srch); + displayContext.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch); + + }); + }; + + /** + * Starts a new path at the specified point. + * + * @param {Number} x The X coordinate of the point to draw. + * @param {Number} y The Y coordinate of the point to draw. + */ + this.moveTo = function(x, y) { + scheduleTask(function() { + + // Start a new path if current path is closed + if (pathClosed) { + displayContext.beginPath(); + pathClosed = false; + } + + if (layer.autosize != 0) fitRect(x, y, 0, 0); + displayContext.moveTo(x, y); + + }); + }; + + /** + * Add the specified line to the current path. + * + * @param {Number} x The X coordinate of the endpoint of the line to draw. + * @param {Number} y The Y coordinate of the endpoint of the line to draw. + */ + this.lineTo = function(x, y) { + scheduleTask(function() { + + // Start a new path if current path is closed + if (pathClosed) { + displayContext.beginPath(); + pathClosed = false; + } + + if (layer.autosize != 0) fitRect(x, y, 0, 0); + displayContext.lineTo(x, y); + + }); + }; + + /** + * Add the specified arc to the current path. + * + * @param {Number} x The X coordinate of the center of the circle which + * will contain the arc. + * @param {Number} y The Y coordinate of the center of the circle which + * will contain the arc. + * @param {Number} radius The radius of the circle. + * @param {Number} startAngle The starting angle of the arc, in radians. + * @param {Number} endAngle The ending angle of the arc, in radians. + * @param {Boolean} negative Whether the arc should be drawn in order of + * decreasing angle. + */ + this.arc = function(x, y, radius, startAngle, endAngle, negative) { + scheduleTask(function() { + + // Start a new path if current path is closed + if (pathClosed) { + displayContext.beginPath(); + pathClosed = false; + } + + if (layer.autosize != 0) fitRect(x, y, 0, 0); + displayContext.arc(x, y, radius, startAngle, endAngle, negative); + + }); + }; + + /** + * Starts a new path at the specified point. + * + * @param {Number} cp1x The X coordinate of the first control point. + * @param {Number} cp1y The Y coordinate of the first control point. + * @param {Number} cp2x The X coordinate of the second control point. + * @param {Number} cp2y The Y coordinate of the second control point. + * @param {Number} x The X coordinate of the endpoint of the curve. + * @param {Number} y The Y coordinate of the endpoint of the curve. + */ + this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { + scheduleTask(function() { + + // Start a new path if current path is closed + if (pathClosed) { + displayContext.beginPath(); + pathClosed = false; + } + + if (layer.autosize != 0) fitRect(x, y, 0, 0); + displayContext.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); + + }); + }; + + /** + * Closes the current path by connecting the end point with the start + * point (if any) with a straight line. + */ + this.close = function() { + scheduleTask(function() { + + // Close path + displayContext.closePath(); + pathClosed = true; + + }); + }; + + /** + * Add the specified rectangle to the current path. + * + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to draw. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to draw. + * @param {Number} w The width of the rectangle to draw. + * @param {Number} h The height of the rectangle to draw. + */ + this.rect = function(x, y, w, h) { + scheduleTask(function() { + + // Start a new path if current path is closed + if (pathClosed) { + displayContext.beginPath(); + pathClosed = false; + } + + if (layer.autosize != 0) fitRect(x, y, w, h); + displayContext.rect(x, y, w, h); + + }); + }; + + /** + * Clip all future drawing operations by the current path. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as fillColor()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + */ + this.clip = function() { + scheduleTask(function() { + + // Set new clipping region + displayContext.clip(); + + // Path now implicitly closed + pathClosed = true; + + }); + }; + + /** + * Stroke the current path with the specified color. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {String} cap The line cap style. Can be "round", "square", + * or "butt". + * @param {String} join The line join style. Can be "round", "bevel", + * or "miter". + * @param {Number} thickness The line thickness in pixels. + * @param {Number} r The red component of the color to fill. + * @param {Number} g The green component of the color to fill. + * @param {Number} b The blue component of the color to fill. + * @param {Number} a The alpha component of the color to fill. + */ + this.strokeColor = function(cap, join, thickness, r, g, b, a) { + scheduleTask(function() { + + // Stroke with color + displayContext.lineCap = cap; + displayContext.lineJoin = join; + displayContext.lineWidth = thickness; + displayContext.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; + displayContext.stroke(); + + // Path now implicitly closed + pathClosed = true; + + }); + }; + + /** + * Fills the current path with the specified color. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Number} r The red component of the color to fill. + * @param {Number} g The green component of the color to fill. + * @param {Number} b The blue component of the color to fill. + * @param {Number} a The alpha component of the color to fill. + */ + this.fillColor = function(r, g, b, a) { + scheduleTask(function() { + + // Fill with color + displayContext.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; + displayContext.fill(); + + // Path now implicitly closed + pathClosed = true; + + }); + }; + + /** + * Stroke the current path with the image within the specified layer. The + * image data will be tiled infinitely within the stroke. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {String} cap The line cap style. Can be "round", "square", + * or "butt". + * @param {String} join The line join style. Can be "round", "bevel", + * or "miter". + * @param {Number} thickness The line thickness in pixels. + * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern + * within the stroke. + */ + this.strokeLayer = function(cap, join, thickness, srcLayer) { + scheduleTaskSynced(srcLayer, function() { + + // Stroke with image data + displayContext.lineCap = cap; + displayContext.lineJoin = join; + displayContext.lineWidth = thickness; + displayContext.strokeStyle = displayContext.createPattern( + srcLayer.getCanvas(), + "repeat" + ); + displayContext.stroke(); + + // Path now implicitly closed + pathClosed = true; + + }); + }; + + /** + * Fills the current path with the image within the specified layer. The + * image data will be tiled infinitely within the stroke. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern + * within the fill. + */ + this.fillLayer = function(srcLayer) { + scheduleTask(function() { + + // Fill with image data + displayContext.fillStyle = displayContext.createPattern( + srcLayer.getCanvas(), + "repeat" + ); + displayContext.fill(); + + // Path now implicitly closed + pathClosed = true; + + }); + }; + + /** + * Push current layer state onto stack. + */ + this.push = function() { + scheduleTask(function() { + + // Save current state onto stack + displayContext.save(); + stackSize++; + + }); + }; + + /** + * Pop layer state off stack. + */ + this.pop = function() { + scheduleTask(function() { + + // Restore current state from stack + if (stackSize > 0) { + displayContext.restore(); + stackSize--; + } + + }); + }; + + /** + * Reset the layer, clearing the stack, the current path, and any transform + * matrix. + */ + this.reset = function() { + scheduleTask(function() { + + // Clear stack + while (stackSize > 0) { + displayContext.restore(); + stackSize--; + } + + // Restore to initial state + displayContext.restore(); + displayContext.save(); + + // Clear path + displayContext.beginPath(); + pathClosed = false; + + }); + }; + + /** + * Sets the given affine transform (defined with six values from the + * transform's matrix). + * + * @param {Number} a The first value in the affine transform's matrix. + * @param {Number} b The second value in the affine transform's matrix. + * @param {Number} c The third value in the affine transform's matrix. + * @param {Number} d The fourth value in the affine transform's matrix. + * @param {Number} e The fifth value in the affine transform's matrix. + * @param {Number} f The sixth value in the affine transform's matrix. + */ + this.setTransform = function(a, b, c, d, e, f) { + scheduleTask(function() { + + // Set transform + displayContext.setTransform( + a, b, c, + d, e, f + /*0, 0, 1*/ + ); + + }); + }; + + + /** + * Applies the given affine transform (defined with six values from the + * transform's matrix). + * + * @param {Number} a The first value in the affine transform's matrix. + * @param {Number} b The second value in the affine transform's matrix. + * @param {Number} c The third value in the affine transform's matrix. + * @param {Number} d The fourth value in the affine transform's matrix. + * @param {Number} e The fifth value in the affine transform's matrix. + * @param {Number} f The sixth value in the affine transform's matrix. + */ + this.transform = function(a, b, c, d, e, f) { + scheduleTask(function() { + + // Apply transform + displayContext.transform( + a, b, c, + d, e, f + /*0, 0, 1*/ + ); + + }); + }; + + + /** + * Sets the channel mask for future operations on this Layer. + * + * The channel mask is a Guacamole-specific compositing operation identifier + * with a single bit representing each of four channels (in order): source + * image where destination transparent, source where destination opaque, + * destination where source transparent, and destination where source + * opaque. + * + * @param {Number} mask The channel mask for future operations on this + * Layer. + */ + this.setChannelMask = function(mask) { + scheduleTask(function() { + displayContext.globalCompositeOperation = compositeOperation[mask]; + }); + }; + + /** + * Sets the miter limit for stroke operations using the miter join. This + * limit is the maximum ratio of the size of the miter join to the stroke + * width. If this ratio is exceeded, the miter will not be drawn for that + * joint of the path. + * + * @param {Number} limit The miter limit for stroke operations using the + * miter join. + */ + this.setMiterLimit = function(limit) { + scheduleTask(function() { + displayContext.miterLimit = limit; + }); + }; + + // Initialize canvas dimensions + display.width = width; + display.height = height; + +}; + +/** + * Channel mask for the composite operation "rout". + */ +Guacamole.Layer.ROUT = 0x2; + +/** + * Channel mask for the composite operation "atop". + */ +Guacamole.Layer.ATOP = 0x6; + +/** + * Channel mask for the composite operation "xor". + */ +Guacamole.Layer.XOR = 0xA; + +/** + * Channel mask for the composite operation "rover". + */ +Guacamole.Layer.ROVER = 0xB; + +/** + * Channel mask for the composite operation "over". + */ +Guacamole.Layer.OVER = 0xE; + +/** + * Channel mask for the composite operation "plus". + */ +Guacamole.Layer.PLUS = 0xF; + +/** + * Channel mask for the composite operation "rin". + * Beware that WebKit-based browsers may leave the contents of the destionation + * layer where the source layer is transparent, despite the definition of this + * operation. + */ +Guacamole.Layer.RIN = 0x1; + +/** + * Channel mask for the composite operation "in". + * Beware that WebKit-based browsers may leave the contents of the destionation + * layer where the source layer is transparent, despite the definition of this + * operation. + */ +Guacamole.Layer.IN = 0x4; + +/** + * Channel mask for the composite operation "out". + * Beware that WebKit-based browsers may leave the contents of the destionation + * layer where the source layer is transparent, despite the definition of this + * operation. + */ +Guacamole.Layer.OUT = 0x8; + +/** + * Channel mask for the composite operation "ratop". + * Beware that WebKit-based browsers may leave the contents of the destionation + * layer where the source layer is transparent, despite the definition of this + * operation. + */ +Guacamole.Layer.RATOP = 0x9; + +/** + * Channel mask for the composite operation "src". + * Beware that WebKit-based browsers may leave the contents of the destionation + * layer where the source layer is transparent, despite the definition of this + * operation. + */ +Guacamole.Layer.SRC = 0xC; + + +/** + * Represents a single pixel of image data. All components have a minimum value + * of 0 and a maximum value of 255. + * + * @constructor + * + * @param {Number} r The red component of this pixel. + * @param {Number} g The green component of this pixel. + * @param {Number} b The blue component of this pixel. + * @param {Number} a The alpha component of this pixel. + */ +Guacamole.Layer.Pixel = function(r, g, b, a) { + + /** + * The red component of this pixel, where 0 is the minimum value, + * and 255 is the maximum. + */ + this.red = r; + + /** + * The green component of this pixel, where 0 is the minimum value, + * and 255 is the maximum. + */ + this.green = g; + + /** + * The blue component of this pixel, where 0 is the minimum value, + * and 255 is the maximum. + */ + this.blue = b; + + /** + * The alpha component of this pixel, where 0 is the minimum value, + * and 255 is the maximum. + */ + this.alpha = a; + +}; diff --git a/guacamole-common-js/src/main/resources/mouse.js b/guacamole-common-js/src/main/resources/mouse.js new file mode 100644 index 000000000..9dc2f222b --- /dev/null +++ b/guacamole-common-js/src/main/resources/mouse.js @@ -0,0 +1,785 @@ + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is guacamole-common-js. + * + * The Initial Developer of the Original Code is + * Michael Jumper. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Namespace for all Guacamole JavaScript objects. + * @namespace + */ +var Guacamole = Guacamole || {}; + +/** + * Provides cross-browser mouse events for a given element. The events of + * the given element are automatically populated with handlers that translate + * mouse events into a non-browser-specific event provided by the + * Guacamole.Mouse instance. + * + * @constructor + * @param {Element} element The Element to use to provide mouse events. + */ +Guacamole.Mouse = function(element) { + + /** + * Reference to this Guacamole.Mouse. + * @private + */ + var guac_mouse = this; + + /** + * The number of mousemove events to require before re-enabling mouse + * event handling after receiving a touch event. + */ + this.touchMouseThreshold = 3; + + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type Guacamole.Mouse.State + */ + this.currentState = new Guacamole.Mouse.State( + 0, 0, + false, false, false, false, false + ); + + /** + * Fired whenever the user presses a mouse button down over the element + * associated with this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousedown = null; + + /** + * Fired whenever the user releases a mouse button down over the element + * associated with this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmouseup = null; + + /** + * Fired whenever the user moves the mouse over the element associated with + * this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousemove = null; + + /** + * Counter of mouse events to ignore. This decremented by mousemove, and + * while non-zero, mouse events will have no effect. + * @private + */ + var ignore_mouse = 0; + + function cancelEvent(e) { + e.stopPropagation(); + if (e.preventDefault) e.preventDefault(); + e.returnValue = false; + } + + // Block context menu so right-click gets sent properly + element.addEventListener("contextmenu", function(e) { + cancelEvent(e); + }, false); + + element.addEventListener("mousemove", function(e) { + + cancelEvent(e); + + // If ignoring events, decrement counter + if (ignore_mouse) { + ignore_mouse--; + return; + } + + guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY); + + if (guac_mouse.onmousemove) + guac_mouse.onmousemove(guac_mouse.currentState); + + }, false); + + element.addEventListener("mousedown", function(e) { + + cancelEvent(e); + + // Do not handle if ignoring events + if (ignore_mouse) + return; + + switch (e.button) { + case 0: + guac_mouse.currentState.left = true; + break; + case 1: + guac_mouse.currentState.middle = true; + break; + case 2: + guac_mouse.currentState.right = true; + break; + } + + if (guac_mouse.onmousedown) + guac_mouse.onmousedown(guac_mouse.currentState); + + }, false); + + element.addEventListener("mouseup", function(e) { + + cancelEvent(e); + + // Do not handle if ignoring events + if (ignore_mouse) + return; + + switch (e.button) { + case 0: + guac_mouse.currentState.left = false; + break; + case 1: + guac_mouse.currentState.middle = false; + break; + case 2: + guac_mouse.currentState.right = false; + break; + } + + if (guac_mouse.onmouseup) + guac_mouse.onmouseup(guac_mouse.currentState); + + }, false); + + element.addEventListener("mouseout", function(e) { + + // Get parent of the element the mouse pointer is leaving + if (!e) e = window.event; + + // Check that mouseout is due to actually LEAVING the element + var target = e.relatedTarget || e.toElement; + while (target != null) { + if (target === element) + return; + target = target.parentNode; + } + + cancelEvent(e); + + // Release all buttons + if (guac_mouse.currentState.left + || guac_mouse.currentState.middle + || guac_mouse.currentState.right) { + + guac_mouse.currentState.left = false; + guac_mouse.currentState.middle = false; + guac_mouse.currentState.right = false; + + if (guac_mouse.onmouseup) + guac_mouse.onmouseup(guac_mouse.currentState); + } + + }, false); + + // Override selection on mouse event element. + element.addEventListener("selectstart", function(e) { + cancelEvent(e); + }, false); + + // Ignore all pending mouse events when touch events are the apparent source + function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; } + + element.addEventListener("touchmove", ignorePendingMouseEvents, false); + element.addEventListener("touchstart", ignorePendingMouseEvents, false); + element.addEventListener("touchend", ignorePendingMouseEvents, false); + + // Scroll wheel support + function mousewheel_handler(e) { + + var delta = 0; + if (e.detail) + delta = e.detail; + else if (e.wheelDelta) + delta = -event.wheelDelta; + + // Up + if (delta < 0) { + if (guac_mouse.onmousedown) { + guac_mouse.currentState.up = true; + guac_mouse.onmousedown(guac_mouse.currentState); + } + + if (guac_mouse.onmouseup) { + guac_mouse.currentState.up = false; + guac_mouse.onmouseup(guac_mouse.currentState); + } + } + + // Down + if (delta > 0) { + if (guac_mouse.onmousedown) { + guac_mouse.currentState.down = true; + guac_mouse.onmousedown(guac_mouse.currentState); + } + + if (guac_mouse.onmouseup) { + guac_mouse.currentState.down = false; + guac_mouse.onmouseup(guac_mouse.currentState); + } + } + + cancelEvent(e); + + } + element.addEventListener('DOMMouseScroll', mousewheel_handler, false); + element.addEventListener('mousewheel', mousewheel_handler, false); + +}; + + +/** + * Provides cross-browser relative touch event translation for a given element. + * + * Touch events are translated into mouse events as if the touches occurred + * on a touchpad (drag to push the mouse pointer, tap to click). + * + * @constructor + * @param {Element} element The Element to use to provide touch events. + */ +Guacamole.Mouse.Touchpad = function(element) { + + /** + * Reference to this Guacamole.Mouse.Touchpad. + * @private + */ + var guac_touchpad = this; + + /** + * The distance a two-finger touch must move per scrollwheel event, in + * pixels. + */ + this.scrollThreshold = 20 * (window.devicePixelRatio || 1); + + /** + * The maximum number of milliseconds to wait for a touch to end for the + * gesture to be considered a click. + */ + this.clickTimingThreshold = 250; + + /** + * The maximum number of pixels to allow a touch to move for the gesture to + * be considered a click. + */ + this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); + + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type Guacamole.Mouse.State + */ + this.currentState = new Guacamole.Mouse.State( + 0, 0, + false, false, false, false, false + ); + + /** + * Fired whenever a mouse button is effectively pressed. This can happen + * as part of a "click" gesture initiated by the user by tapping one + * or more fingers over the touchpad element, as part of a "scroll" + * gesture initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousedown = null; + + /** + * Fired whenever a mouse button is effectively released. This can happen + * as part of a "click" gesture initiated by the user by tapping one + * or more fingers over the touchpad element, as part of a "scroll" + * gesture initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmouseup = null; + + /** + * Fired whenever the user moves the mouse by dragging their finger over + * the touchpad element. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousemove = null; + + var touch_count = 0; + var last_touch_x = 0; + var last_touch_y = 0; + var last_touch_time = 0; + var pixels_moved = 0; + + var touch_buttons = { + 1: "left", + 2: "right", + 3: "middle" + }; + + var gesture_in_progress = false; + var click_release_timeout = null; + + element.addEventListener("touchend", function(e) { + + e.stopPropagation(); + e.preventDefault(); + + // If we're handling a gesture AND this is the last touch + if (gesture_in_progress && e.touches.length == 0) { + + var time = new Date().getTime(); + + // Get corresponding mouse button + var button = touch_buttons[touch_count]; + + // If mouse already down, release anad clear timeout + if (guac_touchpad.currentState[button]) { + + // Fire button up event + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); + + // Clear timeout, if set + if (click_release_timeout) { + window.clearTimeout(click_release_timeout); + click_release_timeout = null; + } + + } + + // If single tap detected (based on time and distance) + if (time - last_touch_time <= guac_touchpad.clickTimingThreshold + && pixels_moved < guac_touchpad.clickMoveThreshold) { + + // Fire button down event + guac_touchpad.currentState[button] = true; + if (guac_touchpad.onmousedown) + guac_touchpad.onmousedown(guac_touchpad.currentState); + + // Delay mouse up - mouse up should be canceled if + // touchstart within timeout. + click_release_timeout = window.setTimeout(function() { + + // Fire button up event + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); + + // Gesture now over + gesture_in_progress = false; + + }, guac_touchpad.clickTimingThreshold); + + } + + // If we're not waiting to see if this is a click, stop gesture + if (!click_release_timeout) + gesture_in_progress = false; + + } + + }, false); + + element.addEventListener("touchstart", function(e) { + + e.stopPropagation(); + e.preventDefault(); + + // Track number of touches, but no more than three + touch_count = Math.min(e.touches.length, 3); + + // Clear timeout, if set + if (click_release_timeout) { + window.clearTimeout(click_release_timeout); + click_release_timeout = null; + } + + // Record initial touch location and time for touch movement + // and tap gestures + if (!gesture_in_progress) { + + // Stop mouse events while touching + gesture_in_progress = true; + + // Record touch location and time + var starting_touch = e.touches[0]; + last_touch_x = starting_touch.clientX; + last_touch_y = starting_touch.clientY; + last_touch_time = new Date().getTime(); + pixels_moved = 0; + + } + + }, false); + + element.addEventListener("touchmove", function(e) { + + e.stopPropagation(); + e.preventDefault(); + + // Get change in touch location + var touch = e.touches[0]; + var delta_x = touch.clientX - last_touch_x; + var delta_y = touch.clientY - last_touch_y; + + // Track pixels moved + pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); + + // If only one touch involved, this is mouse move + if (touch_count == 1) { + + // Calculate average velocity in Manhatten pixels per millisecond + var velocity = pixels_moved / (new Date().getTime() - last_touch_time); + + // Scale mouse movement relative to velocity + var scale = 1 + velocity; + + // Update mouse location + guac_touchpad.currentState.x += delta_x*scale; + guac_touchpad.currentState.y += delta_y*scale; + + // Prevent mouse from leaving screen + + if (guac_touchpad.currentState.x < 0) + guac_touchpad.currentState.x = 0; + else if (guac_touchpad.currentState.x >= element.offsetWidth) + guac_touchpad.currentState.x = element.offsetWidth - 1; + + if (guac_touchpad.currentState.y < 0) + guac_touchpad.currentState.y = 0; + else if (guac_touchpad.currentState.y >= element.offsetHeight) + guac_touchpad.currentState.y = element.offsetHeight - 1; + + // Fire movement event, if defined + if (guac_touchpad.onmousemove) + guac_touchpad.onmousemove(guac_touchpad.currentState); + + // Update touch location + last_touch_x = touch.clientX; + last_touch_y = touch.clientY; + + } + + // Interpret two-finger swipe as scrollwheel + else if (touch_count == 2) { + + // If change in location passes threshold for scroll + if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { + + // Decide button based on Y movement direction + var button; + if (delta_y > 0) button = "down"; + else button = "up"; + + // Fire button down event + guac_touchpad.currentState[button] = true; + if (guac_touchpad.onmousedown) + guac_touchpad.onmousedown(guac_touchpad.currentState); + + // Fire button up event + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); + + // Only update touch location after a scroll has been + // detected + last_touch_x = touch.clientX; + last_touch_y = touch.clientY; + + } + + } + + }, false); + +}; + +/** + * Provides cross-browser absolute touch event translation for a given element. + * + * Touch events are translated into mouse events as if the touches occurred + * on a touchscreen (tapping anywhere on the screen clicks at that point, + * long-press to right-click). + * + * @constructor + * @param {Element} element The Element to use to provide touch events. + */ +Guacamole.Mouse.Touchscreen = function(element) { + + /** + * Reference to this Guacamole.Mouse.Touchscreen. + * @private + */ + var guac_touchscreen = this; + + /** + * The distance a two-finger touch must move per scrollwheel event, in + * pixels. + */ + this.scrollThreshold = 20 * (window.devicePixelRatio || 1); + + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type Guacamole.Mouse.State + */ + this.currentState = new Guacamole.Mouse.State( + 0, 0, + false, false, false, false, false + ); + + /** + * Fired whenever a mouse button is effectively pressed. This can happen + * as part of a "mousedown" gesture initiated by the user by pressing one + * finger over the touchscreen element, as part of a "scroll" gesture + * initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousedown = null; + + /** + * Fired whenever a mouse button is effectively released. This can happen + * as part of a "mouseup" gesture initiated by the user by removing the + * finger pressed against the touchscreen element, or as part of a "scroll" + * gesture initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmouseup = null; + + /** + * Fired whenever the user moves the mouse by dragging their finger over + * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, + * dragging a finger over the touchscreen element will always cause + * the mouse button to be effectively down, as if clicking-and-dragging. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousemove = null; + + element.addEventListener("touchend", function(e) { + + // Ignore if more than one touch + if (e.touches.length + e.changedTouches.length != 1) + return; + + e.stopPropagation(); + e.preventDefault(); + + // Release button + guac_touchscreen.currentState.left = false; + + // Fire release event when the last touch is released, if event defined + if (e.touches.length == 0 && guac_touchscreen.onmouseup) + guac_touchscreen.onmouseup(guac_touchscreen.currentState); + + }, false); + + element.addEventListener("touchstart", function(e) { + + // Ignore if more than one touch + if (e.touches.length != 1) + return; + + e.stopPropagation(); + e.preventDefault(); + + // Get touch + var touch = e.touches[0]; + + // Update state + guac_touchscreen.currentState.left = true; + guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY); + + // Fire press event, if defined + if (guac_touchscreen.onmousedown) + guac_touchscreen.onmousedown(guac_touchscreen.currentState); + + + }, false); + + element.addEventListener("touchmove", function(e) { + + // Ignore if more than one touch + if (e.touches.length != 1) + return; + + e.stopPropagation(); + e.preventDefault(); + + // Get touch + var touch = e.touches[0]; + + // Update state + guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY); + + // Fire movement event, if defined + if (guac_touchscreen.onmousemove) + guac_touchscreen.onmousemove(guac_touchscreen.currentState); + + }, false); + +}; + +/** + * Simple container for properties describing the state of a mouse. + * + * @constructor + * @param {Number} x The X position of the mouse pointer in pixels. + * @param {Number} y The Y position of the mouse pointer in pixels. + * @param {Boolean} left Whether the left mouse button is pressed. + * @param {Boolean} middle Whether the middle mouse button is pressed. + * @param {Boolean} right Whether the right mouse button is pressed. + * @param {Boolean} up Whether the up mouse button is pressed (the fourth + * button, usually part of a scroll wheel). + * @param {Boolean} down Whether the down mouse button is pressed (the fifth + * button, usually part of a scroll wheel). + */ +Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { + + /** + * Reference to this Guacamole.Mouse.State. + * @private + */ + var guac_state = this; + + /** + * The current X position of the mouse pointer. + * @type Number + */ + this.x = x; + + /** + * The current Y position of the mouse pointer. + * @type Number + */ + this.y = y; + + /** + * Whether the left mouse button is currently pressed. + * @type Boolean + */ + this.left = left; + + /** + * Whether the middle mouse button is currently pressed. + * @type Boolean + */ + this.middle = middle + + /** + * Whether the right mouse button is currently pressed. + * @type Boolean + */ + this.right = right; + + /** + * Whether the up mouse button is currently pressed. This is the fourth + * mouse button, associated with upward scrolling of the mouse scroll + * wheel. + * @type Boolean + */ + this.up = up; + + /** + * Whether the down mouse button is currently pressed. This is the fifth + * mouse button, associated with downward scrolling of the mouse scroll + * wheel. + * @type Boolean + */ + this.down = down; + + /** + * Updates the position represented within this state object by the given + * element and clientX/clientY coordinates (commonly available within event + * objects). Position is translated from clientX/clientY (relative to + * viewport) to element-relative coordinates. + * + * @param {Element} element The element the coordinates should be relative + * to. + * @param {Number} clientX The X coordinate to translate, viewport-relative. + * @param {Number} clientY The Y coordinate to translate, viewport-relative. + */ + this.fromClientPosition = function(element, clientX, clientY) { + + guac_state.x = clientX - element.offsetLeft; + guac_state.y = clientY - element.offsetTop; + + // This is all JUST so we can get the mouse position within the element + var parent = element.offsetParent; + while (parent && !(parent === document.body)) { + guac_state.x -= parent.offsetLeft - parent.scrollLeft; + guac_state.y -= parent.offsetTop - parent.scrollTop; + + parent = parent.offsetParent; + } + + // Element ultimately depends on positioning within document body, + // take document scroll into account. + if (parent) { + var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; + var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop; + + guac_state.x -= parent.offsetLeft - documentScrollLeft; + guac_state.y -= parent.offsetTop - documentScrollTop; + } + + }; + +}; + diff --git a/guacamole-common-js/src/main/resources/oskeyboard.js b/guacamole-common-js/src/main/resources/oskeyboard.js new file mode 100644 index 000000000..7da6b4b81 --- /dev/null +++ b/guacamole-common-js/src/main/resources/oskeyboard.js @@ -0,0 +1,653 @@ + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is guac-common-js. + * + * The Initial Developer of the Original Code is + * Michael Jumper. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Namespace for all Guacamole JavaScript objects. + * @namespace + */ +var Guacamole = Guacamole || {}; + +/** + * Dynamic on-screen keyboard. Given the URL to an XML keyboard layout file, + * this object will download and use the XML to construct a clickable on-screen + * keyboard with its own key events. + * + * @constructor + * @param {String} url The URL of an XML keyboard layout file. + */ +Guacamole.OnScreenKeyboard = function(url) { + + var on_screen_keyboard = this; + + /** + * State of all modifiers. This is the bitwise OR of all active modifier + * values. + * + * @private + */ + var modifiers = 0; + + var scaledElements = []; + + var modifier_masks = {}; + var next_mask = 1; + + /** + * Adds a class to an element. + * + * @private + * @function + * @param {Element} element The element to add a class to. + * @param {String} classname The name of the class to add. + */ + var addClass; + + /** + * Removes a class from an element. + * + * @private + * @function + * @param {Element} element The element to remove a class from. + * @param {String} classname The name of the class to remove. + */ + var removeClass; + + /** + * The number of mousemove events to require before re-enabling mouse + * event handling after receiving a touch event. + */ + this.touchMouseThreshold = 3; + + /** + * Counter of mouse events to ignore. This decremented by mousemove, and + * while non-zero, mouse events will have no effect. + * @private + */ + var ignore_mouse = 0; + + // Ignore all pending mouse events when touch events are the apparent source + function ignorePendingMouseEvents() { ignore_mouse = on_screen_keyboard.touchMouseThreshold; } + + // If Node.classList is supported, implement addClass/removeClass using that + if (Node.classList) { + + /** @ignore */ + addClass = function(element, classname) { + element.classList.add(classname); + }; + + /** @ignore */ + removeClass = function(element, classname) { + element.classList.remove(classname); + }; + + } + + // Otherwise, implement own + else { + + /** @ignore */ + addClass = function(element, classname) { + + // Simply add new class + element.className += " " + classname; + + }; + + /** @ignore */ + removeClass = function(element, classname) { + + // Filter out classes with given name + element.className = element.className.replace(/([^ ]+)[ ]*/g, + function(match, testClassname, spaces, offset, string) { + + // If same class, remove + if (testClassname == classname) + return ""; + + // Otherwise, allow + return match; + + } + ); + + }; + + } + + // Returns a unique power-of-two value for the modifier with the + // given name. The same value will be returned for the same modifier. + function getModifierMask(name) { + + var value = modifier_masks[name]; + if (!value) { + + // Get current modifier, advance to next + value = next_mask; + next_mask <<= 1; + + // Store value of this modifier + modifier_masks[name] = value; + + } + + return value; + + } + + function ScaledElement(element, width, height, scaleFont) { + + this.width = width; + this.height = height; + + this.scale = function(pixels) { + element.style.width = (width * pixels) + "px"; + element.style.height = (height * pixels) + "px"; + + if (scaleFont) { + element.style.lineHeight = (height * pixels) + "px"; + element.style.fontSize = pixels + "px"; + } + } + + } + + // For each child of element, call handler defined in next + function parseChildren(element, next) { + + var children = element.childNodes; + for (var i=0; i= 0x0000 && charCode <= 0x00FF) + real_keysym = charCode; + else if (charCode >= 0x0100 && charCode <= 0x10FFFF) + real_keysym = 0x01000000 | charCode; + + } + + // Create cap + var cap = new Guacamole.OnScreenKeyboard.Cap(content, real_keysym); + + if (e.getAttribute("modifier")) + cap.modifier = e.getAttribute("modifier"); + + // Create cap element + var cap_element = document.createElement("div"); + cap_element.className = "guac-keyboard-cap"; + cap_element.textContent = content; + key_element.appendChild(cap_element); + + // Append class if specified + if (e.getAttribute("class")) + cap_element.className += " " + e.getAttribute("class"); + + // Get modifier value + var modifierValue = 0; + if (e.getAttribute("if")) { + + // Get modifier value for specified comma-delimited + // list of required modifiers. + var requirements = e.getAttribute("if").split(","); + for (var i=0; i 0) { + + sendingMessages = true; + + var message_xmlhttprequest = new XMLHttpRequest(); + message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); + message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + + // Once response received, send next queued event. + message_xmlhttprequest.onreadystatechange = function() { + if (message_xmlhttprequest.readyState == 4) { + + // If an error occurs during send, handle it + if (message_xmlhttprequest.status != 200) + handleHTTPTunnelError(message_xmlhttprequest); + + // Otherwise, continue the send loop + else + sendPendingMessages(); + + } + } + + message_xmlhttprequest.send(outputMessageBuffer); + outputMessageBuffer = ""; // Clear buffer + + } + else + sendingMessages = false; + + } + + function getHTTPTunnelErrorMessage(xmlhttprequest) { + + var status = xmlhttprequest.status; + + // Special cases + if (status == 0) return "Disconnected"; + if (status == 200) return "Success"; + if (status == 403) return "Unauthorized"; + if (status == 404) return "Connection closed"; /* While it may be more + * accurate to say the + * connection does not + * exist, it is confusing + * to the user. + * + * In general, this error + * will only happen when + * the tunnel does not + * exist, which happens + * after the connection + * is closed and the + * tunnel is detached. + */ + // Internal server errors + if (status >= 500 && status <= 599) return "Server error"; + + // Otherwise, unknown + return "Unknown error"; + + } + + function handleHTTPTunnelError(xmlhttprequest) { + + // Get error message + var message = getHTTPTunnelErrorMessage(xmlhttprequest); + + // Call error handler + if (tunnel.onerror) tunnel.onerror(message); + + // Finish + tunnel.disconnect(); + + } + + + function handleResponse(xmlhttprequest) { + + var interval = null; + var nextRequest = null; + + var dataUpdateEvents = 0; + + // The location of the last element's terminator + var elementEnd = -1; + + // Where to start the next length search or the next element + var startIndex = 0; + + // Parsed elements + var elements = new Array(); + + function parseResponse() { + + // Do not handle responses if not connected + if (currentState != STATE_CONNECTED) { + + // Clean up interval if polling + if (interval != null) + clearInterval(interval); + + return; + } + + // Do not parse response yet if not ready + if (xmlhttprequest.readyState < 2) return; + + // Attempt to read status + var status; + try { status = xmlhttprequest.status; } + + // If status could not be read, assume successful. + catch (e) { status = 200; } + + // Start next request as soon as possible IF request was successful + if (nextRequest == null && status == 200) + nextRequest = makeRequest(); + + // Parse stream when data is received and when complete. + if (xmlhttprequest.readyState == 3 || + xmlhttprequest.readyState == 4) { + + // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) + if (pollingMode == POLLING_ENABLED) { + if (xmlhttprequest.readyState == 3 && interval == null) + interval = setInterval(parseResponse, 30); + else if (xmlhttprequest.readyState == 4 && interval != null) + clearInterval(interval); + } + + // If canceled, stop transfer + if (xmlhttprequest.status == 0) { + tunnel.disconnect(); + return; + } + + // Halt on error during request + else if (xmlhttprequest.status != 200) { + handleHTTPTunnelError(xmlhttprequest); + return; + } + + // Attempt to read in-progress data + var current; + try { current = xmlhttprequest.responseText; } + + // Do not attempt to parse if data could not be read + catch (e) { return; } + + // While search is within currently received data + while (elementEnd < current.length) { + + // If we are waiting for element data + if (elementEnd >= startIndex) { + + // We now have enough data for the element. Parse. + var element = current.substring(startIndex, elementEnd); + var terminator = current.substring(elementEnd, elementEnd+1); + + // Add element to array + elements.push(element); + + // If last element, handle instruction + if (terminator == ";") { + + // Get opcode + var opcode = elements.shift(); + + // Call instruction handler. + if (tunnel.oninstruction != null) + tunnel.oninstruction(opcode, elements); + + // Clear elements + elements.length = 0; + + } + + // Start searching for length at character after + // element terminator + startIndex = elementEnd + 1; + + } + + // Search for end of length + var lengthEnd = current.indexOf(".", startIndex); + if (lengthEnd != -1) { + + // Parse length + var length = parseInt(current.substring(elementEnd+1, lengthEnd)); + + // If we're done parsing, handle the next response. + if (length == 0) { + + // Clean up interval if polling + if (interval != null) + clearInterval(interval); + + // Clean up object + xmlhttprequest.onreadystatechange = null; + xmlhttprequest.abort(); + + // Start handling next request + if (nextRequest) + handleResponse(nextRequest); + + // Done parsing + break; + + } + + // Calculate start of element + startIndex = lengthEnd + 1; + + // Calculate location of element terminator + elementEnd = startIndex + length; + + } + + // If no period yet, continue search when more data + // is received + else { + startIndex = current.length; + break; + } + + } // end parse loop + + } + + } + + // If response polling enabled, attempt to detect if still + // necessary (via wrapping parseResponse()) + if (pollingMode == POLLING_ENABLED) { + xmlhttprequest.onreadystatechange = function() { + + // If we receive two or more readyState==3 events, + // there is no need to poll. + if (xmlhttprequest.readyState == 3) { + dataUpdateEvents++; + if (dataUpdateEvents >= 2) { + pollingMode = POLLING_DISABLED; + xmlhttprequest.onreadystatechange = parseResponse; + } + } + + parseResponse(); + } + } + + // Otherwise, just parse + else + xmlhttprequest.onreadystatechange = parseResponse; + + parseResponse(); + + } + + /** + * Arbitrary integer, unique for each tunnel read request. + * @private + */ + var request_id = 0; + + function makeRequest() { + + // Make request, increment request ID + var xmlhttprequest = new XMLHttpRequest(); + xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); + xmlhttprequest.send(null); + + return xmlhttprequest; + + } + + this.connect = function(data) { + + // Start tunnel and connect synchronously + var connect_xmlhttprequest = new XMLHttpRequest(); + connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false); + connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + connect_xmlhttprequest.send(data); + + // If failure, throw error + if (connect_xmlhttprequest.status != 200) { + var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest); + throw new Error(message); + } + + // Get UUID from response + tunnel_uuid = connect_xmlhttprequest.responseText; + + // Start reading data + currentState = STATE_CONNECTED; + handleResponse(makeRequest()); + + }; + + this.disconnect = function() { + currentState = STATE_DISCONNECTED; + }; + +}; + +Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); + + +/** + * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. + * + * @constructor + * @augments Guacamole.Tunnel + * @param {String} tunnelURL The URL of the WebSocket tunneling service. + */ +Guacamole.WebSocketTunnel = function(tunnelURL) { + + /** + * Reference to this WebSocket tunnel. + * @private + */ + var tunnel = this; + + /** + * The WebSocket used by this tunnel. + * @private + */ + var socket = null; + + /** + * The WebSocket protocol corresponding to the protocol used for the current + * location. + * @private + */ + var ws_protocol = { + "http:": "ws:", + "https:": "wss:" + }; + + var status_code = { + 1000: "Connection closed normally.", + 1001: "Connection shut down.", + 1002: "Protocol error.", + 1003: "Invalid data.", + 1004: "[UNKNOWN, RESERVED]", + 1005: "No status code present.", + 1006: "Connection closed abnormally.", + 1007: "Inconsistent data type.", + 1008: "Policy violation.", + 1009: "Message too large.", + 1010: "Extension negotiation failed." + }; + + var STATE_IDLE = 0; + var STATE_CONNECTED = 1; + var STATE_DISCONNECTED = 2; + + var currentState = STATE_IDLE; + + // Transform current URL to WebSocket URL + + // If not already a websocket URL + if ( tunnelURL.substring(0, 3) != "ws:" + && tunnelURL.substring(0, 4) != "wss:") { + + var protocol = ws_protocol[window.location.protocol]; + + // If absolute URL, convert to absolute WS URL + if (tunnelURL.substring(0, 1) == "/") + tunnelURL = + protocol + + "//" + window.location.host + + tunnelURL; + + // Otherwise, construct absolute from relative URL + else { + + // Get path from pathname + var slash = window.location.pathname.lastIndexOf("/"); + var path = window.location.pathname.substring(0, slash + 1); + + // Construct absolute URL + tunnelURL = + protocol + + "//" + window.location.host + + path + + tunnelURL; + + } + + } + + this.sendMessage = function(elements) { + + // Do not attempt to send messages if not connected + if (currentState != STATE_CONNECTED) + return; + + // Do not attempt to send empty messages + if (arguments.length == 0) + return; + + /** + * Converts the given value to a length/string pair for use as an + * element in a Guacamole instruction. + * + * @private + * @param value The value to convert. + * @return {String} The converted value. + */ + function getElement(value) { + var string = new String(value); + return string.length + "." + string; + } + + // Initialized message with first element + var message = getElement(arguments[0]); + + // Append remaining elements + for (var i=1; i + guacamole-common-js + + zip + + + + src/main/resources + + + +