pax_global_header00006660000000000000000000000064141065505250014515gustar00rootroot0000000000000052 comment=5f563e6c33afac948d79046f4720c63745f4c7a0 quicktext-4.1/000077500000000000000000000000001410655052500134025ustar00rootroot00000000000000quicktext-4.1/CONTRIBUTORS.md000066400000000000000000000005111410655052500156560ustar00rootroot00000000000000## Original Creator * Emil Hesslow ## Current Maintainer * John Bieling ## Contributors * John Bieling * Sebastian Haberey * Emil Hesslow * R Kent James * Philippe Lieser * Christian Schneider * Janning Vygen * bgeiring * drakulis * Peñalara Software S.L. * Samuel Plentz ## Translators * Alexey Sinitsyn (ru) * Óvári (hu) quicktext-4.1/LICENSE000066400000000000000000000405261410655052500144160ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that 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. License Grants and Conditions -------------------------------- 2.1. Grants 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 such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 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 Software 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 placed in a text file included with all distributions of the Covered Software under this License. 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. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software 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 Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any 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 Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, 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. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the 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. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. quicktext-4.1/README.md000066400000000000000000000026061410655052500146650ustar00rootroot00000000000000[Quicktext](https://addons.thunderbird.net/addon/quicktext/) is a [Thunderbird](https://www.thunderbird.net/) extension, which has been created by Emil Hesslow. However, he was no longer able to update Quicktext or provide any kind of support. Thankfully he changed the license of Quicktext to MPL 2.0, so I could continue to update the extension and make it work with the most recent version of Thunderbird. I will try to keep Quicktext going, but support will be limited. You may use the [issue section](https://github.com/jobisoft/quicktext/issues) of this repository to report bugs. You may also discuss Quicktext related issues with other users at https://discourse.mozilla.org/c/thunderbird. **Note**: Emil maintained two versions of Quicktext, a free “Standard” version and a paid “Pro” version with additional features. Since he released both versions under MPL 2.0, the [official version of Quicktext](https://addons.thunderbird.net/addon/quicktext/) is based on the “Pro” version. However, the name of the extension remains just “Quicktext”. There is no longer a seperate Quicktext Pro version. More information and usage descriptions can be found in the [wiki](https://github.com/jobisoft/quicktext/wiki) of this repository. **The latest release and a change log can be found on [addons.thunderbird.net](https://addons.thunderbird.net/en-US/thunderbird/addon/quicktext/versions/)**. quicktext-4.1/_locales/000077500000000000000000000000001410655052500151635ustar00rootroot00000000000000quicktext-4.1/_locales/de/000077500000000000000000000000001410655052500155535ustar00rootroot00000000000000quicktext-4.1/_locales/de/messages.json000066400000000000000000000003721410655052500202570ustar00rootroot00000000000000{ "extensionDescription": { "message": "Fügt eine Symbolleiste zum E-Mail-Bearbeitenfenster hinzu, sodass Textbausteine einfach und schnell hinzugefügt werden können. Es ist auch möglich, Variablen wie [[TO=firstname]] zu verwenden." } } quicktext-4.1/_locales/en-US/000077500000000000000000000000001410655052500161125ustar00rootroot00000000000000quicktext-4.1/_locales/en-US/messages.json000066400000000000000000000003171410655052500206150ustar00rootroot00000000000000{ "extensionDescription": { "message": "Adds a toolbar with unlimited number of text to quickly insert. It is also possible to use variables like [[TO=firstname]]. With settings for everything." } } quicktext-4.1/_locales/es/000077500000000000000000000000001410655052500155725ustar00rootroot00000000000000quicktext-4.1/_locales/es/messages.json000066400000000000000000000003451410655052500202760ustar00rootroot00000000000000{ "extensionDescription": { "message": "Agrega una barra de herramientas con un número ilimitado de texto para insertar rápidamente. También es posible usar variables como [[TO=firstname]]. Con ajustes para todo." } } quicktext-4.1/_locales/fr/000077500000000000000000000000001410655052500155725ustar00rootroot00000000000000quicktext-4.1/_locales/fr/messages.json000066400000000000000000000004141410655052500202730ustar00rootroot00000000000000{ "extensionDescription": { "message": "Ajoute une barre d’outils à la fenêtre Email Edit afin que les modules de texte puissent être ajoutés facilement et rapidement. Il est également possible d'utiliser des variables telles que [[TO=firstname]]." } } quicktext-4.1/_locales/hu/000077500000000000000000000000001410655052500155775ustar00rootroot00000000000000quicktext-4.1/_locales/hu/messages.json000066400000000000000000000003751410655052500203060ustar00rootroot00000000000000{ "extensionDescription": { "message": "Eszköztár, parancsfájlok és sablonok korlátlan lehetoségekkel, beleértve a [[TO=firstname]] változókat, a gyors szövegbeillesztéshez" }, "extensionName": { "message": "Gyorsszöveg" } } quicktext-4.1/_locales/ja/000077500000000000000000000000001410655052500155555ustar00rootroot00000000000000quicktext-4.1/_locales/ja/messages.json000066400000000000000000000003171410655052500202600ustar00rootroot00000000000000{ "extensionDescription": { "message": "Adds a toolbar with unlimited number of text to quickly insert. It is also possible to use variables like [[TO=firstname]]. With settings for everything." } } quicktext-4.1/_locales/pt_BR/000077500000000000000000000000001410655052500161715ustar00rootroot00000000000000quicktext-4.1/_locales/pt_BR/messages.json000066400000000000000000000003561410655052500206770ustar00rootroot00000000000000{ "extensionDescription": { "message": "Adiciona uma barra de ferramentas com um número ilimitado de texto para inserir rapidamente. Também é possível usar variáveis como [[TO=firstname]]. Com configurações para tudo." } } quicktext-4.1/_locales/ru/000077500000000000000000000000001410655052500156115ustar00rootroot00000000000000quicktext-4.1/_locales/ru/messages.json000066400000000000000000000005561410655052500203210ustar00rootroot00000000000000{ "extensionDescription": { "message": "Добавляет панель инструментов с неограниченным количеством текста для быстрой вставки. Также можно использовать такие переменные, как [[TO=firstname]]. С настройками для всего." } } quicktext-4.1/_locales/sv-SE/000077500000000000000000000000001410655052500161205ustar00rootroot00000000000000quicktext-4.1/_locales/sv-SE/messages.json000066400000000000000000000003171410655052500206230ustar00rootroot00000000000000{ "extensionDescription": { "message": "Adds a toolbar with unlimited number of text to quickly insert. It is also possible to use variables like [[TO=firstname]]. With settings for everything." } } quicktext-4.1/api/000077500000000000000000000000001410655052500141535ustar00rootroot00000000000000quicktext-4.1/api/LegacyPrefs/000077500000000000000000000000001410655052500163575ustar00rootroot00000000000000quicktext-4.1/api/LegacyPrefs/README.md000066400000000000000000000023721410655052500176420ustar00rootroot00000000000000## Objective Use this API to access Thunderbird's system preferences or to migrate your own preferences from the Thunderbird preference system to the local storage of your MailExtension. ## Usage This API provides the following methods: ### async getPref(aName, [aFallback]) Returns the value for the ``aName`` preference. If it is not defined or has no default value assigned, ``aFallback`` will be returned (which defaults to ``null``). ### async getUserPref(aName) Returns the user defined value for the ``aName`` preference. This will ignore any defined default value and will only return an explicitly set value, which differs from the default. Otherwise it will return ``null``. ### clearUserPref(aName) Clears the user defined value for preference ``aName``. Subsequent calls to ``getUserPref(aName)`` will return ``null``. ### async setPref(aName, aValue) Set the ``aName`` preference to the given value. Will return false and log an error to the console, if the type of ``aValue`` does not match the type of the preference. --- A detailed example using the LegacyPref API to migrate add-on preferences to the local storage can be found in [/scripts/preferences/](https://github.com/thundernest/addon-developer-support/tree/master/scripts/preferences). quicktext-4.1/api/LegacyPrefs/implementation.js000066400000000000000000000133741410655052500217520ustar00rootroot00000000000000/* * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * * Version: 1.7 * add onChanged event * * Version: 1.6 * add setDefaultPref() * * Version: 1.5 * replace set/getCharPref by set/getStringPref to fix encoding issue * * Version: 1.4 * - setPref() function returns true if the value could be set, otherwise false * * Version: 1.3 * - add setPref() function * * Version: 1.2 * - add getPref() function * * Author: John Bieling (john@thunderbird.net) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var { ExtensionCommon } = ChromeUtils.import( "resource://gre/modules/ExtensionCommon.jsm" ); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var LegacyPrefs = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { let self = this; this.getLegacyPref = async function ( aName, aFallback = null, userPrefOnly = true ) { let prefType = Services.prefs.getPrefType(aName); if (prefType == Services.prefs.PREF_INVALID) { return aFallback; } let value = aFallback; if (!userPrefOnly || Services.prefs.prefHasUserValue(aName)) { switch (prefType) { case Services.prefs.PREF_STRING: value = Services.prefs.getStringPref(aName, aFallback); break; case Services.prefs.PREF_INT: value = Services.prefs.getIntPref(aName, aFallback); break; case Services.prefs.PREF_BOOL: value = Services.prefs.getBoolPref(aName, aFallback); break; default: console.error( `Legacy preference <${aName}> has an unknown type of <${prefType}>.` ); } } return value; }; this.observerTracker = null; this.observing = false; this.observedBranch = null; this.observer = { async observe(aSubject, aTopic, aData) { if (aTopic == "nsPref:changed") { let name = aData.substr(self.observedBranch.length); let value = await self.getLegacyPref(aData); self.observerTracker(name, value); } }, QueryInterface: ChromeUtils.generateQI([ "nsIObserver", "nsISupportsWeakReference", ]), }; return { LegacyPrefs: { onChanged: new ExtensionCommon.EventManager({ context, name: "LegacyPrefs.onChanged", register: (fire) => { if (self.observing) throw new Error("Only one onChanged observer allowed."); if (!self.observedBranch) throw new Error( "Please call setObservingBranch() before using the onChanged event" ); self.observing = true; self.observerTracker = fire.sync; Services.prefs .getBranch(null) .addObserver(self.observedBranch, self.observer); return () => { Services.prefs .getBranch(null) .removeObserver(this.observedBranch, this.observer); self.observerTracker = null; self.observing = false; }; }, }).api(), setObservingBranch: function (observeBranch) { // This implementation only supports one observer if (self.observing) throw new Error( "Already observing, cannot change observed branch." ); if (!observeBranch) throw new Error("Invalid branch value."); self.observedBranch = observeBranch; }, // only returns something, if a user pref value is set getUserPref: async function (aName) { return await self.getLegacyPref(aName); }, // returns the default value, if no user defined value exists, // and returns the fallback value, if the preference does not exist getPref: async function (aName, aFallback = null) { return await self.getLegacyPref(aName, aFallback, false); }, clearUserPref: function (aName) { Services.prefs.clearUserPref(aName); }, // sets a pref setPref: async function (aName, aValue) { let prefType = Services.prefs.getPrefType(aName); if (prefType == Services.prefs.PREF_INVALID) { console.error( `Unknown legacy preference <${aName}>, forgot to declare a default?.` ); return false; } switch (prefType) { case Services.prefs.PREF_STRING: Services.prefs.setStringPref(aName, aValue); return true; break; case Services.prefs.PREF_INT: Services.prefs.setIntPref(aName, aValue); return true; break; case Services.prefs.PREF_BOOL: Services.prefs.setBoolPref(aName, aValue); return true; break; default: console.error( `Legacy preference <${aName}> has an unknown type of <${prefType}>.` ); } return false; }, setDefaultPref: async function (aName, aValue) { let defaults = Services.prefs.getDefaultBranch(""); switch (typeof aValue) { case "string": return defaults.setStringPref(aName, aValue); case "number": return defaults.setIntPref(aName, aValue); case "boolean": return defaults.setBoolPref(aName, aValue); } }, }, }; } }; quicktext-4.1/api/LegacyPrefs/schema.json000066400000000000000000000070251410655052500205160ustar00rootroot00000000000000[ { "namespace": "LegacyPrefs", "events": [ { "name": "onChanged", "type": "function", "description": "Fired when a preference has been changed.", "parameters": [ { "name": "name", "type": "string", "description": "Name of the preference." }, { "name": "value", "type": "any", "description": "Value of the preference." } ] } ], "functions": [ { "name": "setObservingBranch", "type": "function", "async": true, "description": "Set the branch which onChanged should be sensitive for.", "parameters": [ { "name": "aBranch", "type": "string", "description": "Name of the branch to observe." } ] }, { "name": "getUserPref", "type": "function", "async": true, "description": "Gets a user value from the legacy pref system.", "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." } ] }, { "name": "getPref", "type": "function", "async": true, "description": "Gets a value from the legacy pref system.", "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." }, { "name": "aFallback", "type": "string", "description": "Value to be returned, if the requested preference does not exist.", "optional": true, "default": null } ] }, { "name": "setPref", "type": "function", "async": true, "description": "Sets a value for an existing pref of the legacy pref system.", "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." }, { "name": "aValue", "choices": [ { "type": "string" }, { "type": "integer" }, { "type": "boolean" } ], "description": "Value to be set." } ] }, { "name": "setDefaultPref", "type": "function", "async": true, "description": "Defines the default value for pref of the legacy pref system. This defines the type of a pref and is needed for new prefs.", "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." }, { "name": "aValue", "choices": [ { "type": "string" }, { "type": "integer" }, { "type": "boolean" } ], "description": "Default value to be set." } ] }, { "name": "clearUserPref", "type": "function", "description": "Removes a user value from the legacy pref system.", "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." } ] } ] } ] quicktext-4.1/api/NotifyTools/000077500000000000000000000000001410655052500164445ustar00rootroot00000000000000quicktext-4.1/api/NotifyTools/README.md000066400000000000000000000105231410655052500177240ustar00rootroot00000000000000# Objective The NotifyTools provide a bidirectional messaging system between Experiments scripts and the WebExtension's background page (even with [e10s](https://developer.thunderbird.net/add-ons/updating/tb91/changes#thunderbird-is-now-multi-process-e-10-s) being enabled in Thunderbird Beta 86). ![messaging](https://user-images.githubusercontent.com/5830621/111921572-90db8d80-8a95-11eb-8673-4e1370d49e4b.png) They allow to work on add-on uprades in smaller steps, as single calls (like `window.openDialog()`) in the middle of legacy code can be replaced by WebExtension calls, by stepping out of the Experiment and back in when the task has been finished. More details can be found in [this update tutorial](https://github.com/thundernest/addon-developer-support/wiki/Tutorial:-Convert-add-on-parts-individually-by-using-a-messaging-system). # Usage Add the [NotifyTools API](https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools) to your add-on. Your `manifest.json` needs an entry like this: ``` "experiment_apis": { "NotifyTools": { "schema": "api/NotifyTools/schema.json", "parent": { "scopes": ["addon_parent"], "paths": [["NotifyTools"]], "script": "api/NotifyTools/implementation.js" } } }, ``` Additionally to the [NotifyTools API](https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools) the [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script is needed as the counterpart in Experiment scripts. **Note:** You need to adjust the `notifyTools.js` script and add your add-on ID at the top. ## Receiving notifications from Experiment scripts Add a listener for the `onNotifyBackground` event in your WebExtension's background page: ``` messenger.NotifyTools.onNotifyBackground.addListener(async (info) => { switch (info.command) { case "doSomething": //do something let rv = await doSomething(); return rv; break; } }); ``` The `onNotifyBackground` event will receive and respond to notifications send from your Experiment scripts: ``` notifyTools.notifyBackground({command: "doSomething"}).then((data) => { console.log(data); }); ``` Include the [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script in your Experiment script to be able to use `notifyTools.notifyBackground()`. **Note**: If multiple `onNotifyBackground` listeners are registered in the WebExtension's background page and more than one is returning data, the value from the first one is returned to the Experiment. This may lead to inconsistent behavior, so make sure that for each request only one listener is returning data. ## Sending notifications to Experiments scripts Use the `notifyExperiment()` method to send a notification from the WebExtension's background page to Experiment scripts: ``` messenger.NotifyTools.notifyExperiment({command: "doSomething"}).then((data) => { console.log(data) }); ``` The receiving Experiment script needs to include the [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script and must setup a listener using the following methods: ### registerListener(callback); Registers a callback function, which is called when a notification from the WebExtension's background page has been received. The `registerListener()` function returns an `id` which can be used to remove the listener again. Example: ``` function doSomething(data) { console.log(data); return true; } let id = notifyTools.registerListener(doSomething); ``` ### removeListener(id) Removes the listener with the given `id`. Example: ``` notifyTools.removeListener(id); ``` ### enable() The [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script attaches its `enable()` method to the `load` event of the current window. If the script is loaded into a window-less environment, `enable()` needs to be called manually. ### disable() The [notifyTools.js](https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools) script attaches its `disable()` method to the `unload` event of the current window. If the script is loaded into a window-less environment, `disable()` needs to be called manually. quicktext-4.1/api/NotifyTools/implementation.js000066400000000000000000000074121410655052500220330ustar00rootroot00000000000000/* * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * * Author: John Bieling (john@thunderbird.net) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Get various parts of the WebExtension framework that we need. var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm"); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var NotifyTools = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { var self = this; this.onNotifyBackgroundObserver = { observe: async function (aSubject, aTopic, aData) { if ( Object.keys(self.observerTracker).length > 0 && aData == self.extension.id ) { let payload = aSubject.wrappedJSObject; // This is called from the BL observer.js and therefore it should have a resolve // payload, but better check. if (payload.resolve) { let observerTrackerPromises = []; // Push listener into promise array, so they can run in parallel for (let listener of Object.values(self.observerTracker)) { observerTrackerPromises.push(listener(payload.data)); } // We still have to await all of them but wait time is just the time needed // for the slowest one. let results = []; for (let observerTrackerPromise of observerTrackerPromises) { let rv = await observerTrackerPromise; if (rv != null) results.push(rv); } if (results.length == 0) { payload.resolve(); } else { if (results.length > 1) { console.warn( "Received multiple results from onNotifyBackground listeners. Using the first one, which can lead to inconsistent behavior.", results ); } payload.resolve(results[0]); } } else { // Just call the listener. for (let listener of Object.values(self.observerTracker)) { listener(payload.data); } } } }, }; this.observerTracker = {}; this.observerTrackerNext = 1; // Add observer for notifyTools.js Services.obs.addObserver( this.onNotifyBackgroundObserver, "NotifyBackgroundObserver", false ); return { NotifyTools: { notifyExperiment(data) { return new Promise(resolve => { Services.obs.notifyObservers( { data, resolve }, "NotifyExperimentObserver", self.extension.id ); }); }, onNotifyBackground: new ExtensionCommon.EventManager({ context, name: "NotifyTools.onNotifyBackground", register: (fire) => { let trackerId = self.observerTrackerNext++; self.observerTracker[trackerId] = fire.sync; return () => { delete self.observerTracker[trackerId]; }; }, }).api(), } }; } onShutdown(isAppShutdown) { if (isAppShutdown) { return; // the application gets unloaded anyway } // Remove observer for notifyTools.js Services.obs.removeObserver( this.onNotifyBackgroundObserver, "NotifyBackgroundObserver" ); // Flush all caches Services.obs.notifyObservers(null, "startupcache-invalidate"); } }; quicktext-4.1/api/NotifyTools/schema.json000066400000000000000000000015441410655052500206030ustar00rootroot00000000000000[ { "namespace": "NotifyTools", "events": [ { "name": "onNotifyBackground", "type": "function", "description": "Fired when a new notification from notifyTools.js in an Experiment has been received.", "parameters": [ { "name": "data", "type": "any", "description": "Restrictions of the structured clone algorithm apply." } ] } ], "functions": [ { "name": "notifyExperiment", "type": "function", "async": true, "description": "Notifies notifyTools.js in an Experiment and sends data.", "parameters": [ { "name": "data", "type": "any", "description": "Restrictions of the structured clone algorithm apply." } ] } ] } ] quicktext-4.1/api/WindowListener/000077500000000000000000000000001410655052500171305ustar00rootroot00000000000000quicktext-4.1/api/WindowListener/CHANGELOG.md000066400000000000000000000055451410655052500207520ustar00rootroot00000000000000Version: 1.54 ------------- - fix "ownerDoc.getElementById() is undefined" bug Version: 1.53 ------------- - fix "tab.browser is undefined" bug Version: 1.52 ------------- - clear cache only if add-on is uninstalled/updated, not on app shutdown Version: 1.51 ------------- - use wrench button for options for TB78.10 Version: 1.50 ------------- - use built-in CSS rules to fix options button for dark themes (thanks to Thunder) - fix some occasions where options button was not added Version: 1.49 ------------- - fixed missing eventListener for Beta + Daily Version: 1.48 ------------- - moved notifyTools into its own NotifyTools API. Version: 1.39 ------------- - fix for 68 Version: 1.36 ------------- - fix for beta 87 Version: 1.35 ------------- - add support for options button/menu in add-on manager and fix 68 double menu entry Version: 1.34 ------------- - fix error in unload Version: 1.33 ------------- - fix for e10s Version: 1.30 ------------- - replace setCharPref by setStringPref to cope with UTF-8 encoding Version: 1.29 ------------- - open options window centered Version: 1.28 ------------- - do not crash on missing icon Version: 1.27 ------------- - add openOptionsDialog() Version: 1.26 ------------- - pass WL object to legacy preference window Version: 1.25 ------------- - adding waitForMasterPassword Version: 1.24 ------------- - automatically localize i18n locale strings in injectElements() Version: 1.22 ------------- - to reduce confusions, only check built-in URLs as add-on URLs cannot be resolved if a temp installed add-on has bin zipped Version: 1.21 ------------- - print debug messages only if add-ons are installed temporarily from the add-on debug page - add checks to registered windows and scripts, if they actually exists Version: 1.20 ------------- - fix long delay before customize window opens - fix non working removal of palette items Version: 1.19 ------------- - add support for ToolbarPalette Version: 1.18 ------------- - execute shutdown script also during global app shutdown (fixed) Version: 1.17 ------------- - execute shutdown script also during global app shutdown Version: 1.16 ------------- - support for persist Version: 1.15 ------------- - make (undocumented) startup() async Version: 1.14 ------------- - support resource urls Version: 1.12 ------------- - no longer allow to enforce custom "namespace" - no longer call it namespace but uniqueRandomID / scopeName - expose special objects as the global WL object - autoremove injected elements after onUnload has ben executed Version: 1.9 ------------- - automatically remove all entries added by injectElements Version: 1.8 ------------- - add injectElements Version: 1.7 ------------- - add injectCSS - add optional enforced namespace Version: 1.6 ------------- - added mutation observer to be able to inject into browser elements - use larger icons as fallback quicktext-4.1/api/WindowListener/README.md000066400000000000000000000003311410655052500204040ustar00rootroot00000000000000Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-WindowListener-API-to-convert-a-Legacy-Overlay-WebExtension-into-a-MailExtension-for-Thunderbird-78). quicktext-4.1/api/WindowListener/implementation.js000066400000000000000000001231221410655052500225140ustar00rootroot00000000000000/* * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * * Version: 1.54 * * Author: John Bieling (john@thunderbird.net) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Import some things we need. var { ExtensionCommon } = ChromeUtils.import( "resource://gre/modules/ExtensionCommon.jsm" ); var { ExtensionSupport } = ChromeUtils.import( "resource:///modules/ExtensionSupport.jsm" ); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var WindowListener = class extends ExtensionCommon.ExtensionAPI { log(msg) { if (this.debug) console.log("WindowListener API: " + msg); } getThunderbirdVersion() { let parts = Services.appinfo.version.split("."); return { major: parseInt(parts[0]), minor: parseInt(parts[1]), } } getCards(e) { // This gets triggered by real events but also manually by providing the outer window. // The event is attached to the outer browser, get the inner one. let doc; // 78,86, and 87+ need special handholding. *Yeah*. if (this.getThunderbirdVersion().major < 86) { let ownerDoc = e.document || e.target.ownerDocument; doc = ownerDoc.getElementById("html-view-browser").contentDocument; } else if (this.getThunderbirdVersion().major < 87) { let ownerDoc = e.document || e.target; doc = ownerDoc.getElementById("html-view-browser").contentDocument; } else { doc = e.document || e.target; } return doc.querySelectorAll("addon-card"); } // Add pref entry to 68 add68PrefsEntry(event) { let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID; // Get the best size of the icon (16px or bigger) let iconSizes = this.extension.manifest.icons ? Object.keys(this.extension.manifest.icons) : []; iconSizes.sort((a, b) => a - b); let bestSize = iconSizes.filter((e) => parseInt(e) >= 16).shift(); let icon = bestSize ? this.extension.manifest.icons[bestSize] : ""; let name = this.extension.manifest.name; let entry = icon ? event.target.ownerGlobal.MozXULElement.parseXULToFragment( `` ) : event.target.ownerGlobal.MozXULElement.parseXULToFragment( `` ); event.target.appendChild(entry); let noPrefsElem = event.target.querySelector('[disabled="true"]'); // using collapse could be undone by core, so we use display none // noPrefsElem.setAttribute("collapsed", "true"); noPrefsElem.style.display = "none"; event.target.ownerGlobal.document .getElementById(id) .addEventListener("command", this); } // Event handler for the addon manager, to update the state of the options button. handleEvent(e) { switch (e.type) { // 68 add-on options menu showing case "popupshowing": { this.add68PrefsEntry(e); } break; // 78/88 add-on options menu/button click case "click": { e.preventDefault(); e.stopPropagation(); let WL = {}; WL.extension = this.extension; WL.messenger = this.getMessenger(this.context); let w = Services.wm.getMostRecentWindow("mail:3pane"); w.openDialog( this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL ); } break; // 68 add-on options menu command case "command": { let WL = {}; WL.extension = this.extension; WL.messenger = this.getMessenger(this.context); e.target.ownerGlobal.openDialog( this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL ); } break; // update, ViewChanged and manual call for add-on manager options overlay default: { let cards = this.getCards(e); for (let card of cards) { // Setup either the options entry in the menu or the button //window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL)}); if (card.addon.id == this.extension.id) { let optionsMenu = (this.getThunderbirdVersion().major > 78 && this.getThunderbirdVersion().major < 88) || (this.getThunderbirdVersion().major == 78 && this.getThunderbirdVersion().minor < 10); if (optionsMenu) { // Options menu in 78.0-78.10 and 79-87 let addonOptionsLegacyEntry = card.querySelector( ".extension-options-legacy" ); if (card.addon.isActive && !addonOptionsLegacyEntry) { let addonOptionsEntry = card.querySelector( "addon-options panel-list panel-item[action='preferences']" ); addonOptionsLegacyEntry = card.ownerDocument.createElement( "panel-item" ); addonOptionsLegacyEntry.setAttribute( "data-l10n-id", "preferences-addon-button" ); addonOptionsLegacyEntry.classList.add( "extension-options-legacy" ); addonOptionsEntry.parentNode.insertBefore( addonOptionsLegacyEntry, addonOptionsEntry ); card .querySelector(".extension-options-legacy") .addEventListener("click", this); } else if (!card.addon.isActive && addonOptionsLegacyEntry) { addonOptionsLegacyEntry.remove(); } } else { // Add-on button in 88 let addonOptionsButton = card.querySelector( ".windowlistener-options-button" ); if (card.addon.isActive && !addonOptionsButton) { addonOptionsButton = card.ownerDocument.createElement("button"); addonOptionsButton.classList.add("windowlistener-options-button"); addonOptionsButton.classList.add("extension-options-button"); card.optionsButton.parentNode.insertBefore( addonOptionsButton, card.optionsButton ); card .querySelector(".windowlistener-options-button") .addEventListener("click", this); } else if (!card.addon.isActive && addonOptionsButton) { addonOptionsButton.remove(); } } } } } } } // Some tab/add-on-manager related functions getTabMail(window) { return window.document.getElementById("tabmail"); } // returns the outer browser, not the nested browser of the add-on manager // events must be attached to the outer browser getAddonManagerFromTab(tab) { if (tab.browser) { let win = tab.browser.contentWindow; if (win && win.location.href == "about:addons") { return win; } } } getAddonManagerFromWindow(window) { let tabMail = this.getTabMail(window); for (let tab of tabMail.tabInfo) { let win = this.getAddonManagerFromTab(tab); if (win) { return win; } } } setupAddonManager(managerWindow, forceLoad = false) { if (!managerWindow) { return; } if (!( managerWindow && managerWindow[this.uniqueRandomID] && managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners )) { managerWindow.document.addEventListener("ViewChanged", this); managerWindow.document.addEventListener("update", this); managerWindow.document.addEventListener("view-loaded", this); managerWindow[this.uniqueRandomID] = {}; managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true; } if (forceLoad) this.handleEvent(managerWindow); } getMessenger(context) { let apis = ["storage", "runtime", "extension", "i18n"]; function getStorage() { let localstorage = null; try { localstorage = context.apiCan.findAPIPath("storage"); localstorage.local.get = (...args) => localstorage.local.callMethodInParentProcess("get", args); localstorage.local.set = (...args) => localstorage.local.callMethodInParentProcess("set", args); localstorage.local.remove = (...args) => localstorage.local.callMethodInParentProcess("remove", args); localstorage.local.clear = (...args) => localstorage.local.callMethodInParentProcess("clear", args); } catch (e) { console.info("Storage permission is missing"); } return localstorage; } let messenger = {}; for (let api of apis) { switch (api) { case "storage": XPCOMUtils.defineLazyGetter(messenger, "storage", () => getStorage()); break; default: XPCOMUtils.defineLazyGetter(messenger, api, () => context.apiCan.findAPIPath(api) ); } } return messenger; } error(msg) { if (this.debug) console.error("WindowListener API: " + msg); } // async sleep function using Promise async sleep(delay) { let timer = Components.classes["@mozilla.org/timer;1"].createInstance( Components.interfaces.nsITimer ); return new Promise(function (resolve, reject) { let event = { notify: function (timer) { resolve(); }, }; timer.initWithCallback( event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT ); }); } getAPI(context) { // Track if this is the background/main context if (context.viewType != "background") throw new Error( "The WindowListener API may only be called from the background page." ); this.context = context; this.uniqueRandomID = "AddOnNS" + context.extension.instanceId; this.menu_addonPrefs_id = "addonPrefs"; this.registeredWindows = {}; this.pathToStartupScript = null; this.pathToShutdownScript = null; this.pathToOptionsPage = null; this.chromeHandle = null; this.chromeData = null; this.resourceData = null; this.openWindows = []; this.debug = context.extension.addonData.temporarilyInstalled; const aomStartup = Cc[ "@mozilla.org/addons/addon-manager-startup;1" ].getService(Ci.amIAddonManagerStartup); const resProto = Cc[ "@mozilla.org/network/protocol;1?name=resource" ].getService(Ci.nsISubstitutingProtocolHandler); let self = this; // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager. this.tabMonitor = { onTabTitleChanged(aTab) {}, onTabClosing(aTab) {}, onTabPersist(aTab) {}, onTabRestored(aTab) {}, onTabSwitched(aNewTab, aOldTab) { //self.setupAddonManager(self.getAddonManagerFromTab(aNewTab)); }, async onTabOpened(aTab) { if (aTab.browser) { if (!aTab.pageLoaded) { // await a location change if browser is not loaded yet await new Promise((resolve) => { let reporterListener = { QueryInterface: ChromeUtils.generateQI([ "nsIWebProgressListener", "nsISupportsWeakReference", ]), onStateChange() {}, onProgressChange() {}, onLocationChange( /* in nsIWebProgress*/ aWebProgress, /* in nsIRequest*/ aRequest, /* in nsIURI*/ aLocation ) { aTab.browser.removeProgressListener(reporterListener); resolve(); }, onStatusChange() {}, onSecurityChange() {}, onContentBlockingEvent() {}, }; aTab.browser.addProgressListener(reporterListener); }); } self.setupAddonManager(self.getAddonManagerFromTab(aTab)); } }, }; return { WindowListener: { async waitForMasterPassword() { // Wait until master password has been entered (if needed) while (!Services.logins.isLoggedIn) { self.log("Waiting for master password."); await self.sleep(1000); } self.log("Master password has been entered."); }, aDocumentExistsAt(uriString) { self.log( "Checking if document at <" + uriString + "> used in registration actually exists." ); try { let uriObject = Services.io.newURI(uriString); let content = Cu.readUTF8URI(uriObject); } catch (e) { Components.utils.reportError(e); return false; } return true; }, registerOptionsPage(optionsUrl) { self.pathToOptionsPage = optionsUrl.startsWith("chrome://") ? optionsUrl : context.extension.rootURI.resolve(optionsUrl); }, registerDefaultPrefs(defaultUrl) { let url = context.extension.rootURI.resolve(defaultUrl); let prefsObj = {}; prefsObj.Services = ChromeUtils.import( "resource://gre/modules/Services.jsm" ).Services; prefsObj.pref = function (aName, aDefault) { let defaults = Services.prefs.getDefaultBranch(""); switch (typeof aDefault) { case "string": return defaults.setStringPref(aName, aDefault); case "number": return defaults.setIntPref(aName, aDefault); case "boolean": return defaults.setBoolPref(aName, aDefault); default: throw new Error( "Preference <" + aName + "> has an unsupported type <" + typeof aDefault + ">. Allowed are string, number and boolean." ); } }; Services.scriptloader.loadSubScript(url, prefsObj, "UTF-8"); }, registerChromeUrl(data) { let chromeData = []; let resourceData = []; for (let entry of data) { if (entry[0] == "resource") resourceData.push(entry); else chromeData.push(entry); } if (chromeData.length > 0) { const manifestURI = Services.io.newURI( "manifest.json", null, context.extension.rootURI ); self.chromeHandle = aomStartup.registerChrome( manifestURI, chromeData ); } for (let res of resourceData) { // [ "resource", "shortname" , "path" ] let uri = Services.io.newURI( res[2], null, context.extension.rootURI ); resProto.setSubstitutionWithFlags( res[1], uri, resProto.ALLOW_CONTENT_ACCESS ); } self.chromeData = chromeData; self.resourceData = resourceData; }, registerWindow(windowHref, jsFile) { if (self.debug && !this.aDocumentExistsAt(windowHref)) { self.error( "Attempt to register an injector script for non-existent window: " + windowHref ); return; } if (!self.registeredWindows.hasOwnProperty(windowHref)) { // path to JS file can either be chrome:// URL or a relative URL let path = jsFile.startsWith("chrome://") ? jsFile : context.extension.rootURI.resolve(jsFile); self.registeredWindows[windowHref] = path; } else { self.error( "Window <" + windowHref + "> has already been registered" ); } }, registerStartupScript(aPath) { self.pathToStartupScript = aPath.startsWith("chrome://") ? aPath : context.extension.rootURI.resolve(aPath); }, registerShutdownScript(aPath) { self.pathToShutdownScript = aPath.startsWith("chrome://") ? aPath : context.extension.rootURI.resolve(aPath); }, openOptionsDialog(windowId) { let window = context.extension.windowManager.get(windowId, context) .window; let WL = {}; WL.extension = self.extension; WL.messenger = self.getMessenger(self.context); window.openDialog( self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", WL ); }, async startListening() { // load the registered startup script, if one has been registered // (mail3:pane may not have been fully loaded yet) if (self.pathToStartupScript) { let startupJS = {}; startupJS.WL = {}; startupJS.WL.extension = self.extension; startupJS.WL.messenger = self.getMessenger(self.context); try { if (self.pathToStartupScript) { Services.scriptloader.loadSubScript( self.pathToStartupScript, startupJS, "UTF-8" ); // delay startup until startup has been finished self.log( "Waiting for async startup() in <" + self.pathToStartupScript + "> to finish." ); if (startupJS.startup) { await startupJS.startup(); self.log( "startup() in <" + self.pathToStartupScript + "> finished" ); } else { self.log( "No startup() in <" + self.pathToStartupScript + "> found." ); } } } catch (e) { Components.utils.reportError(e); } } let urls = Object.keys(self.registeredWindows); if (urls.length > 0) { // Before registering the window listener, check which windows are already open self.openWindows = []; for (let window of Services.wm.getEnumerator(null)) { self.openWindows.push(window); } // Register window listener for all pre-registered windows ExtensionSupport.registerWindowListener( "injectListener_" + self.uniqueRandomID, { // React on all windows and manually reduce to the registered // windows, so we can do special actions when the main // messenger window is opened. //chromeURLs: Object.keys(self.registeredWindows), async onLoadWindow(window) { // Create add-on scope window[self.uniqueRandomID] = {}; // Special action #1: If this is the main messenger window if ( window.location.href == "chrome://messenger/content/messenger.xul" || window.location.href == "chrome://messenger/content/messenger.xhtml" ) { if (self.pathToOptionsPage) { if (self.getThunderbirdVersion().major < 78) { let element_addonPrefs = window.document.getElementById( self.menu_addonPrefs_id ); element_addonPrefs.addEventListener( "popupshowing", self ); } else { // Setup the options button/menu in the add-on manager, if it is already open. self.setupAddonManager( self.getAddonManagerFromWindow(window), true ); // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager. self .getTabMail(window) .registerTabMonitor(self.tabMonitor); window[self.uniqueRandomID].hasTabMonitor = true; } } } // Special action #2: If this page contains browser elements let browserElements = window.document.getElementsByTagName( "browser" ); if (browserElements.length > 0) { //register a MutationObserver window[ self.uniqueRandomID ]._mObserver = new window.MutationObserver(function ( mutations ) { mutations.forEach(async function (mutation) { if ( mutation.attributeName == "src" && self.registeredWindows.hasOwnProperty( mutation.target.getAttribute("src") ) ) { // When the MutationObserver callsback, the window is still showing "about:black" and it is going // to unload and then load the new page. Any eventListener attached to the window will be removed // so we cannot listen for the load event. We have to poll manually to learn when loading has finished. // On my system it takes 70ms. let loaded = false; for (let i = 0; i < 100 && !loaded; i++) { await self.sleep(100); let targetWindow = mutation.target.contentWindow.wrappedJSObject; if ( targetWindow && targetWindow.location.href == mutation.target.getAttribute("src") && targetWindow.document.readyState == "complete" ) { loaded = true; break; } } if (loaded) { let targetWindow = mutation.target.contentWindow.wrappedJSObject; // Create add-on scope targetWindow[self.uniqueRandomID] = {}; // Inject with isAddonActivation = false self._loadIntoWindow(targetWindow, false); } } }); }); for (let element of browserElements) { if ( self.registeredWindows.hasOwnProperty( element.getAttribute("src") ) ) { let targetWindow = element.contentWindow.wrappedJSObject; // Create add-on scope targetWindow[self.uniqueRandomID] = {}; // Inject with isAddonActivation = true self._loadIntoWindow(targetWindow, true); } else { // Window/Browser is not yet fully loaded, postpone injection via MutationObserver window[self.uniqueRandomID]._mObserver.observe( element, { attributes: true, childList: false, characterData: false, } ); } } } // Load JS into window self._loadIntoWindow( window, self.openWindows.includes(window) ); }, onUnloadWindow(window) { // Remove JS from window, window is being closed, addon is not shut down self._unloadFromWindow(window, false); }, } ); } else { self.error("Failed to start listening, no windows registered"); } }, }, }; } _loadIntoWindow(window, isAddonActivation) { if ( window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href) ) { try { let uniqueRandomID = this.uniqueRandomID; let extension = this.extension; // Add reference to window to add-on scope window[this.uniqueRandomID].window = window; window[this.uniqueRandomID].document = window.document; // Keep track of toolbarpalettes we are injecting into window[this.uniqueRandomID]._toolbarpalettes = {}; //Create WLDATA object window[this.uniqueRandomID].WL = {}; window[this.uniqueRandomID].WL.scopeName = this.uniqueRandomID; // Add helper function to inject CSS to WLDATA object window[this.uniqueRandomID].WL.injectCSS = function (cssFile) { let element; let v = parseInt(Services.appinfo.version.split(".").shift()); // using createElementNS in TB78 delays the insert process and hides any security violation errors if (v > 68) { element = window.document.createElement("link"); } else { let ns = window.document.documentElement.lookupNamespaceURI("html"); element = window.document.createElementNS(ns, "link"); } element.setAttribute("wlapi_autoinjected", uniqueRandomID); element.setAttribute("rel", "stylesheet"); element.setAttribute("href", cssFile); return window.document.documentElement.appendChild(element); }; // Add helper function to inject XUL to WLDATA object window[this.uniqueRandomID].WL.injectElements = function ( xulString, dtdFiles = [], debug = false ) { let toolbarsToResolve = []; function checkElements(stringOfIDs) { let arrayOfIDs = stringOfIDs.split(",").map((e) => e.trim()); for (let id of arrayOfIDs) { let element = window.document.getElementById(id); if (element) { return element; } } return null; } function localize(entity) { let msg = entity.slice("__MSG_".length, -2); return extension.localeData.localizeMessage(msg); } function injectChildren(elements, container) { if (debug) console.log(elements); for (let i = 0; i < elements.length; i++) { // take care of persists const uri = window.document.documentURI; for (const persistentNode of elements[i].querySelectorAll( "[persist]" )) { for (const persistentAttribute of persistentNode .getAttribute("persist") .trim() .split(" ")) { if ( Services.xulStore.hasValue( uri, persistentNode.id, persistentAttribute ) ) { persistentNode.setAttribute( persistentAttribute, Services.xulStore.getValue( uri, persistentNode.id, persistentAttribute ) ); } } } if ( elements[i].hasAttribute("insertafter") && checkElements(elements[i].getAttribute("insertafter")) ) { let insertAfterElement = checkElements( elements[i].getAttribute("insertafter") ); if (debug) console.log( elements[i].tagName + "#" + elements[i].id + ": insertafter " + insertAfterElement.id ); if ( debug && elements[i].id && window.document.getElementById(elements[i].id) ) { console.error( "The id <" + elements[i].id + "> of the injected element already exists in the document!" ); } elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID); insertAfterElement.parentNode.insertBefore( elements[i], insertAfterElement.nextSibling ); } else if ( elements[i].hasAttribute("insertbefore") && checkElements(elements[i].getAttribute("insertbefore")) ) { let insertBeforeElement = checkElements( elements[i].getAttribute("insertbefore") ); if (debug) console.log( elements[i].tagName + "#" + elements[i].id + ": insertbefore " + insertBeforeElement.id ); if ( debug && elements[i].id && window.document.getElementById(elements[i].id) ) { console.error( "The id <" + elements[i].id + "> of the injected element already exists in the document!" ); } elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID); insertBeforeElement.parentNode.insertBefore( elements[i], insertBeforeElement ); } else if ( elements[i].id && window.document.getElementById(elements[i].id) ) { // existing container match, dive into recursivly if (debug) console.log( elements[i].tagName + "#" + elements[i].id + " is an existing container, injecting into " + elements[i].id ); injectChildren( Array.from(elements[i].children), window.document.getElementById(elements[i].id) ); } else if (elements[i].localName === "toolbarpalette") { // These vanish from the document but still exist via the palette property if (debug) console.log(elements[i].id + " is a toolbarpalette"); let boxes = [ ...window.document.getElementsByTagName("toolbox"), ]; let box = boxes.find( (box) => box.palette && box.palette.id === elements[i].id ); let palette = box ? box.palette : null; if (!palette) { if (debug) console.log( `The palette for ${elements[i].id} could not be found, deferring to later` ); continue; } if (debug) console.log(`The toolbox for ${elements[i].id} is ${box.id}`); toolbarsToResolve.push(...box.querySelectorAll("toolbar")); toolbarsToResolve.push( ...window.document.querySelectorAll( `toolbar[toolboxid="${box.id}"]` ) ); for (let child of elements[i].children) { child.setAttribute("wlapi_autoinjected", uniqueRandomID); } window[uniqueRandomID]._toolbarpalettes[palette.id] = palette; injectChildren(Array.from(elements[i].children), palette); } else { // append element to the current container if (debug) console.log( elements[i].tagName + "#" + elements[i].id + ": append to " + container.id ); elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID); container.appendChild(elements[i]); } } } if (debug) console.log("Injecting into root document:"); let localizedXulString = xulString.replace( /__MSG_(.*?)__/g, localize ); injectChildren( Array.from( window.MozXULElement.parseXULToFragment( localizedXulString, dtdFiles ).children ), window.document.documentElement ); for (let bar of toolbarsToResolve) { let currentset = Services.xulStore.getValue( window.location, bar.id, "currentset" ); if (currentset) { bar.currentSet = currentset; } else if (bar.getAttribute("defaultset")) { bar.currentSet = bar.getAttribute("defaultset"); } } }; // Add extension object to WLDATA object window[this.uniqueRandomID].WL.extension = this.extension; // Add messenger object to WLDATA object window[this.uniqueRandomID].WL.messenger = this.getMessenger( this.context ); // Load script into add-on scope Services.scriptloader.loadSubScript( this.registeredWindows[window.location.href], window[this.uniqueRandomID], "UTF-8" ); window[this.uniqueRandomID].onLoad(isAddonActivation); } catch (e) { Components.utils.reportError(e); } } } _unloadFromWindow(window, isAddonDeactivation) { // unload any contained browser elements if ( window.hasOwnProperty(this.uniqueRandomID) && window[this.uniqueRandomID].hasOwnProperty("_mObserver") ) { window[this.uniqueRandomID]._mObserver.disconnect(); let browserElements = window.document.getElementsByTagName("browser"); for (let element of browserElements) { if (element.contentWindow) { this._unloadFromWindow( element.contentWindow.wrappedJSObject, isAddonDeactivation ); } } } if ( window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href) ) { // Remove this window from the list of open windows this.openWindows = this.openWindows.filter((e) => e != window); if (window[this.uniqueRandomID].onUnload) { try { // Call onUnload() window[this.uniqueRandomID].onUnload(isAddonDeactivation); } catch (e) { Components.utils.reportError(e); } } // Remove all auto injected objects let elements = Array.from( window.document.querySelectorAll( '[wlapi_autoinjected="' + this.uniqueRandomID + '"]' ) ); for (let element of elements) { element.remove(); } // Remove all autoinjected toolbarpalette items for (const palette of Object.values( window[this.uniqueRandomID]._toolbarpalettes )) { let elements = Array.from( palette.querySelectorAll( '[wlapi_autoinjected="' + this.uniqueRandomID + '"]' ) ); for (let element of elements) { element.remove(); } } } // Remove add-on scope, if it exists if (window.hasOwnProperty(this.uniqueRandomID)) { delete window[this.uniqueRandomID]; } } onShutdown(isAppShutdown) { if (isAppShutdown) { return; // the application gets unloaded anyway } // Unload from all still open windows let urls = Object.keys(this.registeredWindows); if (urls.length > 0) { for (let window of Services.wm.getEnumerator(null)) { //remove our entry in the add-on options menu if ( this.pathToOptionsPage && (window.location.href == "chrome://messenger/content/messenger.xul" || window.location.href == "chrome://messenger/content/messenger.xhtml") ) { if (this.getThunderbirdVersion().major < 78) { let element_addonPrefs = window.document.getElementById( this.menu_addonPrefs_id ); element_addonPrefs.removeEventListener("popupshowing", this); // Remove our entry. let entry = window.document.getElementById( this.menu_addonPrefs_id + "_" + this.uniqueRandomID ); if (entry) entry.remove(); // Do we have to unhide the noPrefsElement? if (element_addonPrefs.children.length == 1) { let noPrefsElem = element_addonPrefs.querySelector( '[disabled="true"]' ); noPrefsElem.style.display = "inline"; } } else { // Remove event listener for addon manager view changes let managerWindow = this.getAddonManagerFromWindow(window); if ( managerWindow && managerWindow[this.uniqueRandomID] && managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners ) { managerWindow.document.removeEventListener("ViewChanged", this); managerWindow.document.removeEventListener("view-loaded", this); managerWindow.document.removeEventListener("update", this); let cards = this.getCards(managerWindow); if (this.getThunderbirdVersion().major < 88) { // Remove options menu in 78-87 for (let card of cards) { let addonOptionsLegacyEntry = card.querySelector( ".extension-options-legacy" ); if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove(); } } else { // Remove options button in 88 for (let card of cards) { if (card.addon.id == this.extension.id) { let addonOptionsButton = card.querySelector( ".windowlistener-options-button" ); if (addonOptionsButton) addonOptionsButton.remove(); break; } } } } // Remove tabmonitor if (window[this.uniqueRandomID].hasTabMonitor) { this.getTabMail(window).unregisterTabMonitor(this.tabMonitor); window[this.uniqueRandomID].hasTabMonitor = false; } } } // if it is app shutdown, it is not just an add-on deactivation this._unloadFromWindow(window, !isAppShutdown); } // Stop listening for new windows. ExtensionSupport.unregisterWindowListener( "injectListener_" + this.uniqueRandomID ); } // Load registered shutdown script let shutdownJS = {}; shutdownJS.extension = this.extension; try { if (this.pathToShutdownScript) Services.scriptloader.loadSubScript( this.pathToShutdownScript, shutdownJS, "UTF-8" ); } catch (e) { Components.utils.reportError(e); } // Extract all registered chrome content urls let chromeUrls = []; if (this.chromeData) { for (let chromeEntry of this.chromeData) { if (chromeEntry[0].toLowerCase().trim() == "content") { chromeUrls.push("chrome://" + chromeEntry[1] + "/"); } } } // Unload JSMs of this add-on const rootURI = this.extension.rootURI.spec; for (let module of Cu.loadedModules) { if ( module.startsWith(rootURI) || (module.startsWith("chrome://") && chromeUrls.find((s) => module.startsWith(s))) ) { this.log("Unloading: " + module); Cu.unload(module); } } // Flush all caches Services.obs.notifyObservers(null, "startupcache-invalidate"); this.registeredWindows = {}; if (this.resourceData) { const resProto = Cc[ "@mozilla.org/network/protocol;1?name=resource" ].getService(Ci.nsISubstitutingProtocolHandler); for (let res of this.resourceData) { // [ "resource", "shortname" , "path" ] resProto.setSubstitution(res[1], null); } } if (this.chromeHandle) { this.chromeHandle.destruct(); this.chromeHandle = null; } } }; quicktext-4.1/api/WindowListener/schema.json000066400000000000000000000055631410655052500212740ustar00rootroot00000000000000[ { "namespace": "WindowListener", "functions": [ { "name": "registerDefaultPrefs", "type": "function", "parameters": [ { "name": "aPath", "type": "string", "description": "Relative path to the default file." } ] }, { "name": "registerOptionsPage", "type": "function", "parameters": [ { "name": "aPath", "type": "string", "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu." } ] }, { "name": "registerChromeUrl", "type": "function", "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)", "parameters": [ { "name": "data", "type": "array", "items": { "type": "array", "items" : { "type": "string" } }, "description": "Array of manifest url definitions (content, locale, resource)" } ] }, { "name": "waitForMasterPassword", "type": "function", "async": true, "parameters": [] }, { "name": "openOptionsDialog", "type": "function", "parameters": [ { "name": "windowId", "type": "integer", "description": "Id of the window the dialog should be opened from." } ] }, { "name": "startListening", "type": "function", "async": true, "parameters": [] }, { "name": "registerWindow", "type": "function", "parameters": [ { "name": "windowHref", "type": "string", "description": "Url of the window, which should be listen for." }, { "name": "jsFile", "type": "string", "description": "Path to the JavaScript file, which should be loaded into the window." } ] }, { "name": "registerStartupScript", "type": "function", "parameters": [ { "name": "aPath", "type": "string", "description": "Path to a JavaScript file, which should be executed on add-on startup. The script will be executed after the main application window has been sucessfully loaded." } ] }, { "name": "registerShutdownScript", "type": "function", "parameters": [ { "name": "aPath", "type": "string", "description": "Path to a JavaScript file, which should be executed on add-on shutdown." } ] } ] } ] quicktext-4.1/background.js000066400000000000000000000071151410655052500160630ustar00rootroot00000000000000(async () => { // Define default prefs. let defaultPrefs = { "counter": 0, "templateFolder": "", "defaultImport": "", "menuCollapse": true, "toolbar": true, "popup": false, "keywordKey": "Tab", "shortcutModifier": "alt", "shortcutTypeAdv": false, "collapseState": "" }; await preferences.init(defaultPrefs); // Migrate legacy prefs using the LegacyPrefs API. const legacyPrefBranch = "extensions.quicktext."; const prefNames = Object.keys(defaultPrefs); for (let prefName of prefNames) { let legacyValue = await messenger.LegacyPrefs.getUserPref(`${legacyPrefBranch}${prefName}`); if (legacyValue !== null) { console.log(`Migrating legacy preference <${legacyPrefBranch}${prefName}> = <${legacyValue}>.`); // Store the migrated value in local storage. // Check out the MDN documentation at // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage // or use preference.js bundled with this API preferences.setPref(prefName, legacyValue); // Clear the legacy value. messenger.LegacyPrefs.clearUserPref(`${legacyPrefBranch}${prefName}`); } } // Allow to set defaultImport from user_prefs let defaultImportOverride = await messenger.LegacyPrefs.getUserPref(`${legacyPrefBranch}defaultImportOverride`); if (defaultImportOverride !== null) { preferences.setPref("defaultImport", defaultImportOverride); } // Allow to override templateFolder from user_prefs let templateFolderOverride = await messenger.LegacyPrefs.getUserPref(`${legacyPrefBranch}templateFolderOverride`); if (templateFolderOverride !== null) { preferences.setPref("templateFolder", templateFolderOverride); } messenger.NotifyTools.onNotifyBackground.addListener(async (info) => { switch (info.command) { case "setPref": preferences.setPref(info.pref, info.value); break; case "getPref": return await preferences.getPref(info.pref); break; } }); // load add-on via WindowListener API messenger.WindowListener.registerChromeUrl([ ["content", "quicktext", "chrome/content/"], ["resource", "quicktext", "chrome/"], ["locale", "quicktext", "de", "chrome/locale/de/"], ["locale", "quicktext", "pt-BR", "chrome/locale/pt_BR/"], ["locale", "quicktext", "en-US", "chrome/locale/en-US/"], ["locale", "quicktext", "es", "chrome/locale/es/"], ["locale", "quicktext", "fr", "chrome/locale/fr/"], ["locale", "quicktext", "hu", "chrome/locale/hu/"], ["locale", "quicktext", "ja", "chrome/locale/ja/"], ["locale", "quicktext", "ru", "chrome/locale/ru/"], ["locale", "quicktext", "sv-SE", "chrome/locale/sv-SE/"], ["locale", "quicktext", "cs", "chrome/locale/cs/"], ]); messenger.WindowListener.registerOptionsPage("chrome://quicktext/content/addonoptions.xhtml") messenger.WindowListener.registerWindow( "chrome://messenger/content/messengercompose/messengercompose.xhtml", "chrome://quicktext/content/scripts/messengercompose.js"); messenger.WindowListener.registerWindow( "chrome://messenger/content/messenger.xhtml", "chrome://quicktext/content/scripts/messenger.js"); browser.composeAction.onClicked.addListener(tab => { messenger.WindowListener.openOptionsDialog(tab.windowId); }); browser.browserAction.onClicked.addListener(tab => { messenger.WindowListener.openOptionsDialog(tab.windowId); }); messenger.WindowListener.startListening(); })(); quicktext-4.1/chrome/000077500000000000000000000000001410655052500146575ustar00rootroot00000000000000quicktext-4.1/chrome/content/000077500000000000000000000000001410655052500163315ustar00rootroot00000000000000quicktext-4.1/chrome/content/addonoptions.xhtml000066400000000000000000000007121410655052500221100ustar00rootroot00000000000000 \n"; } } buffer += ""; this.writeFile(aFile, buffer); } , exportTemplatesToFile: function(aFile) { var buffer = "\n\n\ttemplates\n"; for (var i = 0; i < this.mGroup.length; i++) { // Only export templates which have not been auto imported. if (this.mGroup[i].type == 0) { if (this.mTexts[i]) { buffer += "\t\n\t\t<![CDATA["+ this.mGroup[i].name +"]]>\n\t\t\n"; for (var j = 0; j < this.mTexts[i].length; j++) { var text = this.mTexts[i][j]; buffer += "\t\t\t\n\t\t\t\t\n"; if (text.keyword != "") buffer += "\t\t\t\t\n"; if (text.subject != "") buffer += "\t\t\t\t\n"; if (text.text != "") buffer += "\t\t\t\t\n"; if (text.attachments != "") buffer += "\t\t\t\t\n"; // There seems to be no use to write dynamically gathered header informations from the last use of a template to the file // var headerLength = 0; // if (headerLength = text.getHeaderLength() > 0) // { // buffer += "\t\t\t\t\n"; // for (var k = 0; k < headerLength; k++) // { // var header = text.getHeader(k); // buffer += "\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n"; // } // buffer += "\t\t\t\t
\n"; // } buffer += "\t\t\t
\n"; } buffer += "\t\t
\n\t
\n"; } } } buffer += "
"; this.writeFile(aFile, buffer); } , importFromFile: function(aFile, aType, aBefore, aEditingMode) { var start = this.mGroup.length; var data = this.readFile(aFile); this.parseImport(data, aType, aBefore, aEditingMode); } , importFromHTTPFile: function(aURI, aType, aBefore, aEditingMode) { var req = new XMLHttpRequest(); req.open('GET', aURI, true); req.mQuicktext = this; req.mType = aType; req.mBefore = aBefore; req.mEditingMode = aEditingMode req.onload = function(event) { var self = event.target; if (self.status == 200) { if (typeof self.mQuicktext != 'undefined') { self.mQuicktext.parseImport(self.responseText, self.mType, self.mBefore, self.mEditingMode); self.mQuicktext.notifyObservers("updatesettings", ""); } else debug('Something strange has happen!'); } } req.send(null); } , parseImport: function(aData, aType, aBefore, aEditingMode) { var parser = new DOMParser(); var dom = parser.parseFromString(aData, "text/xml"); var version = dom.documentElement.getAttribute("version"); var group = []; var texts = []; var scripts = []; switch (version) { case "2": var filetype = this.getTagValue(dom.documentElement, "filetype"); switch (filetype) { case "scripts": var elems = dom.documentElement.getElementsByTagName("script"); for (var i = 0; i < elems.length; i++) { var tmp = new wzQuicktextScript(); tmp.name = this.getTagValue(elems[i], "name"); tmp.script = this.getTagValue(elems[i], "body"); tmp.type = aType; scripts.push(tmp); } break; case "": case "templates": var elems = dom.documentElement.getElementsByTagName("menu"); for (var i = 0; i < elems.length; i++) { var tmp = new wzQuicktextGroup(); tmp.name = this.getTagValue(elems[i], "title"); tmp.type = aType; group.push(tmp); var subTexts = []; var textsNodes = elems[i].getElementsByTagName("texts"); if (textsNodes.length > 0) { var subElems = textsNodes[0].getElementsByTagName("text"); for (var j = 0; j < subElems.length; j++) { var tmp = new wzQuicktextTemplate(); tmp.name = this.getTagValue(subElems[j], "name"); tmp.text = this.getTagValue(subElems[j], "body"); tmp.shortcut = subElems[j].getAttribute("shortcut"); tmp.type = subElems[j].getAttribute("type"); tmp.keyword = this.getTagValue(subElems[j], "keyword"); tmp.subject = this.getTagValue(subElems[j], "subject"); tmp.attachments = this.getTagValue(subElems[j], "attachments"); // There seems to be no use to read dynamically gathered header informations from the last use of a template from the file // var headersTag = subElems[j].getElementsByTagName("headers"); // if (headersTag.length > 0) // { // var headers = headersTag[0].getElementsByTagName("header"); // for (var k = 0; k < headers.length; k++) // tmp.addHeader(this.getTagValue(headers[k], "type"), this.getTagValue(headers[k], "value")); // } subTexts.push(tmp); } } texts.push(subTexts); } break; default: // Alert the user that the importer don't understand the filetype break; } break; case null: // When the version-number not is set it is version 1. var elems = dom.documentElement.getElementsByTagName("menu"); for (var i = 0; i < elems.length; i++) { var tmp = new wzQuicktextGroup(); tmp.name = elems[i].getAttribute("title"); tmp.type = aType; group.push(tmp); var subTexts = []; var subElems = elems[i].getElementsByTagName("text"); for (var j = 0; j < subElems.length; j++) { var tmp = new wzQuicktextTemplate(); tmp.name = subElems[j].getAttribute("title"); tmp.text = subElems[j].firstChild.nodeValue; tmp.shortcut = subElems[j].getAttribute("shortcut"); tmp.type = subElems[j].getAttribute("type"); tmp.keyword = subElems[j].getAttribute("keyword"); tmp.subject = subElems[j].getAttribute("subject"); subTexts.push(tmp); } texts.push(subTexts); } break; default: // Alert the user that there version of Quicktext can't import the file, need to upgrade return; } if (scripts.length > 0) { if (aBefore) { scripts.reverse(); if (!aEditingMode) for (var i = 0; i < scripts.length; i++) this.mScripts.unshift(scripts[i]); for (var i = 0; i < scripts.length; i++) this.mEditingScripts.unshift(scripts[i]); } else { if (!aEditingMode) for (var i = 0; i < scripts.length; i++) this.mScripts.push(scripts[i]); for (var i = 0; i < scripts.length; i++) this.mEditingScripts.push(scripts[i]); } } if (group.length > 0 && texts.length > 0) { if (aBefore) { group.reverse(); texts.reverse(); if (!aEditingMode) { for (var i = 0; i < group.length; i++) this.mGroup.unshift(group[i]); for (var i = 0; i < texts.length; i++) this.mTexts.unshift(texts[i]); } for (var i = 0; i < group.length; i++) this.mEditingGroup.unshift(group[i]); for (var i = 0; i < texts.length; i++) this.mEditingTexts.unshift(texts[i]); } else { if (!aEditingMode) { for (var i = 0; i < group.length; i++) this.mGroup.push(group[i]); for (var i = 0; i < texts.length; i++) this.mTexts.push(texts[i]); } for (var i = 0; i < group.length; i++) this.mEditingGroup.push(group[i]); for (var i = 0; i < texts.length; i++) this.mEditingTexts.push(texts[i]); } } } , getTagValue: function(aElem, aTag) { var tagElem = aElem.getElementsByTagName(aTag); if (tagElem.length > 0) return tagElem[0].firstChild.nodeValue; return ""; } , removeIllegalChars: function(aStr) { return aStr.replace(new RegExp("["+ kIllegalChars +"]", 'g'), ''); } , /* * OBSERVERS */ addObserver: function(aObserver) { this.mObserverList.push(aObserver); } , removeObserver: function(aObserver) { for (var i = 0; i < this.mObserverList.length; i++) { if (this.mObserverList[i] == aObserver) this.mObserverList.splice(i, 1); } } , notifyObservers: function(aTopic, aData) { for (var i = 0; i < this.mObserverList.length; i++) this.mObserverList[i].observe(this, aTopic, aData); } } var debug = kDebug ? function(m) {dump("\t *** wzQuicktext: " + m + "\n");} : function(m) {}; function TrimString(aStr) { if (!aStr) return ""; return aStr.replace(/(^\s+)|(\s+$)/g, '') } Services.scriptloader.loadSubScript("chrome://quicktext/content/notifyTools/notifyTools.js", gQuicktext, "UTF-8"); quicktext-4.1/chrome/content/modules/wzQuicktextGroup.jsm000066400000000000000000000012431410655052500240730ustar00rootroot00000000000000var EXPORTED_SYMBOLS = ["wzQuicktextGroup"]; const kDebug = true; function wzQuicktextGroup() { this.mName = ""; this.mType = ""; } wzQuicktextGroup.prototype = { get name() { return this.mName; }, set name(aName) { if (typeof aName != 'undefined') return this.mName = aName; } , get type() { return this.mType; }, set type(aType) { if (typeof aType != 'undefined') return this.mType = aType; } , clone: function() { var newGroup = new wzQuicktextGroup(); newGroup.name = this.mName; newGroup.type = this.mType; return newGroup; } } var debug = kDebug ? function(m) {dump("\t *** wzQuicktext: " + m + "\n");} : function(m) {}; quicktext-4.1/chrome/content/modules/wzQuicktextHeader.jsm000066400000000000000000000013001410655052500241610ustar00rootroot00000000000000var EXPORTED_SYMBOLS = ["wzQuicktextHeader"]; const kDebug = true; function wzQuicktextHeader() { this.mType = ""; this.mValue = ""; } wzQuicktextHeader.prototype = { get type() { return this.mType; }, set type(aType) { if (typeof aType != 'undefined') return this.mType = aType; } , get value() { return this.mValue; }, set value(aValue) { if (typeof aValue != 'undefined') return this.mValue = aValue; } , clone: function() { var newHeader = new wzQuicktextHeader(); newHeader.type = this.mType; newHeader.value = this.mValue; return newHeader; } } var debug = kDebug ? function(m) {dump("\t *** wzQuicktext: " + m + "\n");} : function(m) {}; quicktext-4.1/chrome/content/modules/wzQuicktextScript.jsm000066400000000000000000000015761410655052500242540ustar00rootroot00000000000000var EXPORTED_SYMBOLS = ["wzQuicktextScript"]; const kDebug = true; function wzQuicktextScript() { this.mName = ""; this.mScript = ""; this.mType = 0; } wzQuicktextScript.prototype = { get name() { return this.mName; }, set name(aName) { if (typeof aName != 'undefined') return this.mName = aName; } , get script() { return this.mScript; }, set script(aScript) { if (typeof aScript != 'undefined') return this.mScript = aScript; } , get type() { return this.mType; }, set type(aType) { if (typeof aType != 'undefined') return this.mType = aType; } , clone: function() { var newScript = new wzQuicktextScript(); newScript.name = this.mName; newScript.script = this.mScript; newScript.type = this.mType; return newScript; } } var debug = kDebug ? function(m) {dump("\t *** wzQuicktext: " + m + "\n");} : function(m) {}; quicktext-4.1/chrome/content/modules/wzQuicktextTemplate.jsm000066400000000000000000000046751410655052500245660ustar00rootroot00000000000000var EXPORTED_SYMBOLS = ["wzQuicktextTemplate"]; var { wzQuicktextHeader } = ChromeUtils.import("chrome://quicktext/content/modules/wzQuicktextHeader.jsm"); const kDebug = true; function wzQuicktextTemplate() { this.mName = ""; this.mText = ""; this.mShortcut = ""; this.mType = ""; this.mKeyword = ""; this.mSubject = ""; this.mAttachments = ""; this.mHeaders = []; } wzQuicktextTemplate.prototype = { get name() { return this.mName; }, set name(aName) { if (typeof aName != 'undefined') return this.mName = aName; } , get text() { return this.mText; }, set text(aText) { if (typeof aText != 'undefined') return this.mText = aText; } , get shortcut() { return this.mShortcut; }, set shortcut(aShortcut) { if (typeof aShortcut != 'undefined') return this.mShortcut = aShortcut; } , get type() { return this.mType; }, set type(aType) { if (typeof aType != 'undefined') return this.mType = aType; } , get keyword() { return this.mKeyword; }, set keyword(aKeyword) { if (typeof aKeyword != 'undefined') return this.mKeyword = aKeyword; } , get subject() { return this.mSubject; }, set subject(aSubject) { if (typeof aSubject != 'undefined') return this.mSubject = aSubject; } , get attachments() { return this.mAttachments; }, set attachments(aAttachments) { if (typeof aAttachments != 'undefined') return this.mAttachments = aAttachments; } , getHeader: function (aIndex) { return this.mHeaders[aIndex]; } , addHeader: function (aType, aValue) { var tmp = new wzQuicktextHeader(); tmp.type = aType; tmp.value = aValue; this.mHeaders.push(tmp); } , removeHeader: function (aIndex) { this.mHeaders.splice(aIndex, 0); } , removeHeaders: function () { this.mHeaders = []; } , getHeaderLength: function() { return this.mHeaders.length; } , clone: function() { var newTemplate = new wzQuicktextTemplate(); newTemplate.name = this.mName; newTemplate.text = this.mText; newTemplate.shortcut = this.mShortcut; newTemplate.type = this.mType; newTemplate.keyword = this.mKeyword; newTemplate.subject = this.mSubject; newTemplate.attachments = this.mAttachments; for (var i = 0; i < this.mHeaders.length; i++) newTemplate.addHeader(this.mHeaders[i].type, this.mHeaders[i].value); return newTemplate; } } var debug = kDebug ? function(m) {dump("\t *** wzQuicktext: " + m + "\n");} : function(m) {}; quicktext-4.1/chrome/content/modules/wzQuicktextVar.jsm000077500000000000000000001165611410655052500235440ustar00rootroot00000000000000var EXPORTED_SYMBOLS = ["wzQuicktextVar"]; var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { quicktextUtils } = ChromeUtils.import("chrome://quicktext/content/modules/utils.jsm"); var { gQuicktext } = ChromeUtils.import("chrome://quicktext/content/modules/wzQuicktext.jsm"); var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); try { var { cardbookRepository } = ChromeUtils.import("chrome://cardbook/content/cardbookRepository.js"); } catch(e) {} const kDebug = true; const persistentTags = ['COUNTER', 'ORGATT', 'ORGHEADER', 'VERSION']; const allowedTags = ['ATT', 'CLIPBOARD', 'COUNTER', 'DATE', 'FILE', 'IMAGE', 'FROM', 'INPUT', 'ORGATT', 'ORGHEADER', 'SCRIPT', 'SUBJECT', 'TEXT', 'TIME', 'TO', 'URL', 'VERSION', 'SELECTION', 'HEADER']; function streamListener(aInspector) { var newStreamListener = { mAttachments: [], mHeaders: [], onStartRequest : function (aRequest, aContext) { this.mAttachments = []; this.mHeaders = []; var channel = aRequest.QueryInterface(Components.interfaces.nsIChannel); channel.URI.QueryInterface(Components.interfaces.nsIMsgMailNewsUrl); channel.URI.msgHeaderSink = this; // adds this header sink interface to the channel }, onStopRequest : function (aRequest, aContext, aStatusCode) { aInspector.exitNestedEventLoop(); }, onDataAvailable : function (aRequest, aContext, aInputStream, aOffset, aCount) {}, onStartHeaders: function() {}, onEndHeaders: function() {}, processHeaders: function(aHeaderNameEnumerator, aHeaderValueEnumerator, aDontCollectAddress) { while (aHeaderNameEnumerator.hasMore()) this.mHeaders.push({name:aHeaderNameEnumerator.getNext().toLowerCase(), value:aHeaderValueEnumerator.getNext()}); }, handleAttachment: function(aContentType, aUrl, aDisplayName, aUri, aIsExternalAttachment) { if (aContentType == "text/html") return; this.mAttachments.push({contentType:aContentType, url:aUrl, displayName:aDisplayName, uri:aUri, isExternal:aIsExternalAttachment}); }, onEndAllAttachments: function() {}, onEndMsgDownload: function(aUrl) {}, onEndMsgHeaders: function(aUrl) {}, onMsgHasRemoteContent: function(aMsgHdr) {}, getSecurityInfo: function() {}, setSecurityInfo: function(aSecurityInfo) {}, getDummyMsgHeader: function() {}, QueryInterface : function(aIID) { if (aIID.equals(Components.interfaces.nsIStreamListener) || aIID.equals(Components.interfaces.nsIMsgHeaderSink) || aIID.equals(Components.interfaces.nsISupports)) return this; throw Components.results.NS_NOINTERFACE; return 0; } }; return newStreamListener; } function wzQuicktextVar() { this.mData = {}; this.mWindow = null; // Need the Main Quicktext component this.mQuicktext = gQuicktext; } wzQuicktextVar.prototype = { init: function(aWindow) { // Save the window we are in this.mWindow = aWindow; // New mail so we need to destroy all tag-data this.mData = {}; } , cleanTagData: function() { // Just save some of the tag-data. var tmpData = {}; for (var i in this.mData) { if (persistentTags.indexOf(i) > -1) tmpData[i] = this.mData[i]; } this.mData = tmpData; } , parse: async function(aStr, aType) { // Reparse the text until there is no difference in the text // or that we parse 100 times (so we don't make an infinitive loop) var oldStr; var count = 0; do { count++; oldStr = aStr; aStr = await this.parseText(aStr, aType); } while (aStr != oldStr && count < 20); return aStr; } , parseText: async function(aStr, aType) { var tags = this.getTags(aStr); // If we don't find any tags there will be no changes to the string so return. if (tags.length == 0) return aStr; // Replace all tags with there right contents for (var i = 0; i < tags.length; i++) { var value = ""; var variable_limit = -1; switch (tags[i].tagName.toLowerCase()) { case 'att': case 'clipboard': case 'selection': case 'counter': case 'date': case 'subject': case 'time': case 'version': case 'orgatt': variable_limit = 0; break; case 'file': case 'image': case 'from': case 'input': case 'orgheader': case 'script': case 'to': case 'url': variable_limit = 1; break; case 'text': case 'header': variable_limit = 2; break; } // if the method "get_[tagname]" exists and there is enough arguments we call it if (typeof this["get_"+ tags[i].tagName.toLowerCase()] == "function" && variable_limit >= 0 && tags[i].variables.length >= variable_limit) { // these tags need different behaviour if added in "text" or "html" mode if ( tags[i].tagName.toLowerCase() == "image" || tags[i].tagName.toLowerCase() == "clipboard" || tags[i].tagName.toLowerCase() == "selection") { value = await this["get_"+ tags[i].tagName.toLowerCase()](tags[i].variables, aType); } else { value = await this["get_"+ tags[i].tagName.toLowerCase()](tags[i].variables); } } aStr = this.replaceText(tags[i].tag, value, aStr); } return aStr; } , getTags: function(aStr) { // We only get the beginning of the tag. // This is because we want to handle recursive use of tags. var rexp = new RegExp("\\[\\[(("+ allowedTags.join("|") +")(\\_[a-z]+)?)", "ig"); var results = []; var result = null; while ((result = rexp.exec(aStr))) results.push(result); // If we don't found any tags we return if (results.length == 0) return []; // Take care of the tags starting with the last one var hits = []; results.reverse(); var strLen = aStr.length; for (var i = 0; i < results.length; i++) { var tmpHit = {}; tmpHit.tag = results[i][0]; tmpHit.variables = []; // if the tagname contains a "_"-char that means // that is an old tag and we need to translate it // to a tagname and a variable var pos = results[i][1].indexOf("_"); if (pos > 0) { tmpHit.variables.push(results[i][1].substr(pos+1).toLowerCase()); tmpHit.tagName = results[i][1].substring(0,pos); } else tmpHit.tagName = results[i][1]; // Get the end of the starttag pos = results[i].index + results[i][1].length + 2; // If the tag ended here we're done if (aStr.substr(pos, 2) == "]]") { tmpHit.tag += "]]"; hits = this.addTag(hits, tmpHit); } // If there is arguments we get them else if (aStr[pos] == "=") { // We go through until we find ]] but we must have went // through the same amount of [ and ] before. So if there // is an tag in the middle we just jump over it. pos++; var bracketCount = 0; var ready = false; var vars = ""; while (!ready && pos < strLen) { if (aStr[pos] == "[") bracketCount++; if (aStr[pos] == "]") { bracketCount--; if (bracketCount == -1 && aStr[pos+1] == "]") { ready = true; break; } } vars += aStr[pos]; pos++; } // If we found the end we parses the arguments if (ready) { tmpHit.tag += "="+ vars +"]]"; vars = vars.split("|"); for (var j = 0; j < vars.length; j++) tmpHit.variables.push(vars[j]); // Adds the tag hits = this.addTag(hits, tmpHit); } } // We don't want to go over this tag again strLen = results[i].index; } hits.reverse(); return hits; } , // Checks if the tag isn't added before. // We just want to handle all unique tags once addTag: function(aTags, aNewTag) { for (var i = 0; i < aTags.length; i++) if (aTags[i].tag == aNewTag.tag) return aTags; aTags.push(aNewTag); return aTags; } , // The get-functions takes the data from the process-functions and // returns string depending of what aVariables is get_file: function(aVariables) { return this.process_file(aVariables); } , get_image: function(aVariables, aType) { if (aType == 1) { // image tag may only be added in html mode return this.process_image_content(aVariables); } else { return ""; } } , get_text: function(aVariables) { return this.process_text(aVariables); } , get_script: function(aVariables) { return this.process_script(aVariables); } , get_att: function(aVariables) { var data = this.process_att(aVariables); if (data.length > 0) { var value = []; for (var i in data) { if (aVariables[0] == "full") value.push(data[i][0] +" ("+ this.niceFileSize(data[i][1]) +")"); else value.push(data[i][0]); } if (aVariables.length < 2) aVariables[1] = ", "; return TrimString(value.join(aVariables[1].replace(/\\n/g, "\n").replace(/\\t/g, "\t"))); } return ""; } , get_input: function(aVariables) { var data = this.process_input(aVariables); if (typeof data[aVariables[0]] != "undefined") return data[aVariables[0]]; return ""; } , get_header: function(aVariables) { gQuicktext.mCurrentTemplate.addHeader(aVariables[0], aVariables[1]); // return an empty string, to remove the header tags from the body. return ""; } , get_clipboard: function(aVariables, aType) { return TrimString(this.process_clipboard(aVariables, aType)); } , get_selection: function(aVariables, aType) { return this.process_selection(aVariables, aType); } , get_from: function(aVariables) { var data = this.process_from(aVariables); if (typeof data[aVariables[0]] != 'undefined') return TrimString(data[aVariables[0]]); return ""; } , get_to: function(aVariables) { var data = this.process_to(aVariables); if (typeof data[aVariables[0]] != 'undefined') { // use ", " as default seperator let mainSep = (aVariables.length > 1) ? aVariables[1].replace(/\\n/g, "\n").replace(/\\t/g, "\t") : ", "; let lastSep = (aVariables.length > 2) ? aVariables[2].replace(/\\n/g, "\n").replace(/\\t/g, "\t") : mainSep; // clone the data, so we can work on it without mod the source object let entries = data[aVariables[0]].slice(0); let last = entries.pop(); // build the final string let all = []; if (entries.length > 0) all.push(entries.join(mainSep)); all.push(last); return all.join(lastSep); } else return ""; } , get_url: async function(aVariables) { return await this.process_url(aVariables); } , get_version: function(aVariables) { var data = this.process_version(aVariables); if (aVariables.length < 1) aVariables[0] = "full"; if (typeof data[aVariables[0]] != 'undefined') return data[aVariables[0]]; return ""; } , get_counter: async function(aVariables) { return await this.process_counter(aVariables); } , get_subject: function(aVariables) { return this.process_subject(aVariables); } , get_date: function(aVariables) { var data = this.process_date(aVariables); if (aVariables.length < 1) aVariables[0] = "short"; if (typeof data[aVariables[0]] != 'undefined') return data[aVariables[0]]; return ""; } , get_time: function(aVariables) { var data = this.process_time(aVariables); if (aVariables.length < 1) aVariables[0] = "noseconds"; if (typeof data[aVariables[0]] != 'undefined') return data[aVariables[0]]; return ""; } , get_orgheader: function(aVariables) { var data = this.process_orgheader(aVariables); aVariables[0] = aVariables[0].toLowerCase(); if (typeof data[aVariables[0]] != 'undefined') { if (aVariables.length < 2) aVariables[1] = ", "; return data[aVariables[0]].join(aVariables[1].replace(/\\n/g, "\n").replace(/\\t/g, "\t")); } else return ""; } , get_orgatt: function(aVariables) { var data = this.process_orgatt(aVariables); if (typeof data != 'undefined') { if (aVariables.length == 0) aVariables[0] = ", "; return data['displayName'].join(aVariables[0].replace(/\\n/g, "\n").replace(/\\t/g, "\t")); } return ""; } , // These process functions get the data and mostly saves it // in this.mData so if the data is requested again it is quick process_file: function(aVariables) { if (aVariables.length > 0 && aVariables[0] != "") { // Tries to open the file and returning the content var fp = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile); try { aVariables[0] = this.mQuicktext.parseFilePath(aVariables[0]); fp.initWithPath(aVariables[0]); let content = this.mQuicktext.readFile(fp); if (aVariables.length > 1 && aVariables[1].includes("strip_html_comments")) { return content.replace(/)/g, ''); } return content; } catch(e) { Components.utils.reportError(e); } } return ""; } , process_image_content: function(aVariables) { let rv = ""; if (aVariables.length > 0 && aVariables[0] != "") { let mode = (aVariables.length > 1 && "src" == aVariables[1].toString().toLowerCase()) ? "src" : "tag"; // Tries to open the file and returning the content try { let fp = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile); fp.initWithPath(this.mQuicktext.parseFilePath(aVariables[0])); let rawContent = this.mQuicktext.readBinaryFile(fp); let decoder = new TextDecoder('utf-8'); let content = this.mWindow.btoa(rawContent); let type = this.mQuicktext.getTypeFromExtension(fp); let src = "data:" + type + ";filename=" + fp.leafName + ";base64," + content; rv = (mode == "tag") ? "" : src; } catch(e) { Components.utils.reportError(e); } } return rv; } , process_text: function(aVariables) { if (aVariables.length != 2) return ""; // Looks after the group and text-name and returns // the text from it var groupLength = this.mQuicktext.getGroupLength(false); for (var j = 0; j < groupLength; j++) { if (aVariables[0] == this.mQuicktext.getGroup(j, true).name) { var textLength = this.mQuicktext.getTextLength(j, false); for (var k = 0; k < textLength; k++) { var text = this.mQuicktext.getText(j, k, false); if (aVariables[1] == text.name) { return text.text; } } } } return ""; } , process_script: function(aVariables) { if (aVariables.length == 0) return ""; var scriptName = aVariables.shift(); // Looks through all scripts and tries to find the // one we look for var scriptLength = this.mQuicktext.getScriptLength(false); for (var i = 0; i < scriptLength; i++) { var script = this.mQuicktext.getScript(i, false); if (script.name == scriptName) { let returnValue = ""; var referenceLineNumber = 0 try { var error = variableNotAvailable; // provoke an error to create a reference for the other linenumber } catch (eReference) { referenceLineNumber = eReference.lineNumber; } try { var s = Components.utils.Sandbox(this.mWindow); s.mQuicktext = this; s.mVariables = aVariables; s.mWindow = this.mWindow; returnValue = Components.utils.evalInSandbox("scriptObject = {}; scriptObject.mQuicktext = mQuicktext; scriptObject.mVariables = mVariables; scriptObject.mWindow = mWindow; scriptObject.run = function() {\n" + script.script +"\nreturn ''; }; scriptObject.run();", s); } catch (e) { if (this.mWindow) { var lines = script.script.split("\n"); // Takes the linenumber where the error where and remove // the line that it was run on so we get the line in the script // calculate it by using a reference error linenumber and an offset // offset: 10 lines between "variableNotAvailable" and "evalInSandbox" var lineNumber = e.lineNumber - referenceLineNumber - 10; this.mWindow.alert(gQuicktext.mStringBundle.GetStringFromName("scriptError") + " " + script.name + "\n" + e.name + ": "+ e.message + "\n" + gQuicktext.mStringBundle.GetStringFromName("scriptLine") + " " + lineNumber + ": " + lines[lineNumber-1]); } } return returnValue; } } //if we reach this point, the user requested an non-existing script this.mWindow.alert(gQuicktext.mStringBundle.formatStringFromName("scriptNotFound", [scriptName], 1)) return ""; } , process_att: function(aVariables) { if (this.mData['ATT'] && this.mData['ATT'].checked) return this.mData['ATT'].data; this.mData['ATT'] = {}; this.mData['ATT'].checked = true; this.mData['ATT'].data = []; // To get the attachments we look in the attachment-field // in compose-window. var bucket = this.mWindow.document.getElementById("attachmentBucket"); for (var index = 0; index < bucket.getRowCount(); index++) { var item = bucket.getItemAtIndex(index); var attachment = item.attachment; if (attachment) { var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); var fileHandler = ios.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler); try { var file = fileHandler.getFileFromURLSpec(attachment.url); if (file.exists()) this.mData['ATT'].data.push([attachment.name, file.fileSize]); } catch(e) { this.mData['ATT'].data.push([attachment.name]); } } } return this.mData['ATT'].data; } , process_input: function(aVariables) { if (typeof this.mData['INPUT'] == 'undefined') this.mData['INPUT'] = {}; if (typeof this.mData['INPUT'].data == 'undefined') this.mData['INPUT'].data = {}; if (typeof this.mData['INPUT'].data[aVariables[0]] != 'undefined') return this.mData['INPUT'].data; // There are two types of input select and text. var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService); if (aVariables[1] == 'select') { var checkValue = {}; var value = {}; if (typeof aVariables[2] != 'undefined') value.value = aVariables[2].split(";"); if (promptService.select(this.mWindow, gQuicktext.mStringBundle.GetStringFromName("inputTitle"), gQuicktext.mStringBundle.formatStringFromName("inputText", [aVariables[0]], 1), value.value, checkValue)) this.mData['INPUT'].data[aVariables[0]] = value.value[checkValue.value]; else this.mData['INPUT'].data[aVariables[0]] = ""; } else { var checkValue = {}; var value = {}; if (typeof aVariables[2] != 'undefined') value.value = aVariables[2]; if (promptService.prompt(this.mWindow, gQuicktext.mStringBundle.GetStringFromName("inputTitle"), gQuicktext.mStringBundle.formatStringFromName("inputText", [aVariables[0]], 1), value, null, checkValue)) this.mData['INPUT'].data[aVariables[0]] = value.value; else this.mData['INPUT'].data[aVariables[0]] = ""; } return this.mData['INPUT'].data; } , process_selection: function(aVariables, aType) { if (aType == 0) { // return selected text as plain text return this.mQuicktext.mSelectionContent; } else { // return selected text as html return this.mQuicktext.mSelectionContentHtml; } } , process_clipboard: function(aVariables, aType) { if (this.mData['CLIPBOARD'] && this.mData['CLIPBOARD'].checked) return this.mData['CLIPBOARD'].data; this.mData['CLIPBOARD'] = {}; this.mData['CLIPBOARD'].checked = true; this.mData['CLIPBOARD'].data = ""; // Gets the data from the clipboard var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard); if (clip) { var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable); if (trans) { // HTML templates: request the clipboard content as html first var clipboardHTMLfilled = 0; if (aType == 1) { trans.addDataFlavor("text/html"); clip.getData(trans, clip.kGlobalClipboard); var clipboardHTML = {}; try { trans.getTransferData("text/html", clipboardHTML); if (clipboardHTML) { clipboardHTML = clipboardHTML.value.QueryInterface(Components.interfaces.nsISupportsString); if (clipboardHTML) { this.mData['CLIPBOARD'].data = clipboardHTML.data; clipboardHTMLfilled = 1; } } } catch (e) { Components.utils.reportError(e); } } // HTML templates: request clipboard content as plain text, if requesting as html failed // Text templates: request clipboard content as plain text only if(clipboardHTMLfilled == 0) { trans.addDataFlavor("text/unicode"); clip.getData(trans, clip.kGlobalClipboard); var clipboard = {}; try { trans.getTransferData("text/unicode", clipboard); if (clipboard) { clipboard = clipboard.value.QueryInterface(Components.interfaces.nsISupportsString); if (clipboard) this.mData['CLIPBOARD'].data = clipboard.data; } } catch (e) { Components.utils.reportError(e); } } } } return this.mData['CLIPBOARD'].data; } , getcarddata_from: function(aData, aIdentity) { let passStandardCheck = false; try { let card = cardbookRepository.cardbookUtils.getCardFromEmail(aIdentity.email.toLowerCase()); if (card) { aData['FROM'].data['firstname'] = TrimString(card.firstname); aData['FROM'].data['lastname'] = TrimString(card.lastname); aData['FROM'].data['displayname'] = TrimString(card.fn); aData['FROM'].data['nickname'] = TrimString(card.nickname); aData['FROM'].data['fullname'] = TrimString(cardbookRepository.cardbookUtils.getName(card)); aData['FROM'].data['title'] = TrimString(card.title); aData['FROM'].data['workphone'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.worktype", false)); aData['FROM'].data['faxnumber'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.faxtype", false)); aData['FROM'].data['cellularnumber'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.celltype", false)); aData['FROM'].data['custom1'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM1", false)); aData['FROM'].data['custom2'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM2", false)); aData['FROM'].data['custom3'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM3", false)); aData['FROM'].data['custom4'] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM4", false)); passStandardCheck = true; } } catch(e) {} if (!passStandardCheck) { let card = this.getCardForEmail(aIdentity.email.toLowerCase()); if (card == null && aIdentity.escapedVCard != null) { const manager = Cc["@mozilla.org/addressbook/msgvcardservice;1"] .getService(Ci.nsIMsgVCardService) card = manager.escapedVCardToAbCard(aIdentity.escapedVCard); } if (card != null) { var props = this.getPropertiesFromCard(card); for (var p in props) this.mData['FROM'].data[p] = props[p]; aData['FROM'].data['fullname'] = TrimString(aData['FROM'].data['firstname'] +" "+ aData['FROM'].data['lastname']); } } return aData; } , process_from: function(aVariables) { if (this.mData['FROM'] && this.mData['FROM'].checked) return this.mData['FROM'].data; const identity = this.mWindow.gCurrentIdentity; this.mData['FROM'] = {}; this.mData['FROM'].checked = true; this.mData['FROM'].data = { 'email': identity.email, 'displayname': identity.fullName, 'firstname': '', 'lastname': '' }; this.mData = this.getcarddata_from(this.mData, identity); return this.mData['FROM'].data; } , getcarddata_to: function(aData, aIndex) { let passStandardCheck = false; try { let card = cardbookRepository.cardbookUtils.getCardFromEmail(aData['TO'].data['email'][aIndex]); if (card) { aData['TO'].data['firstname'][aIndex] = TrimString(card.firstname); aData['TO'].data['lastname'][aIndex] = TrimString(card.lastname); aData['TO'].data['fullname'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getName(card)); // others for (let prop of [ 'displayname', 'nickname', 'title', 'workphone', 'faxnumber', 'cellularnumber', 'custom1', 'custom2', 'custom3', 'custom4' ]) { if (typeof aData['TO'].data[prop] == 'undefined') aData['TO'].data[prop] = [] } aData['TO'].data['displayname'][aIndex] = TrimString(card.fn); aData['TO'].data['nickname'][aIndex] = TrimString(card.nickname); aData['TO'].data['title'][aIndex] = TrimString(card.title); aData['TO'].data['workphone'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.worktype", false)); aData['TO'].data['faxnumber'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.faxtype", false)); aData['TO'].data['cellularnumber'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "tel.0.celltype", false)); aData['TO'].data['custom1'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM1", false)); aData['TO'].data['custom2'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM2", false)); aData['TO'].data['custom3'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM3", false)); aData['TO'].data['custom4'][aIndex] = TrimString(cardbookRepository.cardbookUtils.getCardValueByField(card, "X-CUSTOM4", false)); passStandardCheck = true; } } catch(e) {} if (!passStandardCheck) { // take card value, if it exists var card = this.getCardForEmail(aData['TO'].data['email'][aIndex]); if (card != null) { var props = this.getPropertiesFromCard(card); for (var p in props) { if (typeof aData['TO'].data[p] == 'undefined') aData['TO'].data[p] = [] if (props[p] != "" || typeof aData['TO'].data[p][aIndex] == 'undefined' || aData['TO'].data[p][aIndex] == "") aData['TO'].data[p][aIndex] = TrimString(props[p]); } } } return aData; } , process_to: function(aVariables) { if (this.mData['TO'] && this.mData['TO'].checked) return this.mData['TO'].data; this.mData['TO'] = {}; this.mData['TO'].checked = true; this.mData['TO'].data = { 'email': [], 'firstname': [], 'lastname': [], 'fullname': [] }; this.mWindow.Recipients2CompFields(this.mWindow.gMsgCompose.compFields); let emailAddresses = MailServices.headerParser.parseEncodedHeader(this.mWindow.gMsgCompose.compFields.to); if (emailAddresses.length > 0) { for (var i = 0; i < emailAddresses.length; i++) { // TODO: Add code for getting info about all people in a mailing list var k = this.mData['TO'].data['email'].length; this.mData['TO'].data['email'][k] = emailAddresses[i].email.toLowerCase(); this.mData['TO'].data['fullname'][k] = TrimString(emailAddresses[i].name); this.mData['TO'].data['firstname'][k] = ""; this.mData['TO'].data['lastname'][k] = ""; this.mData = this.getcarddata_to(this.mData, k); let validParts = [this.mData['TO'].data['firstname'][k], this.mData['TO'].data['lastname'][k]].filter(e => e.trim() != ""); if (validParts.length == 0) { // if no first and last name, generate them from fullname let parts = this.mData['TO'].data['fullname'][k].replace(/,/g, ", ").split(" ").filter(e => e.trim() != ""); this.mData['TO'].data['firstname'][k] = parts.length > 1 ? TrimString(parts.splice(0, 1)) : ""; this.mData['TO'].data['lastname'][k] = TrimString(parts.join(" ")); } else { // if we have a first and/or last name (which can only happen if read from card), generate fullname from it this.mData['TO'].data['fullname'][k] = validParts.join(" "); } // swap names if wrong if (this.mData['TO'].data['firstname'][k].endsWith(",")) { let temp_firstname = this.mData['TO'].data['firstname'][k].replace(/,/g, ""); let temp_lastname = this.mData['TO'].data['lastname'][k]; this.mData['TO'].data['firstname'][k] = temp_lastname; this.mData['TO'].data['lastname'][k] = temp_firstname; // rebuild fullname this.mData['TO'].data['fullname'][k] = [this.mData['TO'].data['firstname'][k], this.mData['TO'].data['lastname'][k]].join(" "); } } } return this.mData['TO'].data; } , process_url: async function(aVariables) { if (aVariables.length == 0) return ""; var url = aVariables.shift(); if (url != "") { var debug = false; var method = "post"; var post = []; if (aVariables.length > 0) { var variables = aVariables.shift().split(";"); for (var k = 0; k < variables.length; k++) { var tag = variables[k].toLowerCase(); var data = null; switch (tag) { case 'to': case 'att': case 'orgheader': case 'orgatt': data = await this["process_"+ tag](); if (typeof data != 'undefined') { for (var i in data) for (var j in data[i]) post.push(tag +'['+ i +']['+ j +']='+ data[i][j]); } break; case 'from': case 'version': case 'date': case 'time': data = await this["process_"+ tag](); if (typeof data != 'undefined') { for (var i in data) post.push(tag +'['+ i +']='+ data[i]); } break; case 'subject': case 'clipboard': case 'selection': case 'counter': data = await this["process_"+ tag](); if (typeof data != 'undefined') post.push(tag +'='+ data); break; case 'post': case 'get': case 'options': method = tag; break; case 'debug': debug = true; break; } } } var req = new XMLHttpRequest(); req.open(method, url, true); if (method == "post") req.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); let response = ""; //Lazy async-to-sync implementation with ACK from Philipp Kewisch //http://lists.thunderbird.net/pipermail/maildev_lists.thunderbird.net/2018-June/001205.html let inspector = Components.classes["@mozilla.org/jsinspector;1"].createInstance(Components.interfaces.nsIJSInspector); req.ontimeout = function () { if (debug) response = "Quicktext timeout"; inspector.exitNestedEventLoop(); }; req.onerror = function () { if (debug) response = "error (" + req.status + ")"; inspector.exitNestedEventLoop(); }; req.onload = function() { if (req.status == 200) response = req.responseText; else if (debug) response = "error (" + req.status + ")"; inspector.exitNestedEventLoop(); }; if (method == "post") req.send(post.join("&")); else req.send(); inspector.enterNestedEventLoop(0); /* wait for async process to terminate */ return response; } return ""; } , process_version: function(aVariables) { if (this.mData['VERSION'] && this.mData['VERSION'].checked) return this.mData['VERSION'].data; this.mData['VERSION'] = {}; this.mData['VERSION'].checked = true; this.mData['VERSION'].data = {}; this.mData['VERSION'].data['number'] = Services.appinfo.version; this.mData['VERSION'].data['full'] = Services.appinfo.name + ' ' + Services.appinfo.version; return this.mData['VERSION'].data; } , process_counter: async function(aVariables) { if (this.mData['COUNTER'] && this.mData['COUNTER'].checked) return this.mData['COUNTER'].data; this.mData['COUNTER'] = {}; this.mData['COUNTER'].checked = true; this.mData['COUNTER'].data = await this.mQuicktext.notifyTools.notifyBackground({command:"getPref", pref: "counter"}); this.mData['COUNTER'].data++; await this.mQuicktext.notifyTools.notifyBackground({command:"setPref", pref: "counter", value: this.mData['COUNTER'].data}); return this.mData['COUNTER'].data; } , process_subject: function(aVariables) { if (this.mData['SUBJECT'] && this.mData['SUBJECT'].checked) return this.mData['SUBJECT'].data; this.mData['SUBJECT'] = {}; this.mData['SUBJECT'].checked = true; this.mData['SUBJECT'].data = ""; if (this.mWindow.document.getElementById('msgSubject')) this.mData['SUBJECT'].data = this.mWindow.document.getElementById('msgSubject').value; return this.mData['SUBJECT'].data; } , process_date: function(aVariables) { if (this.mData['DATE'] && this.mData['DATE'].checked) return this.mData['DATE'].data; this.preprocess_datetime(); return this.mData['DATE'].data; } , process_time: function(aVariables) { if (this.mData['TIME'] && this.mData['TIME'].checked) return this.mData['TIME'].data; this.preprocess_datetime(); return this.mData['TIME'].data; } , process_orgheader: function(aVariables) { if (this.mData['ORGHEADER'] && this.mData['ORGHEADER'].checked) return this.mData['ORGHEADER'].data; this.preprocess_org(); return this.mData['ORGHEADER'].data; } , process_orgatt: function(aVariables) { if (this.mData['ORGATT'] && this.mData['ORGATT'].checked) return this.mData['ORGATT'].data; this.preprocess_org(); return this.mData['ORGATT'].data; } , preprocess_datetime: function() { this.mData['DATE'] = {}; this.mData['DATE'].checked = true; this.mData['DATE'].data = {}; this.mData['TIME'] = {}; this.mData['TIME'].checked = true; this.mData['TIME'].data = {}; var timeStamp = new Date(); let fields = ["DATE-long", "DATE-short", "DATE-monthname", "TIME-seconds", "TIME-noseconds"]; for (let i=0; i < fields.length; i++) { let field = fields[i]; let fieldinfo = field.split("-"); this.mData[fieldinfo[0]].data[fieldinfo[1]] = TrimString(quicktextUtils.dateTimeFormat(field, timeStamp)); } } , preprocess_org: function() { this.mData['ORGHEADER'] = {}; this.mData['ORGHEADER'].checked = true; this.mData['ORGHEADER'].data = {}; this.mData['ORGATT'] = {}; this.mData['ORGATT'].checked = true; this.mData['ORGATT'].data = {contentType:[], url:[], displayName:[], uri:[], isExternal:[]}; var msgURI = this.mWindow.gMsgCompose.originalMsgURI; if (!msgURI || msgURI == "") return; var messenger = Components.classes["@mozilla.org/messenger;1"].createInstance(Components.interfaces.nsIMessenger); var mms = messenger.messageServiceFromURI(msgURI).QueryInterface(Components.interfaces.nsIMsgMessageService); //Lazy async-to-sync implementation with ACK from Philipp Kewisch //http://lists.thunderbird.net/pipermail/maildev_lists.thunderbird.net/2018-June/001205.html let inspector = Components.classes["@mozilla.org/jsinspector;1"].createInstance(Components.interfaces.nsIJSInspector); let listener = streamListener(inspector); mms.streamMessage(msgURI, listener, null, null, true, "filter"); //lazy async, wait for listener inspector.enterNestedEventLoop(0); /* wait for async process to terminate */ // Store all headers in the mData-variable for (var i = 0; i < listener.mHeaders.length; i++) { var name = listener.mHeaders[i].name; if (typeof this.mData['ORGHEADER'].data[name] == 'undefined') this.mData['ORGHEADER'].data[name] = []; this.mData['ORGHEADER'].data[name].push(listener.mHeaders[i].value); } // Store all attachments in the mData-variable for (var i = 0; i < listener.mAttachments.length; i++) { var attachment = listener.mAttachments[i]; for (var fields in attachment) this.mData['ORGATT'].data[fields][i] = attachment[fields]; } } , escapeRegExp: function(aStr) { return aStr.replace(/([\^\$\_\.\\\[\]\(\)\|\+\?])/g, "\\$1"); } , replaceText: function(tag, value, text) { var replaceRegExp; if (value != "") replaceRegExp = new RegExp(this.escapeRegExp(tag), 'g'); else replaceRegExp = new RegExp("( )?"+ this.escapeRegExp(tag), 'g'); return text.replace(replaceRegExp, value); } , niceFileSize: function(size) { var unit = ["B", "kB", "MB", "GB", "TB"]; var i = 0; while (size > 1024) { i++; size = size / 1024; } return (Math.round(size * 100) / 100) + " " + unit[i]; } , getCardForEmail: function(aAddress) { let directories = MailServices.ab.directories; for (let addrbook of directories) { let card = addrbook.cardForEmailAddress(aAddress); if (card) { return card; } } return null; } , getPropertiesFromCard: function(card) { var retval = {} var props = card.properties; for (let prop of props) { retval[prop.name.toLowerCase()] = prop.value; } return retval; } , removeBadHTML: function (aStr) { // Remove the head-tag aStr = aStr.replace(/]*)>.*<\/head>/gim, ''); // Remove html and body tags aStr = aStr.replace(/<(|\/)(head|body)(| [^>]*)>/gim, ''); return aStr; } } var debug = kDebug ? function(m) {dump("\t *** wzQuicktext: " + m + "\n");} : function(m) {}; function TrimString(aStr) { if (!aStr) return ""; return aStr.toString().replace(/(^\s+)|(\s+$)/g, '') } quicktext-4.1/chrome/content/notifyTools/000077500000000000000000000000001410655052500206625ustar00rootroot00000000000000quicktext-4.1/chrome/content/notifyTools/README.md000066400000000000000000000002471410655052500221440ustar00rootroot00000000000000This script is intended to be used together with the [NotifyTools API](https://github.com/thundernest/addon-developer-support/tree/master/auxiliary-apis/NotifyTools). quicktext-4.1/chrome/content/notifyTools/notifyTools.js000066400000000000000000000070371410655052500235600ustar00rootroot00000000000000// Set this to the ID of your add-on. const ADDON_ID = "{8845E3B3-E8FB-40E2-95E9-EC40294818C4}"; /* * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * * For usage descriptions, please check: * https://github.com/thundernest/addon-developer-support/tree/master/scripts/notifyTools * * Version: 1.3 * - registered listeners for notifyExperiment can return a value * - remove WindowListener from name of observer * * Author: John Bieling (john@thunderbird.net) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var notifyTools = { registeredCallbacks: {}, registeredCallbacksNextId: 1, onNotifyExperimentObserver: { observe: async function (aSubject, aTopic, aData) { if (ADDON_ID == "") { throw new Error("notifyTools: ADDON_ID is empty!"); } if (aData != ADDON_ID) { return; } let payload = aSubject.wrappedJSObject; if (payload.resolve) { let observerTrackerPromises = []; // Push listener into promise array, so they can run in parallel for (let registeredCallback of Object.values( notifyTools.registeredCallbacks )) { observerTrackerPromises.push(registeredCallback(payload.data)); } // We still have to await all of them but wait time is just the time needed // for the slowest one. let results = []; for (let observerTrackerPromise of observerTrackerPromises) { let rv = await observerTrackerPromise; if (rv != null) results.push(rv); } if (results.length == 0) { payload.resolve(); } else { if (results.length > 1) { console.warn( "Received multiple results from onNotifyExperiment listeners. Using the first one, which can lead to inconsistent behavior.", results ); } payload.resolve(results[0]); } } else { // Just call the listener. for (let registeredCallback of Object.values( notifyTools.registeredCallbacks )) { registeredCallback(payload.data); } } }, }, registerListener: function (listener) { let id = this.registeredCallbacksNextId++; this.registeredCallbacks[id] = listener; return id; }, removeListener: function (id) { delete this.registeredCallbacks[id]; }, notifyBackground: function (data) { if (ADDON_ID == "") { throw new Error("notifyTools: ADDON_ID is empty!"); } return new Promise((resolve) => { Services.obs.notifyObservers( { data, resolve }, "NotifyBackgroundObserver", ADDON_ID ); }); }, enable: function() { Services.obs.addObserver( this.onNotifyExperimentObserver, "NotifyExperimentObserver", false ); }, disable: function() { Services.obs.removeObserver( this.onNotifyExperimentObserver, "NotifyExperimentObserver" ); }, }; if (typeof window != "undefined" && window) { window.addEventListener( "load", function (event) { notifyTools.enable(); window.addEventListener( "unload", function (event) { notifyTools.disable(); }, false ); }, false ); } quicktext-4.1/chrome/content/quicktext.js000066400000000000000000000667251410655052500207300ustar00rootroot00000000000000var { gQuicktext } = ChromeUtils.import("chrome://quicktext/content/modules/wzQuicktext.jsm"); var { wzQuicktextVar } = ChromeUtils.import("chrome://quicktext/content/modules/wzQuicktextVar.jsm"); var gQuicktextVar = new wzQuicktextVar(); var { quicktextUtils } = ChromeUtils.import("chrome://quicktext/content/modules/utils.jsm"); var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); var quicktextStateListener = { NotifyComposeBodyReady: function() { quicktext.insertDefaultTemplate(); }, NotifyComposeFieldsReady: function() {}, ComposeProcessDone: function(aResult) {}, SaveInFolderDone: function(folderURI) {} } var quicktext = { mLoaded: false, mLastFocusedElement: null, mShortcuts: {}, mShortcutString: "", mShortcutModifierDown: false, mKeywords: {} , load: async function() { if (!this.mLoaded) { this.mLoaded = true; gQuicktext.addObserver(this); if (!(await gQuicktext.loadSettings(false))) this.updateGUI(); gQuicktextVar.init(window); // Add an eventlistener for keypress in the window window.addEventListener("keypress", function(e) { quicktext.windowKeyPress(e); }, true); window.addEventListener("keydown", function(e) { quicktext.windowKeyDown(e); }, true); window.addEventListener("keyup", function(e) { quicktext.windowKeyUp(e); }, true); // Add an eventlistener for keypress in the editor var contentFrame = GetCurrentEditorElement(); contentFrame.addEventListener("keypress", function(e) { quicktext.editorKeyPress(e); }, false); // Add an eventlistener for the popup-menu. var menu = document.getElementById("msgComposeContext"); menu.addEventListener("popupshowing", function(e) { quicktext.popupshowing(e); }, false); // Need to update GUI when the Quicktext-button is added to the toolbar (updating on ANY change to the toolbar is much more simple, and it does not hurt) window.addEventListener("aftercustomization", function() { quicktext.updateGUI(); } , false); } } , reload: function() { gQuicktextVar.init(window); } , unload: function() { // Remove the observer gQuicktext.removeObserver(this); window.removeEventListener("keypress", function(e) { quicktext.windowKeyPress(e); }, true); window.removeEventListener("keydown", function(e) { quicktext.windowKeyDown(e); }, true); window.removeEventListener("keyup", function(e) { quicktext.windowKeyUp(e); }, true); // Remove the eventlistener from the editor var contentFrame = GetCurrentEditorElement(); contentFrame.removeEventListener("keypress", function(e) { quicktext.editorKeyPress(e); }, false); // Remove the eventlistener for the popup-menu. var menu = document.getElementById("msgComposeContext"); menu.removeEventListener("popupshowing", function(e) { quicktext.popupshowing(e); }, false); window.removeEventListener("aftercustomization", function() { quicktext.updateGUI(); } , false); } , /** * This is called when the var gMsgCompose is init. We now take * the extraArguments value and listen for state changes so * we know when the editor is finished. */ windowInit: function() { gMsgCompose.RegisterStateListener(quicktextStateListener); } , /* * This is called when the body of the mail is set up. * So now it is time to insert the default template if * there exists one. */ insertDefaultTemplate: function() { dump("insertDefaultTemplate\n"); } , updateGUI: function() { // Set the date/time in the variablemenu var timeStamp = new Date(); let fields = ["date-short", "date-long", "date-monthname", "time-noseconds", "time-seconds"]; for (let i=0; i < fields.length; i++) { let field = fields[i]; let fieldtype = field.split("-")[0]; if (document.getElementById(field)) { document.getElementById(field).setAttribute("label", gQuicktext.mStringBundle.formatStringFromName(fieldtype, [quicktextUtils.dateTimeFormat(field, timeStamp)], 1)); } } // Empty all shortcuts and keywords this.mShortcuts = {}; this.mKeywords = {}; // Update the toolbar var toolbar = document.getElementById("quicktext-toolbar"); if (toolbar != null) { //clear toolbar and store current "variables" and "other" menus (the two rightmost ones) var toolbarbuttonVar = null; var toolbarbuttonOther = null; var length = toolbar.children.length; for(var i = length-1; i >= 0; i--) { var element = toolbar.children[i]; switch(element.getAttribute("id")) { case 'quicktext-variables': toolbarbuttonVar = element.cloneNode(true); break; case 'quicktext-other': toolbarbuttonOther = element.cloneNode(true); break; } toolbar.removeChild(element); } //rebuild template groups (the leftmost entries) var groupLength = gQuicktext.getGroupLength(false); for (var i = 0; i < groupLength; i++) { var textLength = gQuicktext.getTextLength(i, false); if (textLength) { //Add first level element, this will be either a menu or a button (if only one text in this group) var toolbarbuttonGroup; let t = document.createXULElement("button"); // add a tabindex of -1 (not reachable via sequential keyboard navigation) // also added a tabindex to the buttons "variables" and "other" in "chrome/content/scripts/messengercompose.js" // see: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex t.setAttribute("tabindex", "-1"); if (textLength == 1 && gQuicktext.collapseGroup) { toolbarbuttonGroup = toolbar.appendChild(t); toolbarbuttonGroup.setAttribute("label", gQuicktext.getText(i, 0, false).name); toolbarbuttonGroup.setAttribute("i", i); toolbarbuttonGroup.setAttribute("j", 0); toolbarbuttonGroup.setAttribute("class", "customEventListenerForDynamicMenu"); } else { t.setAttribute("type", "menu"); toolbarbuttonGroup = toolbar.appendChild(t); toolbarbuttonGroup.setAttribute("label", gQuicktext.getGroup(i, false).name); var menupopup = toolbarbuttonGroup.appendChild(document.createXULElement("menupopup")); //add second level elements: all found texts of this group for (var j = 0; j < textLength; j++) { var text = gQuicktext.getText(i, j, false); var toolbarbutton = document.createXULElement("menuitem"); toolbarbutton.setAttribute("label", text.name); toolbarbutton.setAttribute("i", i); toolbarbutton.setAttribute("j", j); toolbarbutton.setAttribute("class", "customEventListenerForDynamicMenu"); var shortcut = text.shortcut; if (shortcut > 0) { if (shortcut == 10) shortcut = 0; toolbarbutton.setAttribute("acceltext", "Alt+" + shortcut); } menupopup.appendChild(toolbarbutton); } } toolbarbuttonGroup = null; // Update the keyshortcuts for (var j = 0; j < textLength; j++) { var text = gQuicktext.getText(i, j, false); var shortcut = text.shortcut; if (shortcut != "" && typeof this.mShortcuts[shortcut] == "undefined") this.mShortcuts[shortcut] = [i, j]; var keyword = text.keyword; if (keyword != "" && typeof this.mKeywords[keyword.toLowerCase()] == "undefined") this.mKeywords[keyword.toLowerCase()] = [i, j]; } } } //add a flex spacer to push the VAR and OTHER elements to the right var spacer = document.createXULElement("spacer"); spacer.setAttribute("flex", "1"); toolbar.appendChild(spacer); toolbar.appendChild(toolbarbuttonVar); toolbar.appendChild(toolbarbuttonOther); // Update the toolbar inside the toolbarpalette and the drop-down menu - if used let optionalUI = ["button-quicktext", "quicktext-popup"]; for (let a=0; a < optionalUI.length; a++) { if (document.getElementById(optionalUI[a] + "-menupopup")) { let rootElement = document.getElementById(optionalUI[a] + "-menupopup"); //clear let length = rootElement.children.length; for (let i = length-1; i >= 0; i--) rootElement.removeChild(rootElement.children[i]); //rebuild via copy from the quicktext toolbar - loop over toolbarbuttons inside toolbar for (let i = 0; i < toolbar.children.length; i++) { let menu = null; let node = toolbar.children[i]; switch (node.nodeName) { case "button": case "toolbarbutton": // Check if the group is collapse or not if (node.getAttribute("type") == "menu") { menu = document.createXULElement("menu"); menu.setAttribute("label", node.getAttribute("label")); let childs = node.querySelectorAll(":not(menu) > menupopup"); for (let child of childs) { menu.appendChild(child.cloneNode(true)); } } else { menu = document.createXULElement("menuitem"); menu.setAttribute("label", node.getAttribute("label")); menu.setAttribute("i", node.getAttribute("i")); menu.setAttribute("j", node.getAttribute("j")); menu.setAttribute("class", "customEventListenerForDynamicMenu"); } rootElement.appendChild(menu); break; case "spacer": rootElement.appendChild(document.createXULElement("menuseparator")); break; } } } } } //add event listeners let items = document.getElementsByClassName("customEventListenerForDynamicMenu"); for (let i=0; i < items.length; i++) { items[i].addEventListener("command", function() { quicktext.insertTemplate(this.getAttribute("i"), this.getAttribute("j"), true, true); }, true); } this.visibleToolbar(); } , popupshowing: function(aEvent) { var hidden = !gQuicktext.viewPopup; document.getElementById("quicktext-popup").hidden = hidden; document.getElementById("quicktext-popupsep").hidden = hidden; } , openSettings: function() { var settingsHandle = window.open("chrome://quicktext/content/settings.xhtml", "quicktextConfig", "chrome,resizable,centerscreen"); settingsHandle.focus(); } , toogleToolbar: function() { gQuicktext.viewToolbar = !gQuicktext.viewToolbar; } , visibleToolbar: function() { // Set the view of the toolbar to what it should be if (gQuicktext.viewToolbar) { document.getElementById("quicktext-view").setAttribute("checked", true); document.getElementById("quicktext-toolbar").removeAttribute("collapsed"); } else { document.getElementById("quicktext-view").removeAttribute("checked"); document.getElementById("quicktext-toolbar").setAttribute("collapsed", true); } } , /* * INSERTING TEXT */ insertVariable: async function(aVar) { gQuicktextVar.cleanTagData(); await this.insertBody("[["+ aVar +"]] ", 0, true); } , insertTemplate: async function(aGroupIndex, aTextIndex, aHandleTransaction = true, aFocusBody = false) { //store selected content var editor = GetCurrentEditor(); var selection = editor.selection; if (selection.rangeCount > 0) { // store the selected content as plain text gQuicktext.mSelectionContent = selection.toString(); // store the selected content as html text gQuicktext.mSelectionContentHtml = editor.outputToString('text/html', 1); } if (gQuicktext.doTextExists(aGroupIndex, aTextIndex, false)) { this.mLastFocusedElement = document.activeElement; gQuicktextVar.cleanTagData(); var text = gQuicktext.getText(aGroupIndex, aTextIndex, false); text.removeHeaders(); gQuicktext.mCurrentTemplate = text; // this parsing of the header informations isn't able to parse something like: [[HEADER=to|[[SCRIPT=getReciepients]]]] // // Parse text for HEADER tags and move them to the header object // let headers = text.text.match(/\[\[header=[^\]]*\]\]/ig); // if (headers && Array.isArray(headers)) { // for (let header of headers) { // let parts = header.split(/=|\]\]|\|/); // if (parts.length==4) { // text.addHeader(parts[1], parts[2]); // } // } // } await this.insertSubject(text.subject); await this.insertAttachments(text.attachments); if (text.text != "" && text.text.indexOf('[[CURSOR]]') > -1) { // only if we really have text to insert with a [[CURSOR]] tag, // focus the message body first this.focusMessageBody(); } await this.insertBody(text.text, text.type, aHandleTransaction); // has to be inserted below "insertBody" as "insertBody" gathers the header data from the header tags await this.insertHeaders(text); if(aFocusBody){ // the variable aFocusBody is only used from Quicktext-toolbar to focus the message body after using the toolbar setTimeout(function () { quicktext.focusMessageBody(); }, 1); } else { // if we insert any headers we maybe needs to return the placement of the focus setTimeout(function () { quicktext.moveFocus(); }, 1); } } } , insertAttachments: async function(aStr) { if (aStr != "") { aStr = await gQuicktextVar.parse(aStr); var files = aStr.split(";"); for (var i = 0; i < files.length; i++) { var currentFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile); currentFile.initWithPath(files[i]); if (!currentFile.exists()) continue; var attachment = FileToAttachment(currentFile); if (!DuplicateFileAlreadyAttached(attachment.url)) { AddAttachments([attachment]); } } } } , insertHeaders: async function(aText) { var headerLength = aText.getHeaderLength(); if (headerLength == 0) return; var convertHeaderToType = []; convertHeaderToType["to"] = "to"; convertHeaderToType["cc"] = "cc"; convertHeaderToType["bcc"] = "bcc"; convertHeaderToType["reply-to"] = "reply"; var convertHeaderToParse = []; convertHeaderToParse["to"] = "to"; convertHeaderToParse["cc"] = "cc"; convertHeaderToParse["bcc"] = "bcc"; convertHeaderToParse["reply-to"] = "replyTo"; var recipientHeaders = []; recipientHeaders["to"] = []; recipientHeaders["cc"] = []; recipientHeaders["bcc"] = []; recipientHeaders["reply-to"] = []; // Add all recipient headers to an array var count = 0; for (var i = 0; i < headerLength; i++) { var header = aText.getHeader(i); var type = header.type.toLowerCase(); if (typeof recipientHeaders[type] != "undefined") { recipientHeaders[type].push(await gQuicktextVar.parse(header.value)); count++; } } if (count > 0) { Recipients2CompFields(gMsgCompose.compFields); // Go through all recipientHeaders to remove duplicates var tmpRecipientHeaders = []; count = 0; for (var header in recipientHeaders) { if (recipientHeaders[header].length == 0) continue; tmpRecipientHeaders[header] = []; // Create an array of emailaddresses for this header that allready added let tmpEmailAddresses = MailServices.headerParser.parseEncodedHeader(gMsgCompose.compFields[convertHeaderToParse[header]]); let emailAddresses = []; for (let i = 0; i < tmpEmailAddresses.length; i++) emailAddresses.push(tmpEmailAddresses[i].email); // Go through all recipient of this header that I want to add for (var i = 0; i < recipientHeaders[header].length; i++) { // Get the mailaddresses of all the addresses let insertedAddresses = MailServices.headerParser.parseEncodedHeader(recipientHeaders[header][i]); for (var j = 0; j < insertedAddresses.length; j++) { if (insertedAddresses[j].email && !emailAddresses.includes(insertedAddresses[j].email)) { tmpRecipientHeaders[header].push(insertedAddresses[j].toString()); emailAddresses.push(insertedAddresses[j].email); count++; } } } } if (count > 0) { for (var header in tmpRecipientHeaders) for (var i = 0; i < tmpRecipientHeaders[header].length; i++) awAddRecipientsArray("addr_"+ convertHeaderToType[header], [tmpRecipientHeaders[header][i]], false); } } } , moveFocus: function() { if (this.mLastFocusedElement) { this.mLastFocusedElement.focus(); this.mLastFocusedElement = null; } } , focusMessageBody: function() { let editor = GetCurrentEditorElement();//document.getElementsByTagName("editor"); if (editor) { editor.focus(); this.mLastFocusedElement = editor; } } , insertSubject: async function(aStr) { if (aStr != "") { aStr = await gQuicktextVar.parse(aStr); if (aStr != "" && !aStr.match(/^\s+$/) && document.getElementById('msgSubject')) document.getElementById('msgSubject').value = aStr; } } , insertBody: async function(aStr, aType, aHandleTransaction) { if (aStr != "") { aStr = await gQuicktextVar.parse(aStr, aType); if (aStr != "") { // Inserts the text if (aStr != "" && !aStr.match(/^\s+$/)) { var editor = GetCurrentEditor(); if (aHandleTransaction) editor.beginTransaction(); try { if (gMsgCompose.composeHTML && aType > 0) { // It the text is inserted as HTML we need to remove bad stuff // before we insert it. aStr = gQuicktextVar.removeBadHTML(aStr); editor.insertHTML(aStr); } else editor.insertText(aStr); } catch(e) { Components.utils.reportError(e); } try { if (aStr.indexOf('[[CURSOR]]') > -1) { // Take care of the CURSOR-tag await this.parseCursorTag(editor); } } catch(e) { Components.utils.reportError(e); } if (aHandleTransaction) editor.endTransaction(); } } } } , parseCursorTag: async function(aEditor) { //Based on https://searchfox.org/comm-central/source/editor/ui/dialogs/content/EdReplace.js#255 var searchRange = aEditor.document.createRange(); var rootNode = aEditor.rootElement; searchRange.selectNodeContents(rootNode); var startRange = aEditor.document.createRange(); startRange.setStart(searchRange.startContainer, searchRange.startOffset); startRange.setEnd(searchRange.startContainer, searchRange.startOffset); var endRange = aEditor.document.createRange(); endRange.setStart(searchRange.endContainer, searchRange.endOffset); endRange.setEnd(searchRange.endContainer, searchRange.endOffset); var finder = Components.classes["@mozilla.org/embedcomp/rangefind;1"].createInstance().QueryInterface(Components.interfaces.nsIFind); finder.caseSensitive = true; finder.findBackwards = false; let found = false; let failedSearchAttempts = 0; let foundRange = null; // Loop until all tags have been replaced, but limit the loop to 10 failed attempts. while (failedSearchAttempts < 30) { // Process the last found tag and update the search region. if (foundRange) { found = true; aEditor.selection.removeAllRanges(); aEditor.selection.addRange(foundRange); aEditor.selection.deleteFromDocument(); startRange.setEnd(foundRange.endContainer, foundRange.endOffset); startRange.setStart(foundRange.endContainer, foundRange.endOffset); } // Search. foundRange = finder.Find("[[CURSOR]]", searchRange, startRange, endRange); // If we have found at least one tag, but the last search failed, we are done. if (found && !foundRange) { break; } // If we have never found one, we might run into the "searched too early bug" and need to delay and try again if (!found && !foundRange) { await new Promise(resolve => setTimeout(resolve, 2)); failedSearchAttempts++; } } if (!found) { console.log("CURSOR LOG Failed to find CURSOR tag!"); aEditor.selection.removeAllRanges(); aEditor.selection.addRange(endRange); } else { console.log("CURSOR LOG failedSearchAttempts : " + failedSearchAttempts); } } , dumpTree: function(aNode, aLevel) { for (var i = 0; i < aLevel*2; i++) dump(" "); dump(aNode.nodeName +": "+ aNode.nodeValue +"\n"); for (var i = 0; i < aNode.childNodes.length; i++) { this.dumpTree(aNode.childNodes[i], aLevel+1); } } , insertContentFromFile: async function(aType) { if ((file = await gQuicktext.pickFile(window, aType, 0, gQuicktext.mStringBundle.GetStringFromName("insertFile"))) != null) await this.insertBody(gQuicktext.readFile(file), aType, true); } , /* * KEYPRESS */ windowKeyPress: async function(e) { if (gQuicktext.shortcutTypeAdv) { var shortcut = e.charCode-48; if (shortcut >= 0 && shortcut < 10 && this.mShortcutModifierDown) { this.mShortcutString += String.fromCharCode(e.charCode); e.stopPropagation(); e.preventDefault(); } } else { var modifier = gQuicktext.shortcutModifier; var shortcut = e.charCode-48; if (shortcut >= 0 && shortcut < 10 && typeof this.mShortcuts[shortcut] != "undefined" && ( e.altKey && modifier == "alt" || e.ctrlKey && modifier == "control" || e.metaKey && modifier == "meta")) { await this.insertTemplate(this.mShortcuts[shortcut][0], this.mShortcuts[shortcut][1]); e.stopPropagation(); e.preventDefault(); } } } , windowKeyDown: function(e) { var modifier = gQuicktext.shortcutModifier; if (!this.mShortcutModifierDown && gQuicktext.shortcutTypeAdv && ( e.keyCode == e.DOM_VK_ALT && modifier == "alt" || e.keyCode == e.DOM_VK_CONTROL && modifier == "control" || e.keyCode == e.DOM_VK_META && modifier == "meta")) this.mShortcutModifierDown = true; } , windowKeyUp: async function(e) { var modifier = gQuicktext.shortcutModifier; if (gQuicktext.shortcutTypeAdv && ( e.keyCode == e.DOM_VK_ALT && modifier == "alt" || e.keyCode == e.DOM_VK_CONTROL && modifier == "control" || e.keyCode == e.DOM_VK_META && modifier == "meta")) { if (this.mShortcutString != "" && typeof this.mShortcuts[this.mShortcutString] != "undefined") { await this.insertTemplate(this.mShortcuts[this.mShortcutString][0], this.mShortcuts[this.mShortcutString][1]); e.stopPropagation(); e.preventDefault(); } this.mShortcutModifierDown = false; this.mShortcutString = ""; } } , editorKeyPress: async function(e) { if (e.code == gQuicktext.keywordKey) { var editor = GetCurrentEditor(); var selection = editor.selection; if (!(selection.rangeCount > 0)) return; // All operations between beginTransaction and endTransaction // are done "at once" as a single atomic action. editor.beginTransaction(); // This gives us a range object of the currently selected text // and as the user usually does not have any text selected when // triggering keywords, it is a collapsed range at the current // cursor position. var initialSelectionRange = selection.getRangeAt(0).cloneRange(); // Ugly solution to just search to the beginning of the line. // I set the selection to the beginning of the line save the // range and then sets the selection back to was before. // Changing the selections was not visible to me. Most likly is // that is not even rendered. var tmpRange = initialSelectionRange.cloneRange(); tmpRange.collapse(false); editor.selection.removeAllRanges(); editor.selection.addRange(tmpRange); editor.selectionController.intraLineMove(false, true); if (!(selection.rangeCount > 0)) { editor.endTransaction(); return; } // intraLineMove() extended the selection from the cursor to the // beginning of the line. We can get the last word by simply // chopping up its content. let lastWord = selection.toString().split(" ").pop(); let lastWordIsKeyword = this.mKeywords.hasOwnProperty(lastWord.toLowerCase()); // We now need to get a range, which covers the keyword, // as we want to replace it. So we clone the current selection // into a wholeRange and use nsIFind to find lastWord. var wholeRange = selection.getRangeAt(0).cloneRange(); // Restore to the initialSelectionRange. editor.selection.removeAllRanges(); editor.selection.addRange(initialSelectionRange); // If the last word is not a keyword, abort. if (!lastWordIsKeyword || !lastWord) { editor.endTransaction(); return; } // Prepare a range for backward search. var startRange = editor.document.createRange(); startRange.setStart(wholeRange.endContainer, wholeRange.endOffset); startRange.setEnd(wholeRange.endContainer, wholeRange.endOffset); var endRange = editor.document.createRange(); endRange.setStart(wholeRange.startContainer, wholeRange.startOffset); endRange.setEnd(wholeRange.startContainer, wholeRange.startOffset); var finder = Components.classes["@mozilla.org/embedcomp/rangefind;1"].createInstance().QueryInterface(Components.interfaces.nsIFind); finder.findBackwards = true; var lastWordRange = finder.Find(lastWord, wholeRange, startRange, endRange); if (!lastWordRange) { // That should actually never happen, as we know the word is there. editor.endTransaction(); return; } // Replace the keyword. editor.selection.removeAllRanges(); editor.selection.addRange(lastWordRange); var text = this.mKeywords[lastWord.toLowerCase()]; editor.endTransaction(); e.stopPropagation(); e.preventDefault(); await this.insertTemplate(text[0], text[1]); } }, /* * OBSERVERS */ observe: function(aSubject, aTopic, aData) { switch(aTopic) { case "updatesettings": this.updateGUI(); break; case "updatetoolbar": this.visibleToolbar(); break; } } } quicktext-4.1/chrome/content/scripts/000077500000000000000000000000001410655052500200205ustar00rootroot00000000000000quicktext-4.1/chrome/content/scripts/messenger.js000066400000000000000000000015011410655052500223430ustar00rootroot00000000000000// Import any needed modules. var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { gQuicktext } = ChromeUtils.import("chrome://quicktext/content/modules/wzQuicktext.jsm"); // Load an additional JavaScript file. Services.scriptloader.loadSubScript("chrome://quicktext/content/main.js", window, "UTF-8"); function onLoad(activatedWhileWindowOpen) { WL.injectCSS("resource://quicktext/skin/quicktext.css"); WL.injectElements(` `, ["chrome://quicktext/locale/quicktext.dtd"]); gQuicktext.loadSettings(false); } function onUnload(deactivatedWhileWindowOpen) { } quicktext-4.1/chrome/content/scripts/messengercompose.js000066400000000000000000000162271410655052500237440ustar00rootroot00000000000000// Import any needed modules. var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); // Load an additional JavaScript file. Services.scriptloader.loadSubScript("chrome://quicktext/content/quicktext.js", window, "UTF-8"); function onLoad(activatedWhileWindowOpen) { WL.injectCSS("resource://quicktext/skin/quicktext.css"); WL.injectElements(` `, ["chrome://quicktext/locale/quicktext.dtd"]); window.quicktext.load(); // event listener to insert custom / dynamic default template window.addEventListener("compose-window-init", function() { window.quicktext.windowInit(); }, true); window.addEventListener("compose-window-reopen", function() { window.quicktext.reload(); }, true); } function onUnload(deactivatedWhileWindowOpen) { window.quicktext.unload(); delete window.quicktext; } quicktext-4.1/chrome/content/scripts/preferences.js000066400000000000000000000111201410655052500226520ustar00rootroot00000000000000/* * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * * This file is intended to be used in the WebExtension background page, * in popup pages, option pages, content pages as well as in legacy chrome * windows (together with the WindowListener API). * The preferences will be loaded asynchronously from the WebExtension * storage and stored in a local pref obj, so all further access can be done * synchronously. * If preferences are changed elsewhere, the local pref obj will be updated. * * Version: 1.2 * - Bugfix: move to a different saving scheme, as storage.local.get() without * providing a value to get them all, may cause an TransactionInactiveError in * IndexedDB.jsm * * Version: 1.1 * - Bugfix: use messenger.storage instead of browser.storage * * Version: 1.0 * * Author: John Bieling (john@thunderbird.net) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Set the storage area of userPrefs either to "local" or "sync". Setting it to // "sync" is a hack to keep preferences stored even after the add-on has been // removed and installed again (storage.local is cleared upon add-on removal). // Even though Thunderbird does not actually have a sync backend, storage.sync // is not cleared on add-on removal to mimic syncing stored values. // Hint: Reloading/Updating an add-on does not clear storage.local. const userPrefStorageArea = "sync"; var preferences = { _userPrefs : {}, _defaultPrefs : {}, // Get pref value from local pref obj. getPref: function(aName, aFallback = null) { // Get defaultPref. let defaultPref = this._defaultPrefs.hasOwnProperty(aName) ? this._defaultPrefs[aName] : aFallback; // Check if userPref type is defaultPref type and return default if no match. if (this._userPrefs.hasOwnProperty(aName)) { let userPref = this._userPrefs[aName]; if (typeof defaultPref == typeof userPref) { return userPref; } console.log("Type of defaultPref <" + defaultPref + ":" + typeof defaultPref + "> does not match type of userPref <" + userPref + ":" + typeof userPref + ">. Fallback to defaultPref.") } // Fallback to default value. return defaultPref; }, // Set pref value by updating local pref obj and updating storage. setPref: function(aName, aValue) { this._userPrefs[aName] = aValue; messenger.storage[userPrefStorageArea].set({ userPrefs : this._userPrefs }); }, // Remove a preference (calls to getPref will return default value) clearPref: function(aName, aValue) { delete this._userPrefs[aName]; messenger.storage[userPrefStorageArea].set({ userPrefs : this._userPrefs }); }, // Listener for storage changes. storageChanged: function (changes, area) { let changedItems = Object.keys(changes); for (let item of changedItems) { if (area == userPrefStorageArea && item == "userPrefs") { this._userPrefs = changes.userPrefs.newValue; } if (area == "local" && item == "defaultPrefs") { this._defaultPrefs = changes.defaultPrefs.newValue; } } }, // Initialize the local pref obj by loading userPrefs and defaultPrefs from // WebExtension storage. If a defaults obj is given, the defaults in storage // are updated/set. init: async function(defaults = null) { this._userPrefs = {}; this._defaultPrefs = {}; // Store user prefs into the local userPrefs obj. this._userPrefs = (await messenger.storage[userPrefStorageArea].get("userPrefs")).userPrefs || {}; // If defaults are given, push them into storage.local if (defaults) { await messenger.storage.local.set({ defaultPrefs : defaults }); // We need to migration from prefsV1 to prefsV2 for(let prefName of Object.keys(defaults)) { let prefV1Value = (await browser.storage[userPrefStorageArea].get("pref.value." + prefName))["pref.value." + prefName]; if (prefV1Value) { await browser.storage[userPrefStorageArea].remove("pref.value." + prefName); preferences.setPref(prefName, prefV1Value); } } } this._defaultPrefs = (await messenger.storage.local.get("defaultPrefs")).defaultPrefs || {}; // Add storage change listener. if (!(await messenger.storage.onChanged.hasListener(this.storageChanged))) { await messenger.storage.onChanged.addListener(this.storageChanged); } }, } quicktext-4.1/chrome/content/settings.js000066400000000000000000001273731410655052500205440ustar00rootroot00000000000000var { gQuicktext } = ChromeUtils.import("chrome://quicktext/content/modules/wzQuicktext.jsm"); var { quicktextUtils } = ChromeUtils.import("chrome://quicktext/content/modules/utils.jsm"); var { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); var quicktext = { mChangesMade: false, mTextChangesMade: [], mScriptChangesMade: [], mGeneralChangesMade: [], mLoaded: false, mTreeArray: [], mCollapseState: [], mScriptIndex: null, mPickedIndex: null, mOS: "WINNT" , init: async function() { if (!this.mLoaded) { this.mLoaded = true; // add OS as attribute to outer dialog document.getElementById('quicktextSettingsWindow').setAttribute("OS", OS.Constants.Sys.Name); console.log("Adding attribute 'OS' = '"+ OS.Constants.Sys.Name +"' to settings dialog element."); var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULAppInfo).QueryInterface(Components.interfaces.nsIXULRuntime); this.mOS = appInfo.OS; gQuicktext.addObserver(this); var hasLoadedBefore = !(await gQuicktext.loadSettings(false)); var states = gQuicktext.collapseState; if (states != "") { states = states.split(/;/); for (var i = 0; i < states.length; i++) this.mCollapseState[i] = (states[i] == "1"); } var groupLength = gQuicktext.getGroupLength(true); if (states.length < groupLength) { for (var i = states.length; i < groupLength; i++) this.mCollapseState[i] = true; } if (hasLoadedBefore) { gQuicktext.startEditing(); this.updateGUI(); } // window.resizeTo(gQuicktext.getSettingsWindowSize(0), gQuicktext.getSettingsWindowSize(1)); document.getElementById('tabbox-main').selectedIndex = 1; document.getElementById('text-keyword').addEventListener("keypress", function(e) { quicktext.noSpaceForKeyword(e); }, false); this.disableSave(); document.documentElement.getButton("extra1").addEventListener("command", function(e) { quicktext.save(); }, false); } } , unload: function() { gQuicktext.removeObserver(this); var states = []; for (var i = 0; i < this.mCollapseState.length; i++) states[i] = (this.mCollapseState[i]) ? "1" : ""; gQuicktext.collapseState = states.join(";"); document.getElementById('text-keyword').removeEventListener("keypress", function(e) { quicktext.noSpaceForKeyword(e); }, false); } , close: function(aClose) { this.saveText(); this.saveScript(); if (this.mChangesMade) { promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService); if (promptService) { result = promptService.confirmEx(window, gQuicktext.mStringBundle.GetStringFromName("saveMessageTitle"), gQuicktext.mStringBundle.GetStringFromName("saveMessage"), (promptService.BUTTON_TITLE_SAVE * promptService.BUTTON_POS_0) + (promptService.BUTTON_TITLE_CANCEL * promptService.BUTTON_POS_1) + (promptService.BUTTON_TITLE_DONT_SAVE * promptService.BUTTON_POS_2), null, null, null, null, {value:0}); switch (result) { // Cancel case 1: return false; // Save case 0: this.save(); break; // Quit case 2: break; } } } if (aClose) window.close(); return true; } , save: function() { this.saveText(); this.saveScript(); if (document.getElementById("checkbox-viewPopup")) gQuicktext.viewPopup = document.getElementById("checkbox-viewPopup").checked; if (document.getElementById("text-defaultImport")) gQuicktext.defaultImport = document.getElementById("text-defaultImport").value; if (document.getElementById("select-shortcutModifier")) gQuicktext.shortcutModifier = document.getElementById("select-shortcutModifier").value; if (document.getElementById("checkbox-shortcutTypeAdv")) gQuicktext.shortcutTypeAdv = document.getElementById("checkbox-shortcutTypeAdv").checked; if (document.getElementById("select-keywordKey")) gQuicktext.keywordKey = document.getElementById("select-keywordKey").value; if (document.getElementById("checkbox-collapseGroup")) gQuicktext.collapseGroup = document.getElementById("checkbox-collapseGroup").checked; gQuicktext.saveSettings(); this.mChangesMade = false; this.mTextChangesMade = []; this.mScriptChangesMade = []; this.mGeneralChangesMade = []; this.disableSave(); this.updateGUI(); } , shortcutTypeAdv: function() { if (this.mOS.substr(0, 3).toLowerCase() == "mac" || (this.mOS.substr(0, 3).toLowerCase() == "win" && document.getElementById('select-shortcutModifier').value == "alt")) return false; return document.getElementById('checkbox-shortcutTypeAdv').checked; } , saveText: function() { if (this.mPickedIndex != null) { if (this.mPickedIndex[1] > -1) { var title = document.getElementById('text-title').value; if (title.replace(/[\s]/g, '') == "") title = gQuicktext.mStringBundle.GetStringFromName("newTemplate"); this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'name', title); this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'text', document.getElementById('text').value); if (this.shortcutTypeAdv()) this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'shortcut', document.getElementById('text-shortcutAdv').value); else this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'shortcut', document.getElementById('text-shortcutBasic').value); this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'type', document.getElementById('text-type').value); this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'keyword', document.getElementById('text-keyword').value.replace(/[\s]/g, '')); this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'subject', document.getElementById('text-subject').value); this.saveTextCell(this.mPickedIndex[0], this.mPickedIndex[1], 'attachments', document.getElementById('text-attachments').value); } else { var title = document.getElementById('text-title').value; if (title.replace(/[\s]/g, '') == "") title = gQuicktext.mStringBundle.GetStringFromName("newGroup"); this.saveGroupCell(this.mPickedIndex[0], 'name', title); } } } , saveTextCell: function (aGroupIndex, aTextIndex, aColumn, aValue) { var text = gQuicktext.getText(aGroupIndex, aTextIndex, true); if (typeof text[aColumn] != "undefined" && text[aColumn] != aValue) { text[aColumn] = aValue; this.changesMade(); return true; } return false; } , saveGroupCell: function (aGroupIndex, aColumn, aValue) { var group = gQuicktext.getGroup(aGroupIndex, true); if (typeof group[aColumn] != "undefined" && group[aColumn] != aValue) { group[aColumn] = aValue; this.changesMade(); return true; } return false; } , saveScript: function() { if (this.mScriptIndex != null) { var title = document.getElementById('script-title').value; if (title.replace(/[\s]/g, '') == "") title = gQuicktext.mStringBundle.GetStringFromName("newScript"); this.saveScriptCell(this.mScriptIndex, 'name', title); this.saveScriptCell(this.mScriptIndex, 'script', document.getElementById('script').value); } } , saveScriptCell: function (aIndex, aColumn, aValue) { var script = gQuicktext.getScript(aIndex, true); if (typeof script[aColumn] != "undefined" && script[aColumn] != aValue) { script[aColumn] = aValue; this.changesMade(); return true; } return false; } , noSpaceForKeyword: function(e) { if (e.charCode == KeyEvent.DOM_VK_SPACE) { e.stopPropagation(); e.preventDefault(); } } , checkForGeneralChanges: function(aIndex) { var ids = ['checkbox-viewPopup', 'checkbox-collapseGroup', 'select-shortcutModifier', 'checkbox-shortcutTypeAdv', 'select-keywordKey', 'text-defaultImport']; var type = ['checked', 'checked', 'value', 'checked', 'value', 'value']; var keys = ['viewPopup', 'collapseGroup', 'shortcutModifier', 'shortcutTypeAdv', 'keywordKey', 'defaultImport']; if (typeof ids[aIndex] == 'undefined') return; var value = document.getElementById(ids[aIndex])[type[aIndex]]; if (gQuicktext[keys[aIndex]] != value) this.generalChangeMade(aIndex); else this.noGeneralChangeMade(aIndex); } , checkForTextChanges: function(aIndex) { if (!this.mPickedIndex) return; var ids = ['text-title', 'text', 'text-shortcutBasic', 'text-type', 'text-keyword', 'text-subject', 'text-attachments']; var keys = ['name', 'text', 'shortcut', 'type', 'keyword', 'subject', 'attachments']; if (this.shortcutTypeAdv()) ids[2] = 'text-shortcutAdv'; var value = document.getElementById(ids[aIndex]).value; switch (aIndex) { case 0: if (value.replace(/[\s]/g, '') == "") if (this.mPickedIndex[1] > -1) value = gQuicktext.mStringBundle.GetStringFromName("newTemplate"); else value = gQuicktext.mStringBundle.GetStringFromName("newGroup"); break; case 2: if (this.shortcutTypeAdv()) { value = value.replace(/[^\d]/g, ''); document.getElementById(ids[aIndex]).value = value; } case 4: value = value.replace(/[\s]/g, ''); document.getElementById(ids[aIndex]).value = value; break; } if (this.mPickedIndex[1] > -1) { if (gQuicktext.getText(this.mPickedIndex[0], this.mPickedIndex[1], true)[keys[aIndex]] != value) this.textChangeMade(aIndex); else this.noTextChangeMade(aIndex); } else { if (gQuicktext.getGroup(this.mPickedIndex[0], true)[keys[aIndex]] != value) this.textChangeMade(aIndex); else this.noTextChangeMade(aIndex); } if (aIndex == 0 || aIndex == 2) { var selectedIndex = document.getElementById('group-tree').view.selection.currentIndex; if (aIndex == 0) { this.mTreeArray[selectedIndex][6] = value; } else { this.mTreeArray[selectedIndex][7] = value; } document.getElementById('group-tree').invalidateRow(selectedIndex); this.updateVariableGUI(); } } , checkForScriptChanges: function(aIndex) { if (this.mScriptIndex == null) return; var ids = ['script-title', 'script']; var keys = ['name', 'script']; var value = document.getElementById(ids[aIndex]).value; switch (aIndex) { case 0: if (value.replace(/[\s]/g, '') == "") value = gQuicktext.mStringBundle.GetStringFromName("newScript"); break; } if (gQuicktext.getScript(this.mScriptIndex, true)[keys[aIndex]] != value) this.scriptChangeMade(aIndex); else this.noScriptChangeMade(aIndex); if (aIndex == 0) { this.updateVariableGUI(); var listItem = document.getElementById('script-list').getItemAtIndex(this.mScriptIndex); listItem.firstChild.value = value; } } , changesMade: function() { this.mChangesMade = true; this.enableSave(); } , anyChangesMade: function() { if (this.textChangesMade() || this.scriptChangesMade() || this.generalChangesMade()) return true; return false; } , generalChangesMade: function() { for (var i = 0; i < this.mGeneralChangesMade.length; i++) { if (typeof this.mGeneralChangesMade[i] != "undefined" && this.mGeneralChangesMade[i] == true) return true; } return false; } , generalChangeMade: function(aIndex) { this.enableSave(); this.mGeneralChangesMade[aIndex] = true; } , noGeneralChangeMade: function(aIndex) { this.mGeneralChangesMade[aIndex] = false; if (!this.mChangesMade && !this.anyChangesMade()) this.disableSave(); } , textChangesMade: function() { for (var i = 0; i < this.mTextChangesMade.length; i++) { if (typeof this.mTextChangesMade[i] != "undefined" && this.mTextChangesMade[i] == true) return true; } return false; } , textChangeMade: function(aIndex) { this.enableSave(); this.mTextChangesMade[aIndex] = true; } , noTextChangeMade: function(aIndex) { this.mTextChangesMade[aIndex] = false; if (!this.mChangesMade && !this.anyChangesMade()) this.disableSave(); } , scriptChangesMade: function() { for (var i = 0; i < this.mScriptChangesMade.length; i++) { if (typeof this.mScriptChangesMade[i] != "undefined" && this.mScriptChangesMade[i] == true) return true; } return false; } , scriptChangeMade: function(aIndex) { this.enableSave(); this.mScriptChangesMade[aIndex] = true; } , noScriptChangeMade: function(aIndex) { this.mScriptChangesMade[aIndex] = false; if (!this.mChangesMade && !this.anyChangesMade()) this.disableSave(); } , /* * GUI CHANGES */ updateGUI: function() { // Set the date/time in the variablemenu var timeStamp = new Date(); let fields = ["date-short", "date-long", "date-monthname", "time-noseconds", "time-seconds"]; for (let i=0; i < fields.length; i++) { let field = fields[i]; let fieldtype = field.split("-")[0]; if (document.getElementById(field)) { document.getElementById(field).setAttribute("label", gQuicktext.mStringBundle.formatStringFromName(fieldtype, [quicktextUtils.dateTimeFormat(field, timeStamp)], 1)); } } // Update info in the generalsettings tab if (document.getElementById("checkbox-viewPopup")) document.getElementById("checkbox-viewPopup").checked = gQuicktext.viewPopup; if (document.getElementById("checkbox-collapseGroup")) document.getElementById("checkbox-collapseGroup").checked = gQuicktext.collapseGroup; if (document.getElementById("select-shortcutModifier")) document.getElementById("select-shortcutModifier").value = gQuicktext.shortcutModifier; if (document.getElementById("checkbox-shortcutTypeAdv")) { var elem = document.getElementById("checkbox-shortcutTypeAdv"); elem.checked = gQuicktext.shortcutTypeAdv; this.shortcutModifierChange(); } if (document.getElementById("text-defaultImport")) document.getElementById("text-defaultImport").value = gQuicktext.defaultImport; if (document.getElementById("select-keywordKey")) document.getElementById("select-keywordKey").value = gQuicktext.keywordKey; // Update the variable menu this.updateVariableGUI(); // Update Script list this.updateScriptGUI(); // Update the tree this.buildTreeGUI(); // Update the remove and add buttons this.updateButtonStates(); } , updateVariableGUI: function() { // Set all other text in the variablemenu var topParent = document.getElementById('quicktext-other-texts'); for (var i = topParent.childNodes.length-1; i >= 0 ; i--) topParent.removeChild(topParent.childNodes[i]); var groupLength = gQuicktext.getGroupLength(true); if (groupLength > 0) { topParent.removeAttribute('hidden'); parent = document.createXULElement("menupopup"); parent = topParent.appendChild(parent); for(var i = 0; i < groupLength; i++) { var textLength = gQuicktext.getTextLength(i, true); if (textLength > 0) { var group = gQuicktext.getGroup(i, true); var groupElem = document.createXULElement("menu"); groupElem.setAttribute('label', group.name); groupElem = parent.appendChild(groupElem); groupParent = document.createXULElement("menupopup"); groupParent = groupElem.appendChild(groupParent); for (var j = 0; j < textLength; j++) { var textElem = document.createXULElement("menuitem"); var text = gQuicktext.getText(i, j, true); textElem.setAttribute('label', text.name); textElem.setAttribute('group', group.name); textElem.addEventListener("command", function() { quicktext.insertVariable("TEXT="+ this.getAttribute("group") +"|"+ this.getAttribute("label")); }); textElem = groupParent.appendChild(textElem); } } } } else topParent.setAttribute('hidden', true); var topParent = document.getElementById('variables-scripts'); for (var i = topParent.childNodes.length-1; i >= 0 ; i--) topParent.removeChild(topParent.childNodes[i]); var scriptLength = gQuicktext.getScriptLength(true); if (scriptLength > 0) { topParent.removeAttribute('hidden'); parent = document.createXULElement("menupopup"); parent = topParent.appendChild(parent); for (var i = 0; i < scriptLength; i++) { var script = gQuicktext.getScript(i, true); var textElem = document.createXULElement("menuitem"); textElem.setAttribute('label', script.name); textElem.addEventListener("command", function() { quicktext.insertVariable("SCRIPT="+ this.getAttribute("label")); }); textElem = parent.appendChild(textElem); } } else topParent.setAttribute('hidden', true); } , disableShortcuts: function(aShortcut) { var grouplist = document.getElementById('popup-shortcutBasic'); for (var i = 0; i <= 10; i++) grouplist.childNodes[i].removeAttribute("disabled"); var groupLength = gQuicktext.getGroupLength(true); for (var i = 0; i < groupLength; i++) { var textLength = gQuicktext.getTextLength(i, true); for (var j = 0; j < textLength; j++) { var shortcut = gQuicktext.getText(i, j, true).shortcut; var selectedIndex = (shortcut == "0") ? 10 : shortcut; if (shortcut != "" && shortcut != aShortcut && grouplist.childNodes[selectedIndex]) grouplist.childNodes[selectedIndex].setAttribute("disabled", true); } } } , disableSave: function() { document.documentElement.getButton("extra1").setAttribute("disabled", true); document.getElementById("toolbar-save").setAttribute("disabled", true); } , enableSave: function() { document.documentElement.getButton("extra1").removeAttribute("disabled"); document.getElementById("toolbar-save").removeAttribute("disabled"); } , /* * INSERT VARIABLES */ insertVariable: function(aStr) { var textbox = document.getElementById("text-subject"); if (!textbox.getAttribute("focused")) var textbox = document.getElementById("text"); var selStart = textbox.selectionStart; var selEnd = textbox.selectionEnd; var selLength = textbox.textLength; var s1 = (textbox.value).substring(0,selStart); var s2 = (textbox.value).substring(selEnd, selLength) textbox.value = s1 + "[[" + aStr + "]]" + s2; var selNewStart = selStart + 4 + aStr.length; textbox.setSelectionRange(selNewStart, selNewStart); this.enableSave(); } , insertFileVariable: async function() { if ((file = await gQuicktext.pickFile(window, 2, 0, gQuicktext.mStringBundle.GetStringFromName("insertFile"))) != null) this.insertVariable('FILE=' + file.path); this.enableSave(); } , insertImageVariable: async function() { if ((file = await gQuicktext.pickFile(window, 4, 0, gQuicktext.mStringBundle.GetStringFromName("insertImage"))) != null) this.insertVariable('IMAGE=' + file.path); this.enableSave(); } , /* * IMPORT/EXPORT FUNCTIONS */ exportTemplatesToFile: async function() { if ((file = await gQuicktext.pickFile(window, 3, 1, gQuicktext.mStringBundle.GetStringFromName("exportFile"))) != null) gQuicktext.exportTemplatesToFile(file); } , importTemplatesFromFile: async function() { if ((file = await gQuicktext.pickFile(window, 3, 0, gQuicktext.mStringBundle.GetStringFromName("importFile"))) != null) { this.saveText(); this.saveScript(); var length = this.mTreeArray.length; gQuicktext.importFromFile(file, 0, false, true); this.changesMade(); this.makeTreeArray(); document.getElementById('group-tree').rowCountChanged(length-1, this.mTreeArray.length-length); this.updateButtonStates(); } } , exportScriptsToFile: async function() { if ((file = await gQuicktext.pickFile(window, 3, 1, gQuicktext.mStringBundle.GetStringFromName("exportFile"))) != null) gQuicktext.exportScriptsToFile(file); } , importScriptsFromFile: async function() { if ((file = await gQuicktext.pickFile(window, 3, 0, gQuicktext.mStringBundle.GetStringFromName("importFile"))) != null) { this.saveText(); this.saveScript(); gQuicktext.importFromFile(file, 0, false, true); this.changesMade(); this.updateScriptGUI(); this.updateButtonStates(); } } , browseAttachment: async function() { if ((file = await gQuicktext.pickFile(window, -1, 0, gQuicktext.mStringBundle.GetStringFromName("attachmentFile"))) != null) { var filePath = file.path; var attachments = document.getElementById('text-attachments').value; if (attachments != "") document.getElementById('text-attachments').value = attachments +";"+ filePath; else document.getElementById('text-attachments').value = filePath; this.checkForTextChanges(6); } } , pickScript: function() { var index = document.getElementById('script-list').value; if (index == null) { document.getElementById('script-title').value = ""; document.getElementById('script').value = ""; this.mScriptIndex = null; document.getElementById('script-title').disabled = true; document.getElementById('script').hidden = true; return; } document.getElementById('script').hidden = false; if (this.mScriptIndex != index) { if (this.scriptChangesMade()) { this.changesMade(); this.mScriptChangesMade = []; } this.saveScript(); } this.mScriptIndex = index; var script = gQuicktext.getScript(index, true); let disabled = (script.type == 1); document.getElementById('script-title').value = script.name; document.getElementById('script').value = script.script; document.getElementById('script-title').disabled = disabled; document.getElementById('script').disabled = disabled; if (disabled) document.getElementById('script-button-remove').setAttribute("disabled", true); else document.getElementById('script-button-remove').removeAttribute("disabled"); } , pickText: function() { var index = document.getElementById('group-tree').view.selection.currentIndex; if (!this.mTreeArray[index]) { document.getElementById('text-caption').textContent = gQuicktext.mStringBundle.GetStringFromName("group"); document.getElementById('text-title').value = ""; this.showElement("group", true); this.mPickedIndex = null; return; } groupIndex = this.mTreeArray[index][0]; textIndex = this.mTreeArray[index][1]; if (this.mPickedIndex && this.textChangesMade()) { this.changesMade(); this.mTextChangesMade = []; this.saveText(); } this.mPickedIndex = [groupIndex, textIndex]; if (textIndex > -1) { var text = gQuicktext.getText(groupIndex, textIndex, true); document.getElementById('text-caption').textContent = gQuicktext.mStringBundle.GetStringFromName("template"); document.getElementById('text-title').value = text.name; document.getElementById('text').value = text.text; document.getElementById('text-keyword').value = text.keyword; document.getElementById('text-subject').value = text.subject; document.getElementById('text-attachments').value = text.attachments; document.getElementById('label-shortcutModifier').value = gQuicktext.mStringBundle.GetStringFromName(document.getElementById('select-shortcutModifier').value +"Key") +"+"; if (this.shortcutTypeAdv()) { var elem = document.getElementById('text-shortcutAdv'); elem.value = text.shortcut; elem.hidden = false; document.getElementById('text-shortcutBasic').hidden = true; } else { var shortcut = text.shortcut; var elem = document.getElementById('text-shortcutBasic'); if (shortcut < 10) elem.selectedIndex = (shortcut == "0") ? 10 : shortcut; else elem.selectedIndex = 0; elem.hidden = false; document.getElementById('text-shortcutAdv').hidden = true; this.disableShortcuts(shortcut); } var type = text.type; if (!(type > 0)) type = 0; document.getElementById('text-type').selectedIndex = type; } else { document.getElementById('text-caption').textContent = gQuicktext.mStringBundle.GetStringFromName("group"); document.getElementById("text-title").value = gQuicktext.getGroup(groupIndex, true).name; document.getElementById("text").value = ""; document.getElementById("text-keyword").value = ""; document.getElementById("text-subject").value = ""; document.getElementById("text-attachments").value = ""; } var disabled = false; if (gQuicktext.getGroup(groupIndex, true).type > 0) { document.getElementById("group-button-remove").setAttribute("disabled", true); document.getElementById("group-button-add-text").setAttribute("disabled", true); disabled = true; } else { document.getElementById("group-button-remove").removeAttribute("disabled"); document.getElementById("group-button-add-text").removeAttribute("disabled"); } if (textIndex < 0) this.showElement("group", disabled); else this.showElement("text", disabled); } , showElement: function(aType, aDisabled) { var elements = document.getElementsByAttribute("candisable", "true"); for (var i = 0; i < elements.length; i++) { if (aDisabled) elements[i].setAttribute("disabled", true); else elements[i].removeAttribute("disabled"); } var elements = document.getElementsByAttribute("showfor", "*"); for (var i = 0; i < elements.length; i++) { var types = elements[i].getAttribute("showfor").split(","); var found = false; for (var type = 0; type < types.length; type++) { if (types[type] == aType) found = true; } if (found) elements[i].hidden = false; else elements[i].hidden = true; } } , /* * Add/Remove groups/templates */ addGroup: function() { var title = gQuicktext.mStringBundle.GetStringFromName("newGroup"); this.saveText(); gQuicktext.addGroup(title, true); this.mCollapseState.push(true); this.makeTreeArray(); var treeObject = document.getElementById('group-tree'); treeObject.rowCountChanged(this.mTreeArray.length-1, 1); treeObject.invalidateRow(this.mTreeArray.length-1); selectedIndex = this.mTreeArray.length - 1; this.selectTreeRow(selectedIndex); this.updateButtonStates(); this.changesMade(); var titleElem = document.getElementById('text-title'); titleElem.focus(); titleElem.setSelectionRange(0, title.length); } , addText: function() { var title = gQuicktext.mStringBundle.GetStringFromName("newTemplate"); this.saveText(); var groupIndex = -1; if (this.mPickedIndex) groupIndex = this.mPickedIndex[0]; var groupLength = gQuicktext.getGroupLength(true); if (groupIndex == -1) { if (groupLength == 0) return; else groupIndex = 0; } gQuicktext.addText(groupIndex, title, true); this.makeTreeArray(); var selectedIndex = -1; for (var i = 0; i <= groupIndex; i++) { selectedIndex++; if (this.mCollapseState[i]) selectedIndex += gQuicktext.getTextLength(i, true); } var treeObject = document.getElementById('group-tree'); treeObject.rowCountChanged(selectedIndex-1, 1); treeObject.invalidateRow(selectedIndex); this.selectTreeRow(selectedIndex); this.updateButtonStates(); this.changesMade(); var titleElem = document.getElementById('text-title'); titleElem.focus(); titleElem.setSelectionRange(0, title.length); } , removeText: function() { this.saveText(); if (this.mPickedIndex) { var groupIndex = this.mPickedIndex[0]; var textIndex = this.mPickedIndex[1]; var title = gQuicktext.getGroup(groupIndex, true).name; if (textIndex > -1) title = gQuicktext.getText(groupIndex, textIndex, true).name; if (confirm (gQuicktext.mStringBundle.formatStringFromName("remove", [title], 1))) { this.mPickedIndex = null; var textLength = gQuicktext.getTextLength(groupIndex, true); var selectedIndex = document.getElementById('group-tree').view.selection.currentIndex; var moveSelectionUp = false; if (this.mTreeArray[selectedIndex+1] && this.mTreeArray[selectedIndex+1][2] < this.mTreeArray[selectedIndex][2]) moveSelectionUp = true; var treeObject = document.getElementById('group-tree'); if (textIndex == -1) { gQuicktext.removeGroup(groupIndex, true); if (this.mCollapseState[groupIndex]) treeObject.rowCountChanged(selectedIndex, -(textLength+1)); else treeObject.rowCountChanged(selectedIndex, -1); this.makeTreeArray(); treeObject.invalidate(); } else { gQuicktext.removeText(groupIndex, textIndex, true); treeObject.rowCountChanged(selectedIndex, -1); this.makeTreeArray(); treeObject.invalidate(); } this.updateVariableGUI(); this.updateButtonStates(); this.changesMade(); var selectedRow = false; if (moveSelectionUp) { selectedRow = true; this.selectTreeRow(selectedIndex-1); } var rowCount = this.mTreeArray.length -1; if (selectedIndex > rowCount || selectedIndex == -1) { selectedRow = true; this.selectTreeRow(rowCount); } if (!selectedRow) this.selectTreeRow(selectedIndex); } } } , getCommunityScripts: function() { let ioservice = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); let uriToOpen = ioservice.newURI("https://github.com/jobisoft/quicktext/wiki/Community-scripts", null, null); let extps = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService); extps.loadURI(uriToOpen, null); } , addScript: function() { this.saveScript(); var title = gQuicktext.mStringBundle.GetStringFromName("newScript"); gQuicktext.addScript(title, true); this.updateScriptGUI(); this.updateButtonStates(); var listElem = document.getElementById('script-list'); selectedIndex = listElem.getRowCount()-1; listElem.selectedIndex = selectedIndex; this.changesMade(); var titleElem = document.getElementById('script-title'); titleElem.focus(); titleElem.setSelectionRange(0, title.length); } , removeScript: function() { this.saveScript(); var scriptIndex = document.getElementById('script-list').value; if (scriptIndex != null) { var title = gQuicktext.getScript(scriptIndex, true).name; if (confirm (gQuicktext.mStringBundle.formatStringFromName("remove", [title], 1))) { gQuicktext.removeScript(scriptIndex, true); this.changesMade(); if (gQuicktext.getScriptLength(true) > 0) { var selectedIndex = document.getElementById('script-list').selectedIndex -1; if (selectedIndex < 0) selectedIndex = 0; this.mScriptIndex = selectedIndex; } else { this.mScriptIndex = null; selectedIndex = -1; } document.getElementById('script-list').selectedIndex = selectedIndex; this.updateScriptGUI(); this.updateVariableGUI(); this.updateButtonStates(); } } } , updateScriptGUI: function() { // Update the listmenu in the scripttab and the variable-menu var scriptLength = gQuicktext.getScriptLength(true); listElem = document.getElementById('script-list'); var selectedIndex = listElem.selectedIndex; var oldLength = listElem.getRowCount(); if (scriptLength > 0) { for (var i = 0; i < scriptLength; i++) { var script = gQuicktext.getScript(i, true); if (i < oldLength) { var listItem = listElem.getItemAtIndex(i); listItem.firstChild.value = script.name; listItem.value = i; } else { let newItem = document.createXULElement("richlistitem"); newItem.value = i; let newItemLabel = document.createXULElement("label"); newItemLabel.value = script.name; newItem.appendChild(newItemLabel); listElem.appendChild(newItem); } } } if (oldLength > scriptLength) { for (var i = scriptLength; i < oldLength; i++) listElem.getItemAtIndex(scriptLength).remove(); } if (selectedIndex >= 0) listElem.selectedIndex = selectedIndex; else if (scriptLength > 0) listElem.selectedIndex = 0; else listElem.selectedIndex = -1; this.pickScript(); } , /* * Update the treeview */ makeTreeArray: function() { this.mTreeArray = []; var k = 0; var groupLength = gQuicktext.getGroupLength(true); if (this.mCollapseState.length < groupLength) { for (var i = this.mCollapseState.length; i < groupLength; i++) this.mCollapseState[i] = true; } else if (this.mCollapseState.length > groupLength) this.mCollapseState.splice(groupLength, this.mCollapseState.length - groupLength); for (var i = 0; i < groupLength; i++) { var groupIndex = k; var textLength = gQuicktext.getTextLength(i, true); this.mTreeArray[k] = [i, -1, 0, -1, true, textLength, gQuicktext.getGroup(i, true).name, '']; k++; if (!this.mCollapseState[i]) continue; for (var j = 0; j < textLength; j++) { var text = gQuicktext.getText(i, j, true); var shortcut = text.shortcut; this.mTreeArray[k] = [i, j, 1, groupIndex, false, 0, text.name, shortcut]; k++; } } } , updateTreeGUI: function() { // maybe } , buildTreeGUI: function() { this.makeTreeArray(); var treeview = { rowCount: this.mTreeArray.length, lastIndex: null, isContainer: function(aRow) { return (quicktext.mTreeArray[aRow][1] == -1); }, isContainerOpen: function(aRow) { return quicktext.mCollapseState[quicktext.mTreeArray[aRow][0]]; }, isContainerEmpty: function(aRow) { return (quicktext.mTreeArray[aRow][5] == 0); }, isSeparator: function(aRow) { return false; }, isSorted: function(aRow) { return false; }, isEditable: function(aRow) { return false; }, hasNextSibling: function(aRow, aAfter) { return (quicktext.mTreeArray[aAfter+1] && quicktext.mTreeArray[aRow][2] == quicktext.mTreeArray[aAfter+1][2] && quicktext.mTreeArray[aRow][3] == quicktext.mTreeArray[aAfter+1][3]); }, getLevel: function(aRow) { return quicktext.mTreeArray[aRow][2]; }, getImageSrc: function(aRow, aCol) { return null; }, getParentIndex: function(aRow) { return quicktext.mTreeArray[aRow][3]; }, getRowProperties: function(aRow, aProps) { }, getCellProperties: function(aRow, aCol, aProps) { }, getColumnProperties: function(aColid, aCol, aProps) { }, getProgressMode: function(aRow, aCol) { }, getCellValue: function(aRow, aCol) { return null; }, canDropBeforeAfter: function(aRow, aBefore) { if (aBefore) return this.canDrop(aRow, -1); return this.canDrop(aRow, 1); }, canDropOn: function(aRow) { return this.canDrop(aRow, 0); }, canDrop: function(aRow, aOrient) { var index = document.getElementById('group-tree').view.selection.currentIndex; if (index == aRow) return false; // Can only drop templates on groups if (aOrient == 0) { if (quicktext.mTreeArray[index][2] > 0 && quicktext.mTreeArray[aRow][2] == 0) return true; else return false; } // Take care if we drag a group if (quicktext.mTreeArray[index][2] == 0) { if (aOrient < 0 && quicktext.mTreeArray[aRow][2] == 0) return true; if (aOrient > 0 && quicktext.mTreeArray.length-1 == aRow) return true; } // Take care if we drag a template else { if (quicktext.mTreeArray[aRow][2] > 0) return true; } return false; }, drop: function(aRow, aOrient) { quicktext.saveText(); quicktext.mPickedIndex = null; var selectIndex = -1; var index = document.getElementById('group-tree').view.selection.currentIndex; // Droping a group if (quicktext.mTreeArray[index][2] == 0) { var textLength = gQuicktext.getTextLength(quicktext.mTreeArray[index][0], true); if (!quicktext.mCollapseState[quicktext.mTreeArray[index][0]]) textLength = 0; if (aOrient > 0) { gQuicktext.moveGroup(quicktext.mTreeArray[index][0], gQuicktext.getGroupLength(true), true); var state = quicktext.mCollapseState.splice(quicktext.mTreeArray[index][0], 1); state = (state == "false") ? false : true; quicktext.mCollapseState.push(state); selectIndex = quicktext.mTreeArray.length - textLength - 1; } else { gQuicktext.moveGroup(quicktext.mTreeArray[index][0], quicktext.mTreeArray[aRow][0], true); var state = quicktext.mCollapseState.splice(quicktext.mTreeArray[index][0], 1); state = (state == "false") ? false : true; quicktext.mCollapseState.splice(quicktext.mTreeArray[aRow][0], 0, state); selectIndex = (aRow > index) ? aRow - textLength - 1 : aRow; } } // Droping a template else { switch (aOrient) { case 0: var textLength = gQuicktext.getTextLength(quicktext.mTreeArray[aRow][0], true); gQuicktext.moveText(quicktext.mTreeArray[index][0], quicktext.mTreeArray[index][1], quicktext.mTreeArray[aRow][0], textLength, true); selectIndex = (quicktext.mTreeArray[index][0] == quicktext.mTreeArray[aRow][0] || aRow > index) ? aRow + textLength : aRow + textLength + 1; break; case 1: gQuicktext.moveText(quicktext.mTreeArray[index][0], quicktext.mTreeArray[index][1], quicktext.mTreeArray[aRow][0], quicktext.mTreeArray[aRow][1]+1, true); selectIndex = (aRow > index) ? aRow : aRow + 1; break; default: gQuicktext.moveText(quicktext.mTreeArray[index][0], quicktext.mTreeArray[index][1], quicktext.mTreeArray[aRow][0], quicktext.mTreeArray[aRow][1], true); selectIndex = (aRow > index) ? aRow - 1 : aRow; break; } } quicktext.makeTreeArray(); document.getElementById('group-tree').invalidate(); document.getElementById('group-tree').view.selection.select(selectIndex); quicktext.changesMade(); }, getCellText: function(aRow, aCol) { colName = (aCol.id) ? aCol.id : aCol; if (colName == "group") { return quicktext.mTreeArray[aRow][6]; } else if (colName == "shortcut" && quicktext.mTreeArray[aRow][1] > -1) { return quicktext.mTreeArray[aRow][7]; } return ""; }, toggleOpenState: function(aRow) { var state = quicktext.mCollapseState[quicktext.mTreeArray[aRow][0]]; quicktext.mCollapseState[quicktext.mTreeArray[aRow][0]] = !state; quicktext.makeTreeArray(); var treeObject = document.getElementById('group-tree'); if (state) treeObject.rowCountChanged(aRow, -quicktext.mTreeArray[aRow][5]); else treeObject.rowCountChanged(aRow, quicktext.mTreeArray[aRow][5]); treeObject.invalidate(); document.getElementById('group-tree').view.selection.select(aRow); }, setTree: function(aTreebox) { this.treebox=aTreebox; } } var firstVisibleRow = document.getElementById('group-tree').getFirstVisibleRow(); var selectedIndex = document.getElementById('group-tree').view.selection.currentIndex; if (selectedIndex == -1 && this.mTreeArray.length) selectedIndex = 0; document.getElementById('group-tree').view = treeview; document.getElementById('group-tree').scrollToRow(firstVisibleRow); this.selectTreeRow(selectedIndex); this.pickText(); } , selectTreeRow: function(aRow) { document.getElementById('group-tree').view.selection.select(aRow); document.getElementById('group-tree').ensureRowIsVisible(aRow); } , updateButtonStates: function() { // Update the add-buttons if (this.mTreeArray.length) { var index = document.getElementById('group-tree').view.selection.currentIndex; if (this.mTreeArray[index] && gQuicktext.getGroup(this.mTreeArray[index][0], true).type > 0) { document.getElementById("group-button-remove").setAttribute("disabled", true); document.getElementById("group-button-add-text").setAttribute("disabled", true); } else { document.getElementById("group-button-remove").removeAttribute("disabled"); document.getElementById("group-button-add-text").removeAttribute("disabled"); } } else { document.getElementById('group-button-add-text').setAttribute("disabled", true); document.getElementById('group-button-remove').setAttribute("disabled", true); } let scriptIndex = document.getElementById('script-list').value; let script = gQuicktext.getScript(scriptIndex, true); if (gQuicktext.getScriptLength(true) && script.type == 0) document.getElementById('script-button-remove').removeAttribute("disabled"); else document.getElementById('script-button-remove').setAttribute("disabled", true); } , openHomepage: function() { gQuicktext.openHomepage(); } , resetCounter: function() { notifyTools.notifyBackground({command:"setPref", pref: "counter", value: 0}); } , shortcutModifierChange: function() { var state = (this.mOS.substr(0, 3).toLowerCase() == "mac" || (this.mOS.substr(0, 3).toLowerCase() == "win" && document.getElementById('select-shortcutModifier').value == "alt")); document.getElementById('checkbox-shortcutTypeAdv').disabled = state; } , /* * OBSERVERS */ observe: function(aSubject, aTopic, aData) { if (aTopic == "updatesettings") { // this.updateGUI(); } } } quicktext-4.1/chrome/content/settings.xhtml000066400000000000000000000615301410655052500212540ustar00rootroot00000000000000