options/0000755000000000000000000000000014051011234011236 5ustar rootrootoptions/options.html0000644000000000000000000000501514050747612013637 0ustar rootroot __MSG_preferences__
__MSG_Window.defaults__

  

__MSG_Window.defPosition__

  

__MSG_Message.tag__


  

__MSG_Behavior__

__MSG_storage.path__

options/options.js0000644000000000000000000001165614051011234013300 0ustar rootrootimport '/extlib/l10n.js'; //The object only works if retrieved through chrome.extension, but not //through browser.extension or messenger.extension var bgPage = chrome.extension.getBackgroundPage(); var prefs = bgPage.getPreferences(); var btnSelectStoragePath = document.getElementById("btnSelectStoragePath"); function isInputType(node, type){ return node.nodeName.toLowerCase() == "input" && node.type.toLowerCase() == type.toLowerCase(); } async function selectStoragePath() { // console.debug("selectStoragePath called"); let startDir = prefs.storage_path; let profileDir = await bgPage.getProfileDirectory(); if (startDir.startsWith("[ProfD]")) { try { // console.debug(`profileDir: ${profileDir}; startDir: ${startDir}`); startDir = await bgPage.appendRelativePath(profileDir, startDir.substring(7)); // console.debug(`startDir for selectStoragePath: ${startDir}`); } catch (e) { // console.debug(`Directory does not exist: ${startDir}.`, e); startDir = profileDir; } } try { bgPage.selectDirectory(startDir, bgPage.browser.i18n.getMessage("Select.storage.dir")).then((storagePath) => { // console.debug(`selected storage path: ${storagePath}`); if (storagePath == null) return; //Check whether the new path is inside the profile directory //and if yes, make the path relative to the profile. if (storagePath.indexOf(profileDir) == 0) { if (storagePath.length == profileDir.length) { storagePath = "[ProfD]"; } else { storagePath = "[ProfD]"+storagePath.substr(profileDir.length+1); } } let prefPath = document.getElementById("storage.path"); prefPath.value = storagePath; prefs.storage_path = storagePath; }); } catch (e) { console.error(e); } } async function savePrefs() { const storagePathChanged = document.getElementById('storage.path').value != prefs.storage_path; for (const node of document.querySelectorAll('[data-preference]')) { const pref = node.dataset.preference; //console.debug(`Saving preference: ${pref}`); if (pref.startsWith("tag.")) { switch (pref) { case "tag.color": bgPage.setTbPref("mailnews.tags.xnote.color", node.value); break; case "tag.name": bgPage.setTbPref("mailnews.tags.xnote.tag", node.value); break; default: console.error(`Unknown tag preference ${pref}`); } } else { switch(node.nodeName) { case "SELECT": for(let option of node.querySelectorAll("option")){ if(option.selected){ prefs[pref] = node.value; break; } } break; case "INPUT": if(isInputType(node,"checkbox")){ // debugger; prefs[pref] = node.checked; } else if(isInputType(node, "radio") && node.checked){ prefs[pref] = node.value; } else if (isInputType(node, "number")) { prefs[pref] = parseInt(node.value); } else { prefs[pref] = node.value; } break; default: console.error(`Unknown node type ${node.nodeName}`); console.error(node); } } } bgPage.setPreferences(prefs); } async function initOptions() { //console.debug(prefs); for (const node of document.querySelectorAll('[data-preference]')) { const pref = node.dataset.preference; //console.debug(`Loading preference: ${pref}`); const value = prefs[pref]; if (pref.startsWith("tag.")) { switch (pref) { case "tag.color": node.value = await bgPage.getTbPref("mailnews.tags.xnote.color"); break; case "tag.name": node.value = await bgPage.getTbPref("mailnews.tags.xnote.tag"); break; default: console.error(`Unknown tag preference ${pref}`); } } else { switch(node.nodeName) { case "SELECT": for(let option of node.querySelectorAll("option")){ if(option.value == value){ option.selected = true; break; } } break; case "INPUT": if(isInputType(node, "checkbox")) { node.checked = value; } else if(isInputType(node, "radio")) { node.checked = (value === node.value); } else { node.value = value; } break; default: console.error(`Unknown node type ${node.nodeName}`); console.error(node); } } } btnSave.addEventListener('click', savePrefs); btnSelectStoragePath.addEventListener('click', selectStoragePath); } document.addEventListener('DOMContentLoaded', () => { initOptions(); }, { once: true }); mDisplay.js0000644000000000000000000000705314050747612011707 0ustar rootroot var xnote = ""; // k b\nbbbb\nbbbbbbbbbbbböklllll11111111222222222222222333333333llllcbvvv42vvvvvv"; var xnoteOrig = ""; var dateOrig = ""; var btnState = 0;//debugger; //console.log("loading mdisplay script"); function notify(message) { // console.log("received in msgDisplay from background"); // console.log("Msg:", message.XNoteText); //debugger; try { let old = document.getElementById("xnote_msgDisplay"); old.remove(); } catch (e) { }; function truncate(fullStr, strLen, separator) { if (fullStr.length <= strLen) return fullStr; // acknowledgement to https://gist.github.com/ugurozpinar/10263513 separator = separator || '...'; var sepLen = separator.length, charsToShow = strLen - sepLen, frontChars = Math.ceil(charsToShow / 2), backChars = Math.floor(charsToShow / 2); return fullStr.substr(0, frontChars) + separator + fullStr.substr(fullStr.length - backChars); }; xnoteOrig = xnote = message.text; dateOrig = message.date; if (xnote.length > 0) { let no_linebreak = xnote.replace(/(\r\n|\n|\r)/gm, " "); let no_double_space = no_linebreak.replace(/\s+/g, " "); let trunc = truncate(no_double_space, 350, "..."); let imgMax = document.createElement("img"); imgMax.src = messenger.runtime.getURL("icons/iconfinder_maximize-2_2561250MIT16.png"); // https://ourcodeworld.com/articles/read/376/how-to-strip-html-from-a-string-extract-only-text-content-in-javascript let strippedHtml = ""; strippedHtml = trunc.replace(/<[^>]+>/g, ''); //html entities are not converted, <> are stripped //document.documentElement.firstChild.appendChild(img); let text3 = "
" + "XNote " + message.date + ": " + strippedHtml + "
"; document.documentElement.firstChild.insertAdjacentHTML("beforebegin", text3); let btn = document.getElementById("chgSize"); btn.addEventListener("click", showAll, false); }; }; function handleResponse(message) { //console.log(`note Message from the background script: `, message); } function showAll() { //console.log("all", xnoteOrig); let img = document.getElementById("aa"); img.src = messenger.runtime.getURL("icons/iconfinder_minimize-2_2561246MIT16.png"); // btn.textContent = "Compact"; if (btnState == 0) { let note = document.getElementById("xnoteShort"); note.textContent = "";//remove(); let div = document.createElement("div"); div.setAttribute("id", "fullNoteDiv"); note.append(div); let span = document.getElementById("xnoteFull"); // let fullnote = "
" + xnote + "
"; //span.insertAdjacentHTML("afterend",fullnote); let brNote = xnoteOrig.replace(/(?:\r\n|\r|\n)/g, '
');//xnoteOrig.replace("\r", "
"); div.insertAdjacentHTML("afterend", brNote); btnState = 1; } else { btnState = 0; this.value = "Show all"; let all = document.getElementById("xnote_msgDisplay"); all.remove(); notify({ text: xnoteOrig, date: dateOrig }); } }; async function startup() { await messenger.runtime.onMessage.addListener(notify); let message = await messenger.runtime.sendMessage({ command: "getXNote" }); //console.log(message);// sending.then(handleResponse);// notify(message); } startup(); _locales/0000755000000000000000000000000013740054446011344 5ustar rootroot_locales/nl_NL/0000755000000000000000000000000014050747610012343 5ustar rootroot_locales/nl_NL/messages.json0000644000000000000000000000233014050747610015043 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Voorkeuren"}, "btn.browse": {"message": "Browse..."}, "btn.save" : {"message": "Bespaar"}, "Window.defaults": {"message": "Standaard Venster"}, "Width": {"message": "Breedte"}, "Height": {"message": "Hoogte"}, "Window.defPosition" : {"message": "Standaard positie van het venster"}, "Window.horPos" : {"message": "Horizontale"}, "Window.vertPos" : {"message": "Verticaal"}, "storage.path": {"message": "Opslaan als"}, "Message.tag": {"message": "Bericht Label"}, "Name": {"message": "Naam"}, "Color": {"message": "Kleur"}, "use.tag": {"message": "Gebruik Label?"}, "Behavior": {"message": "Gedrag"}, "show.on.select": {"message": "Notitie tonen bij selectie"}, "show.first.x.chars.in.col": {"message": "Toon eerste x karakters in Xnote Kolom"}, "Select.storage.dir": {"message": "Selecteer standaard directory voor Xnote."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } _locales/pt_BR/0000755000000000000000000000000014050747610012347 5ustar rootroot_locales/pt_BR/messages.json0000644000000000000000000000233614050747610015055 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Config"}, "btn.browse": {"message": "Localizar..."}, "btn.save" : {"message": "Salvar"}, "Window.defaults": {"message": "Configuração Padrão"}, "Width": {"message": "Largura"}, "Height": {"message": "Altura"}, "Window.defPosition" : {"message": "Posição padrão da janela"}, "Window.horPos" : {"message": "Horizontal"}, "Window.vertPos" : {"message": "Vertical"}, "storage.path": {"message": "Salvar em..."}, "Message.tag": {"message": "Etiqueta da mensagem"}, "Name": {"message": "Nome"}, "Color": {"message": "Cor"}, "use.tag": {"message": "Usar etiqueta?"}, "Behavior": {"message": "Processo"}, "show.on.select": {"message": "Exibir XNote ao selecionar e-mail"}, "show.first.x.chars.in.col": {"message": "Exibir X caracteres no campo XNote"}, "Select.storage.dir": {"message": "Selecione a pasta de armazenamento do XNotes."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } _locales/ja_JP/0000755000000000000000000000000014050747610012324 5ustar rootroot_locales/ja_JP/messages.json0000644000000000000000000000251314050747610015027 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "メッセージに付箋を貼り付ける機能を追加します。", "description": "Description of the extension." }, "preferences": { "message": "設定"}, "btn.browse": {"message": "参照"}, "btn.save" : {"message": "保存"}, "Window.defaults": {"message": "デフォルトのサイズ"}, "Width": {"message": "幅"}, "Height": {"message": "高さ"}, "Window.defPosition" : {"message": "Fenster 窓の標準位置"}, "Window.horPos" : {"message": "水平方向"}, "Window.vertPos" : {"message": "垂直方向"}, "storage.path": {"message": "メモの保存先"}, "Message.tag": {"message": "タグとの連動"}, "Name": {"message": "タグ名"}, "Color": {"message": "色"}, "use.tag": {"message": "タグを使う"}, "Behavior": {"message": "動作"}, "show.on.select": {"message": "メッセージを選択したらメモを表示する"}, "show.first.x.chars.in.col": {"message": "スレッドペインのXNoteカラムに先頭数文字を表示する"}, "Select.storage.dir": {"message": "メモの保存先を選択してください"}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } _locales/de/0000755000000000000000000000000014050747610011731 5ustar rootroot_locales/de/messages.json0000644000000000000000000000243414050747610014436 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Notizen zu E-Mails hinzufügen.", "description": "Description of the extension." }, "preferences": { "message": "Einstellungen"}, "btn.browse": {"message": "Durchsuchen..."}, "btn.save" : {"message": "Speichern"}, "Window.defaults": {"message": "Fenster Standardgröße"}, "Width": {"message": "Breite"}, "Height": {"message": "Höhe"}, "Window.defPosition" : {"message": "Fenster Standardposition"}, "Window.horPos" : {"message": "Horizontal"}, "Window.vertPos" : {"message": "Vertikal"}, "storage.path": {"message": "Speicherpfad"}, "Message.tag": {"message": "Nachrichtenschlagwort"}, "use.tag": {"message": "Schlagwort verwenden?"}, "Name": {"message": "Name"}, "Color": {"message": "Farbe"}, "Behavior": {"message": "Verhalten"}, "show.on.select": {"message": "XNote nach E-Mail Auswahl anzeigen"}, "show.first.x.chars.in.col": {"message": "Zeige die ersten X Zeichen in der XNote Spalte"}, "Select.storage.dir": {"message": "Wählen Sie bitte das Verzeichnis zum Speichern der XNotes."}, "show.in.messageDisplay": {"message": "XNote Zusammenfassung in der email anzeigen"} } _locales/it_IT/0000755000000000000000000000000014050747610012351 5ustar rootroot_locales/it_IT/messages.json0000644000000000000000000000241614050747610015056 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Impostazioni"}, "btn.browse": {"message": "Sfoglia..."}, "btn.save" : {"message": "Salva"}, "Window.defaults": {"message": "Finestra default"}, "Width": {"message": "Larghezza"}, "Height": {"message": "Altezza"}, "Window.defPosition" : {"message": "Fenster Standardposition"}, "Window.horPos" : {"message": "Orizzontale"}, "Window.vertPos" : {"message": "Verticale"}, "storage.path": {"message": "Percorso salvataggio"}, "Message.tag": {"message": "Etichetta messaggio"}, "Name": {"message": "Nome"}, "Color": {"message": "Colore"}, "use.tag": {"message": "Usare etichetta?"}, "Behavior": {"message": "Comportamento"}, "show.on.select": {"message": "Mostra nota quando si seleziona il messaggio"}, "show.first.x.chars.in.col": {"message": "Mostra i primi x caratteri nella colonna XNote"}, "Select.storage.dir": {"message": "Selezionare la cartella dove salvare i file XNote."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } _locales/fr_FR/0000755000000000000000000000000014050747610012337 5ustar rootroot_locales/fr_FR/messages.json0000644000000000000000000000255714050747610015052 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Préférences"}, "btn.browse": {"message": "Parcourir…"}, "btn.save" : {"message": "Sauvegarder"}, "Window.defaults": {"message": "Taille par défaut d'une nouvelle note"}, "Width": {"message": "Largeur"}, "Height": {"message": "Hauteur"}, "Window.defPosition" : {"message": "Position par défaut d'une nouvelle note"}, "Window.horPos" : {"message": "Horizontal"}, "Window.vertPos" : {"message": "Vertikal"}, "storage.path": {"message": "Chemin de stockage des notes"}, "Message.tag": {"message": "Étiquette de message"}, "Name": {"message": "Nom"}, "Color": {"message": "Couleur"}, "use.tag": {"message": "Utiliser l'étiquette de message ?"}, "Behavior": {"message": "Comportement"}, "show.on.select": {"message": "Afficher la note du message sélectionné"}, "show.first.x.chars.in.col": {"message": "Montrer les n premiers caractères de la note dans la colonne XNote"}, "Select.storage.dir": {"message": "Veuillez sélectionner le dossier d'enregistrement des notes."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} }_locales/en_US/0000755000000000000000000000000014050747610012352 5ustar rootroot_locales/en_US/messages.json0000644000000000000000000000230714050747610015056 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Preferences"}, "btn.browse": {"message": "Browse..."}, "btn.save" : {"message": "Save"}, "Window.defaults": {"message": "Window default size"}, "Width": {"message": "Width"}, "Height": {"message": "Height"}, "Window.defPosition" : {"message": "Window default position"}, "Window.horPos" : {"message": "Horizontal"}, "Window.vertPos" : {"message": "Vertical"}, "storage.path": {"message": "Storage path"}, "Message.tag": {"message": "Message tag"}, "Name": {"message": "Name"}, "Color": {"message": "Color"}, "use.tag": {"message": "Use tag?"}, "Behavior": {"message": "Behavior"}, "show.on.select": {"message": "Show note on message selection"}, "show.first.x.chars.in.col": {"message": "Show first X characters in XNote column"}, "Select.storage.dir": {"message": "Please select the XNote storage directory."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } _locales/gl/0000755000000000000000000000000014050747610011743 5ustar rootroot_locales/gl/messages.json0000644000000000000000000000241214050747610014444 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Preferencias"}, "btn.browse": {"message": "Examinar..."}, "btn.save" : {"message": "Save"}, "Window.defaults": {"message": "Xanela predeterminada"}, "Width": {"message": "Largura"}, "Height": {"message": "Altura"}, "Window.defPosition" : {"message": "Posición predeterminada da xanela"}, "Window.horPos" : {"message": "Horizontal"}, "Window.vertPos" : {"message": "Vertical"}, "storage.path": {"message": "Ruta de almacenamento"}, "Message.tag": {"message": "Etiqueta de mensaxe"}, "Name": {"message": "Nome"}, "Color": {"message": "Cor"}, "use.tag": {"message": "Usar etiqueta?"}, "Behavior": {"message": "Comportamento"}, "show.on.select": {"message": "Amosar nota ao selecionar a mensaxe"}, "show.first.x.chars.in.col": {"message": "Amosar primeiros X caracteres na columna XNote"}, "Select.storage.dir": {"message": "Selecione o cartafol de almacenamento para XNote."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } _locales/pl_PL/0000755000000000000000000000000014050747610012347 5ustar rootroot_locales/pl_PL/messages.json0000644000000000000000000000241014050747610015046 0ustar rootroot{ "extensionName": { "message": "XNote++", "description": "Name of the extension." }, "extensionDescription": { "message": "Add Post-It to your mails.", "description": "Description of the extension." }, "preferences": { "message": "Preferencje"}, "btn.browse": {"message": "Otwórz..."}, "btn.save" : {"message": "Zapisz"}, "Window.defaults": {"message": "Domyślny rozmiar okna"}, "Width": {"message": "Szerokość"}, "Height": {"message": "Wysokość"}, "Window.defPosition" : {"message": "Standardowa pozycja okna"}, "Window.horPos" : {"message": "Horyzontalny"}, "Window.vertPos" : {"message": "Pionowo"}, "storage.path": {"message": "Ścieżka do bazy danych"}, "Message.tag": {"message": "Etykieta wiadomości"}, "Name": {"message": "Nazwa"}, "Color": {"message": "Kolor"}, "use.tag": {"message": "Użyj etykiety?"}, "Behavior": {"message": "Ustawienia notatek"}, "show.on.select": {"message": "Pokaż po wybraniu wiadomości"}, "show.first.x.chars.in.col": {"message": "Pokaż pierwszych X znaków w kolumnie XNote:"}, "Select.storage.dir": {"message": "Proszę wybrać ścieżkę zapisu dla XNote."}, "show.in.messageDisplay": {"message": "Show XNote summary in messsage display"} } manifest.json0000644000000000000000000000426214114406444012263 0ustar rootroot{ "manifest_version": 2, "name": "__MSG_extensionName__", "description": "__MSG_extensionDescription__", "version": "3.2.4", "author": "Lorenz Froihofer: support for TB >= 3.0, since Dec. 2009, Klaus Bücher since TB 78", "homepage_url": "https://github.com/xnotepp/xnote/wiki", "applications": { "gecko": { "id": "xnote@froihofer.net", "strict_min_version": "91.0", "strict_max_version": "91.*" } }, "default_locale" : "en_US", "icons": { "32": "chrome/skin/icon32.png" }, "browser_action": { "default_title": "XNote++", "default_icon": "chrome/skin/icon32.png" }, "options_ui": { "page": "options/options.html" }, "permissions": ["activeTab", "tabs", "storage", "messagesRead", "messagesModify", "clipboardWrite"], "background": { "scripts": ["xn-background.js"] }, "experiment_apis": { "WindowListener": { "schema": "experiment-apis/WindowListener/schema.json", "parent": { "scopes": ["addon_parent"], "paths": [["WindowListener"]], "script": "experiment-apis/WindowListener/implementation.js" } }, "NotifyTools": { "schema": "experiment-apis/NotifyTools/schema.json", "parent": { "scopes": ["addon_parent"], "paths": [["NotifyTools"]], "script": "experiment-apis/NotifyTools/implementation.js" } }, "xnoteUtilities": { "schema": "experiment-apis/Utilities/utilities.json", "parent": { "scopes": ["addon_parent"], "paths": [["xnoteUtilities"]], "script": "experiment-apis/Utilities/utilities.js" } }, "xnoteapi": { "schema": "experiment-apis/xnote/xnote-experiments.json", "parent": { "scopes": ["addon_parent"], "paths": [["xnoteapi"]], "script": "experiment-apis/xnote/xnote-experiments.js" } }, "xnotefiles": { "schema": "experiment-apis/files/files-api.json", "parent": { "scopes": ["addon_parent"], "paths": [["xnotefiles"]], "script": "experiment-apis/files/files-api.js" } } } }experiment-apis/0000755000000000000000000000000014050747610012672 5ustar rootrootexperiment-apis/files/0000755000000000000000000000000014051011234013757 5ustar rootrootexperiment-apis/files/files-api.js0000644000000000000000000000400314051011234016163 0ustar rootrootvar { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"), { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"), win = Services.wm.getMostRecentWindow("mail:3pane"); var xnotefiles = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { return { xnotefiles: { selectDirectory : async function(window, startDirectory, title) { let domWindow = window; if (domWindow == null) { domWindow = Services.wm.getMostRecentWindow(null); } let fp = Components.classes["@mozilla.org/filepicker;1"] .createInstance(Components.interfaces.nsIFilePicker); fp.init(domWindow, title, fp.modeGetFolder); let FileUtils = ChromeUtils.import("resource://gre/modules/FileUtils.jsm").FileUtils; let startDir = new FileUtils.File(startDirectory); fp.displayDirectory = startDir; return new Promise(function(resolve, reject) { fp.open(rv => { if(rv === fp.returnOK){ resolve(fp.file.path); } }); }) }, getProfileDirectory: async function() { var directoryService = Components.classes['@mozilla.org/file/directory_service;1'] .getService(Components.interfaces.nsIProperties); let profileDir = directoryService.get('ProfD', Components.interfaces.nsIFile); // console.debug(`getProfileDir returns: ${profileDir.path}`); return profileDir.path; }, appendRelativePath: async function(path, extension) { let FileUtils = ChromeUtils.import("resource://gre/modules/FileUtils.jsm").FileUtils; var result = new FileUtils.File(path); result.appendRelativePath(extension); // console.debug(result); // console.debug(`appendRelativePath result: ${result.path}`); return result.path; } } } }; } experiment-apis/files/files-api.json0000644000000000000000000000261413761405776016557 0ustar rootroot[ { "namespace": "xnotefiles", "functions": [ { "name": "selectDirectory", "type": "function", "async": true, "parameters": [ { "name": "window", "type": "any", "description": "File picker depends on this window. Pass 'null' to use the last top level window." }, { "name": "startDirectory", "type": "string", "description": "Directory to show initially to the user." }, { "name": "title", "type": "string", "description": "Title of the dialog." } ] }, { "name": "getProfileDirectory", "type": "function", "async": true, "parameters": [] }, { "name": "appendRelativePath", "type": "function", "async": true, "description": "Allows to connect two path segments by taking care of the platform-specific path separator.", "parameters": [ { "name": "path", "type": "string", "description": "Base path" }, { "name": "extension", "type": "string", "description": "Additional path to add to the base path." } ] } ] } ] experiment-apis/WindowListener/0000755000000000000000000000000014101077270015642 5ustar rootrootexperiment-apis/WindowListener/schema.json0000644000000000000000000000573714046775662020033 0ustar rootroot[ { "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." } ] } ] } ] experiment-apis/WindowListener/implementation.js0000644000000000000000000012570414071265224021242 0ustar rootroot/* * This file is provided by the addon-developer-support repository at * https://github.com/thundernest/addon-developer-support * * Version: 1.56 * * 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]), revision: parts.length > 2 ? parseInt(parts[2]) : 0, } } 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) || (this.getThunderbirdVersion().major == 78 && this.getThunderbirdVersion().minor == 10 && this.getThunderbirdVersion().revision < 2); 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; } } }; experiment-apis/WindowListener/CHANGELOG.md0000644000000000000000000000617214071265224017465 0ustar rootrootVersion: 1.56 ------------- - be precise on which revision the wrench symbol should be displayed, instead of the options button Version: 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 experiment-apis/WindowListener/README.md0000644000000000000000000000033213721221232017113 0ustar rootrootUsage 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). experiment-apis/Utilities/0000755000000000000000000000000014051011234014630 5ustar rootrootexperiment-apis/Utilities/utilities.js0000644000000000000000000000303614051011234017203 0ustar rootroot/* eslint-disable object-shorthand */ var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"), { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"), win = Services.wm.getMostRecentWindow("mail:3pane"); var {xnote} = ChromeUtils.import("resource://xnote/modules/xnote.jsm"); //console.log("xnote - implementation utilities"); var xnoteUtilities = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { const PrefTypes = { [Services.prefs.PREF_STRING] : "string", [Services.prefs.PREF_INT] : "number", [Services.prefs.PREF_BOOL] : "boolean", [Services.prefs.PREF_INVALID] : "invalid" }; return { xnoteUtilities: { logDebug (text) { // console.debug(text); }, openLinkExternally(url) { let uri = url; if (!(uri instanceof Ci.nsIURI)) { uri = Services.io.newURI(url); } Cc["@mozilla.org/uriloader/external-protocol-service;1"] .getService(Ci.nsIExternalProtocolService) .loadURI(uri); }, showXhtmlPage(uri) { let mail3PaneWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator) .getMostRecentWindow("mail:3pane"); mail3PaneWindow.openDialog(uri); } // get may only return something, if a value is set } } }; } experiment-apis/Utilities/utilities.json0000644000000000000000000000154614004402740017550 0ustar rootroot[ { "namespace": "xnoteUtilities", "functions": [ { "name": "logDebug", "type": "function", "async": true, "parameters": [ { "name": "text", "type": "string", "description": "what text to log" } ] }, { "name": "showXhtmlPage", "type": "function", "parameters": [ { "name": "url", "type": "string", "description": "url to open in browser" } ] }, { "name": "openLinkExternally", "type": "function", "parameters": [ { "name": "url", "type": "string", "description": "url to open in browser" } ] } ] } ] experiment-apis/NotifyTools/0000755000000000000000000000000014050747610015163 5ustar rootrootexperiment-apis/NotifyTools/schema.json0000644000000000000000000000160614050747610017321 0ustar rootroot[ { "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 algorythm 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 algorythm apply." } ] } ] } ] experiment-apis/NotifyTools/implementation.js0000644000000000000000000000744014050747610020553 0ustar rootroot/* * 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) { // Remove observer for notifyTools.js Services.obs.removeObserver( this.onNotifyBackgroundObserver, "NotifyBackgroundObserver" ); // Flush all caches Services.obs.notifyObservers(null, "startupcache-invalidate"); } }; experiment-apis/NotifyTools/README.md0000644000000000000000000001067614050747610016454 0ustar rootroot# 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. experiment-apis/xnote/0000755000000000000000000000000014051011234014012 5ustar rootrootexperiment-apis/xnote/xnote-experiments.json0000644000000000000000000000400714050747612020423 0ustar rootroot[ { "namespace": "xnoteapi", "functions": [ { "name": "init", "type": "function", "async": true, "parameters": [] }, { "name": "initNote", "type": "function", "async": true, "parameters": [] }, { "name": "closeNoteWindow", "type": "function", "async": true, "parameters": [] }, { "name": "getXNote", "type": "function", "async": true, "parameters": [ { "name": "id", "type": "any", "description": "MX message id" } ] }, { "name": "getPref", "type": "function", "async": true, "parameters": [ { "name": "name", "type": "string", "description": "Old preference string excluding extensions.xnote. prefix." } ] }, { "name": "setPreferences", "type": "function", "async": true, "parameters": [ { "name": "prefs", "type": "any", "description": "Set the preferences object from the local browser storage." } ] }, { "name": "getTbPref", "type": "function", "async": true, "parameters": [ { "name": "name", "type": "string", "description": "Old TB preference string." } ] }, { "name": "setTbPref", "type": "function", "async": true, "parameters": [ { "name": "name", "type": "string", "description": "Old TB preference string." }, { "name": "value", "type": "any", "description": "TB preference value." } ] } ] } ] experiment-apis/xnote/xnote-experiments.js0000644000000000000000000001550714102030160020052 0ustar rootrootvar { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"), { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"), { TagUtils } = ChromeUtils.import("resource:///modules/TagUtils.jsm"), win = Services.wm.getMostRecentWindow("mail:3pane"); const { ExtensionParent } = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); const xnoteExtension = ExtensionParent.GlobalManager.getExtension("xnote@froihofer.net"); var {xnote} = ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/xnote.jsm")); if (!xnote.ns) xnote.ns = {}; ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/commons.jsm"), xnote.ns); ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/storage.jsm"), xnote.ns); ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/xnote-upgrades.jsm"), xnote.ns); const XNOTE_BASE_PREF_NAME = "extensions.xnote."; /** * This maps preference names to their types. This is needed as the prefs * system doesn't actually know what format you've stored your pref in. */ function prefType(name) { switch (name) { case XNOTE_BASE_PREF_NAME+"usetag": { return "bool"; } case XNOTE_BASE_PREF_NAME+"dateformat": { return "string"; } case XNOTE_BASE_PREF_NAME+"width": { return "int"; } case XNOTE_BASE_PREF_NAME+"height": { return "int"; } case XNOTE_BASE_PREF_NAME+"HorPos": { return "int"; } case XNOTE_BASE_PREF_NAME+"VertPos": { return "int"; } case XNOTE_BASE_PREF_NAME+"show_on_select": { return "bool"; } case XNOTE_BASE_PREF_NAME+"show_in_messageDisplay": { return "bool"; } case XNOTE_BASE_PREF_NAME+"show_first_x_chars_in_col": { return "int"; } case XNOTE_BASE_PREF_NAME+"storage_path": { return "string"; } case "mailnews.tags.xnote.tag": { return "string"; } case "mailnews.tags.xnote.color": { return "string"; } } throw new Error(`Unexpected pref type ${name}`); } //console.log("xnote - experiments API"); var xnoteapi = class extends ExtensionCommon.ExtensionAPI { getAPI(context) { return { xnoteapi: { async init() { xnote.ns.Commons.init(); xnote.ns.Storage.updateStoragePath(); //TODO: Move the stored version to the browser storage or do the check //for a previous installation before preferences are migrated let storedVersion = xnote.ns.Commons.xnoteLegacyPrefs.prefHasUserValue("version") ? xnote.ns.Commons.xnoteLegacyPrefs.getCharPref("version") : null // console.log(`storedVersion: ${storedVersion}; comparison: `+ (storedVersion == null)); xnote.ns.Commons.isNewInstallation = storedVersion == null; xnote.ns.Upgrades.checkUpgrades(storedVersion, xnote.ns.Commons.XNOTE_VERSION) xnote.ns.Commons.xnoteLegacyPrefs.setCharPref("version", xnote.ns.Commons.XNOTE_VERSION); xnote.ns.Commons.checkXNoteTag(); }, async closeNoteWindow() { //console.log("now close window"); let winNote = Services.wm.getMostRecentWindow("xnote:note"); //debugger; if (winNote) winNote.close(); }, async initNote() { xnote.ns.Overlay.initialise('clicBouton'); }, async getPref(name) { return this.getTbPref(`${XNOTE_BASE_PREF_NAME}${name}`) }, async setPreferences(prefs) { xnote.ns.Commons.xnotePrefs = prefs; // console.debug({"XnotePrefs" : xnote.ns.Commons.xnotePrefs}); xnote.ns.Storage.updateStoragePath(); xnote.ns.Commons.checkXNoteTag(); }, async getXNote(id) { let note = {}; try { let realMessage = context.extension.messageManager.get(id); // console.log("realmsg", realMessage.messageId ); note = new xnote.ns.Note(realMessage.messageId); // console.log("xnote", note); } catch (ex) { console.error(`Could not get TB mesg` ); } return {text: note.text, date: note.modificationDate}; }, async getTbPref(name) { try { switch (prefType(name)) { case "bool": { return Services.prefs.getBoolPref(name); } case "int": { return Services.prefs.getIntPref(name); } case "char": { return Services.prefs.getStringPref(name); } case "string": { return Services.prefs.getStringPref(name); } default: { console.error(`Unexpected pref type for: ${name}`); } } } catch (ex) { console.error(`Could not get TB pref ${name}` , ex); return undefined; } }, async setTbPref(name, value) { try { if (name == "mailnews.tags.xnote.color") { TagUtils.addTagToAllDocumentSheets("xnote", value); Services.prefs.setStringPref(name, value); } else switch (prefType(name)) { case "bool": { Services.prefs.setBoolPref(name, value); break; } case "int": { Services.prefs.setIntPref(name, value); break; } case "char": { Services.prefs.setStringPref(name, value); break; } case "string": { Services.prefs.setStringPref(name, value); break; } default: console.error(`Unknown preference type: ${prefType(name)}`) } } catch (ex) { console.error(`Could not set TB pref ${name}` , ex); } } } } } onShutdown(isAppShutdown) { // console.debug(`onShutdown: isAppShutdown=${isAppShutdown}`); if (isAppShutdown) return; Components.utils.unload(xnoteExtension.rootURI.resolve("chrome/modules/xnote-upgrades.jsm")); Components.utils.unload(xnoteExtension.rootURI.resolve("chrome/modules/storage.jsm")); Components.utils.unload(xnoteExtension.rootURI.resolve("chrome/modules/commons.jsm")); Components.utils.unload(xnoteExtension.rootURI.resolve("chrome/modules/xnote.jsm")); // invalidate the startup cache, such that after updating the addon the old // version is no longer cached Services.obs.notifyObservers(null, "startupcache-invalidate"); } } experiment-apis/LegacyPrefs/0000755000000000000000000000000013761305360015076 5ustar rootrootexperiment-apis/LegacyPrefs/legacyprefs.js0000644000000000000000000000343013761305360017740 0ustar rootroot/* eslint-disable object-shorthand */ 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) { const PrefTypes = { [Services.prefs.PREF_STRING] : "string", [Services.prefs.PREF_INT] : "number", [Services.prefs.PREF_BOOL] : "boolean", [Services.prefs.PREF_INVALID] : "invalid" }; return { LegacyPrefs: { // get may only return something, if a value is set get: async function(aName, aDefault) { let prefType = Services.prefs.getPrefType(aName); if (prefType == Services.prefs.PREF_INVALID) { return null; } if (typeof aDefault != PrefTypes[prefType]) { throw new Error("PrefType of <" + aName + "> is <" + PrefTypes[prefType] + "> and does not match the type of its default value <" + aDefault + "> which is <" + typeof aDefault + ">!"); } switch (typeof aDefault) { case "string": return Services.prefs.getCharPref(aName, aDefault); case "number": return Services.prefs.getIntPref(aName, aDefault); case "boolean": return Services.prefs.getBoolPref(aName, aDefault); default: throw new Error("Preference <" + aName + "> has an unsupported type <" + typeof aDefault + ">. Allowed are string, number and boolean."); } }, clear: async function(aName) { Services.prefs.clearUserPref(aName); } }, }; } }; experiment-apis/LegacyPrefs/legacyprefs.json0000644000000000000000000000221513761305360020275 0ustar rootroot[ { "namespace": "LegacyPrefs", "functions": [ { "name": "get", "type": "function", "description": "Gets a value from the legacy pref system.", "async": true, "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." }, { "name": "aDefault", "description": "Default value of the preference.", "choices": [ { "type": "integer" }, { "type": "string" }, { "type": "boolean" } ] } ] }, { "name": "clear", "type": "function", "description": "Removes a value from the legacy pref system.", "async": true, "parameters": [ { "name": "aName", "type": "string", "description": "Name of the preference." } ] } ] } ] icons/0000755000000000000000000000000014056761670010703 5ustar rootrooticons/iconfinder_maximize-2_2561250_MIT.svg0000644000000000000000000000053314050310766017212 0ustar rootrooticons/iconfinder_minimize-2_2561246MIT16.png0000644000000000000000000000035214050747612017215 0ustar rootrootPNG  IHDRa pHYs  IDAT8 0 -X^ K'ȥbAMyRX/?QxY.@Pu Dv.O+Y @$FL CoHrRɣu԰2q; tADKa.&popup/0000755000000000000000000000000014050747612010725 5ustar rootrootpopup/installed.js0000644000000000000000000000122014050747612013235 0ustar rootroot addEventListener("click", async (event) => { if (event.target.id.startsWith("donate")) { messenger.windows.openDefaultBrowser("https://www.paypal.com/donate?hosted_button_id=2AKE2G2B9J3ZS"); } }); addEventListener("load", async (event) => { // debugger; let text = document.body.innerHTML, htmltext = text.replace(/{addon}/g, await browser.runtime.getManifest().name ); htmltext2 = htmltext.replace(/{version}/g, await browser.runtime.getManifest().version); let browserInfo = await browser.runtime.getBrowserInfo() htmltext = htmltext2.replace(/{appver}/g, browserInfo.version); document.body.innerHTML = htmltext; });popup/update.js0000644000000000000000000000127414050747612012551 0ustar rootroot addEventListener("click", async (event) => { if (event.target.id.startsWith("donate")) { //console.log("donate link"); messenger.windows.openDefaultBrowser("https://www.paypal.com/donate?hosted_button_id=2AKE2G2B9J3ZS"); } }); addEventListener("load", async (event) => { // debugger; let text = document.body.innerHTML; htmltext = text.replace(/{addon}/g, await browser.runtime.getManifest().name); htmltext2 = htmltext.replace(/{version}/g, await browser.runtime.getManifest().version); let browserInfo = await browser.runtime.getBrowserInfo() htmltext = htmltext2.replace(/{appver}/g, browserInfo.version); document.body.innerHTML = htmltext; }); popup/installed.css0000644000000000000000000000145613746365152013432 0ustar rootrootul.actions { } .actionlinks { text-align: center; } .actionlinks a { background-color: darkred; box-shadow: 5px 5px 5px rgba(50,50,50,0.5); color: white; padding: 5px 8px; cursor: pointer; } .actionlinks a:hover { background-image: linear-gradient(to bottom, #6d0019 0%,#8f0222 34%,#e20425 100%); } .actionlinks a:active { background-image: linear-gradient(to bottom, #633b00 0%,#750202 34%,#ef8204 100%); } body * { background-color: white; } h1, h2, h3, h4 { color: #1a566d; font-family: Impact,Haettenschweiler,Franklin Gothic Bold,Charcoal,Helvetica Inserat,Bitstream Vera Sans Bold,Arial Black,sans serif; text-align: center; } h1 { font-size: 20pt; } h2 { font-size: 16pt; font-weight: normal; } h3 { font-size: 13pt; }popup/popup.css0000644000000000000000000000413513746365152012613 0ustar rootroot.testversion { box-shadow: inset 0 0 10px #000000; padding: 1em; font-style: italic; background-color: rgb(0,0,90); /* dark blue */ } .testversion p { background-color: rgb(0,0,90); /* dark blue */ color: yellow !important; font-weight: bold; } #innerwrapper { padding-left: 10%; padding-right: 10%; } ul.actions { } .actionlinks { text-align: center; } .actionlinks a { background-color: darkred; box-shadow: 5px 5px 5px rgba(50,50,50,0.5); color: white; padding: 5px 8px; cursor: pointer; } .actionlinks div { background-color: darkred; box-shadow: 5px 5px 5px rgba(50,50,50,0.5); color: white; padding: 5px 8px; } .actionlinks a:hover { background-image: linear-gradient(to bottom, #6d0019 0%,#8f0222 34%,#e20425 100%); } .actionlinks a:active { background-image: linear-gradient(to bottom, #633b00 0%,#750202 34%,#ef8204 100%); } .actionlinks div:active { background-image: linear-gradient(to bottom, #633b00 0%,#750202 34%,#ef8204 100%); } body, body * { background-color: white; color: rgb(20,15,05); } body p, body li { background-color: white; color: rgb(20,15,05); font-size: 12pt; } .versionnumber { color: darkblue; } /* https://designshack.net/articles/css/inner-shadows-in-css-images-text-and-beyond/ */ h1, h2, h3, h4 { /* color: #1a566d; */ font-family: Impact,Haettenschweiler,Franklin Gothic Bold,Charcoal,Helvetica Inserat,Bitstream Vera Sans Bold,Arial Black,sans serif; text-align: center; color: rgba(20,30,50,0.6); margin-top: 0.5em; margin-bottom: 0.5em; text-shadow: 3px 3px 0 #EEE, -1px -1px 0 #EEE, 1px -1px 0 #EEE, -1px 1px 0 #EEE, 1px 1px 0 #EEE; /* text-shadow: 0px 2px 3px rgba(255,255,255,0.5); -webkit-background-clip: text; -moz-background-clip: text; background-clip: text; */ text-stroke: 1px lightblue; } h1 { font-size: 20pt; } h2 { font-size: 16pt; font-weight: normal; } h3 { font-size: 13pt; }popup/installed.html0000644000000000000000000000710114050747612013571 0ustar rootroot Welcome to XNote++

Successfully installed {addon}

Thank you for installing {addon}. You are now running {addon} {version} on Thunderbird {appver}.

New/Hidden Features

  • Show note summary in messagedisplay:
  • The messagedisplay can show the first 350 characters of the XNote. The display combines the first 175 chars, followed by ... and the last 175 chars, while linefeeds are removed.
    If note display in a window is disabled, this allows to scroll through the messagetree by up, down or mouse. For relevant messages, the xnote summary will be displayed inside the message display area.
    Using the button, it is possible to toggle between note summary and full note display.
    You can switch this off in addon settings.

  • Print notes:
  • Using the print icon in the xnote window, notes can be printed.

  • Email notes:
  • Using the forward arrow in the xnote window, notes can be forwarded by email.

  • Display of notes:
  • If the messagedisplay is switched off (F8, only threadlist is visible), notes will now display (bugfix).

  • Toggle note display with XNote++ button:
  • Repeatedly clicking the button in the toolbar toggles the display of the note.

  • Preview notes on mouseover:
  • If you display the {addon} column and widen it, so that the three dots are shown for a note, but no text: then, on mouseover/hover, a little window will show the note text. The message does not need to be selected. This can be used to get a fast overview over all notes indicated in the messagelist. The number of characters shown can be chosen in the settings (tools->addon settings-> xnote++).

  • Keyboard operation:
  • New note: Shift+F10 - x - c, Modify note: Shift+F10 - x -m, Delete note: Shift+F10 - x - d (varies by TB language). Some keyboards have a 'context menu' key, which can be used instead of shift+F10

  • Set note position on screen:
  • {addon} now allows to set the standard screen position for notes in the settings. It is even possible to choose a position outside of the main TB window. So the note will no longer cover parts of the TB window.

    Help with a Donation!

    This Add-on is not sponsored in any way by Mozilla.

    In order to keep the addon working, add-on authors have to raise the funds necessary to repair and rewrite so that our users can keep using the functionality we bring to extend Thunderbird.

    At lot of personal time has been put into this. We appreciate if you can support this work by a .

    popup/update.html0000644000000000000000000000703414050747612013101 0ustar rootroot Welcome to the update for XNote++

    {addon} Updated

    Thank you for updating {addon}. You are now running version {version} on Thunderbird {appver}.

    New/Hidden Features

  • Show note summary in messagedisplay:
  • The messagedisplay can show the first 350 characters of the XNote. The display combines the first 175 chars, followed by ... and the last 175 chars, while linefeeds are removed.
    If note display in a window is disabled, this allows to scroll through the messagetree by up, down or mouse. For relevant messages, the xnote summary will be displayed inside the message display area.
    Using the button, it is possible to toggle between note summary and full note display.
    You can switch this off in addon settings.

  • Print notes:
  • Using the print icon in the xnote window, notes can be printed.

  • Email notes:
  • Using the forward arrow in the xnote window, notes can be forwarded by email.

  • Display of notes:
  • If the messagedisplay is switched off (F8, only threadlist is visible), notes will now display (bugfix).

  • Toggle note display with XNote++ button:
  • Repeatedly clicking the button in the toolbar toggles the display of the note.

  • Preview notes on mouseover:
  • If you display the {addon} column and widen it, so that the three dots are shown for a note, but no text: then, on mouseover/hover, a little window will show the note text. The message does not need to be selected. This can be used to get a fast overview over all notes indicated in the messagelist. The number of characters shown can be chosen in the settings (tools->addon settings-> xnote++).

  • Keyboard operation:
  • New note: Shift+F10 - x - c, Modify note: Shift+F10 - x -m, Delete note: Shift+F10 - x - d (varies by TB language). Some keyboards have a 'context menu' key, which can be used instead of shift+F10

  • Set note position on screen:
  • {addon} now allows to set the standard screen position for notes in the settings. It is even possible to choose a position outside of the main TB window. So the note will no longer cover parts of the TB window.

    Help with a Donation!

    This Add-on is not sponsored in any way by Mozilla.

    In order to keep the addon working, add-on authors have to raise the funds necessary to repair and rewrite so that our users can keep using the functionality we bring to extend Thunderbird.

    At lot of personal time has been put into this. We appreciate if you can support this work by a .

    chrome/0000755000000000000000000000000013761405776011051 5ustar rootrootchrome/content/0000755000000000000000000000000014101324060012472 5ustar rootrootchrome/content/scripts/0000755000000000000000000000000014051011234014161 5ustar rootrootchrome/content/scripts/notifyTools.js0000644000000000000000000000721314050747610017070 0ustar rootroot// Set this to the ID of your add-on. var ADDON_ID = "xnote@froihofer.net"; /* * 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 ); } chrome/content/scripts/xn-xnote-overlay.js0000644000000000000000000000735514114373060020000 0ustar rootrootvar { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); Services.scriptloader.loadSubScript("chrome://xnote/content/xnote-classe.js", window, "UTF-8"); Services.scriptloader.loadSubScript("chrome://xnote/content/xnote-overlay.js", window, "UTF-8"); Services.scriptloader.loadSubScript("chrome://xnote/content/xnote-columnnote.js", window, "UTF-8"); Services.scriptloader.loadSubScript("chrome://xnote/content/scripts/notifyTools.js", window, "UTF-8"); //Services.scriptloader.loadSubScript("chrome://global/content/preferencesBindings.js", window, "UTF-8"); //Services.scriptloader.loadSubScript("chrome://xnote/content/preferences.js", window, "UTF-8"); // chrome/content/xnote-overlay.js0000644000000000000000000003547514101607576015702 0ustar rootroot// encoding ='UTF-8' /* # File : xnote-overlay.js # Author : Hugo Smadja, Pierre-Andre Galmes, Lorenz Froihofer # Description : functions associated to the "xnote-overlay.xul" overlay file. */ /* Tag management principles and thoughts - When should labels be applied? - When a new post-it is saved. - When XNote notes are imported from a PC to another PC (cf TODO: import procedure). - When should the XNote label related to a message be removed? - When the message is empty (no text in it). - When the message is removed. - What should happened when XNote is removed? - Remove the XNote tag ? No - Remove the XNote labels associated to messages? No */ if (!ExtensionParent) var { ExtensionParent } = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); if (!xnoteExtension) var xnoteExtension = ExtensionParent.GlobalManager.getExtension("xnote@froihofer.net"); var { xnote } = ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/xnote.jsm")); if (!xnote.ns) xnote.ns = {}; ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/commons.jsm"), xnote.ns); ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/storage.jsm"), xnote.ns); xnote.ns.Overlay = function () { //result var pub = {}; // Variables related to the XNote context menu. var noteForContextMenu; var currentIndex; /** Contains the note for the current message */ var note; /** Contains the XNote window instance. */ var xnoteWindow; /** * Indicates whether the post-it has been opened at the request of the user or * automatically when selecting an email. */ var initSource; /** * CALLER XUL * type : event load element XUL * id : button-xnote * FUNCTION * Executed to load the note before it is displayed on the screen. * Here we can change the style of the window dynamically */ pub.initialise = function (event) { // ~ dump('\n->initialise, messageId='+pub.getMessageID()); //Closes the note (if any) of the old (deselected) message. pub.closeNote(); //Initialize note for the newly selected message note = new xnote.ns.Note(pub.getMessageID()); pub.updateTag(note.text); let msgDisplaytext = note.modificationDate + " " + note.text; notifyTools.notifyBackground({ command: "addToMsgDisplay", text: note.text, date: note.modificationDate }).then((data) => { //console.log(data); }); let xnotePrefs = xnote.ns.Commons.xnotePrefs; if ((xnotePrefs.show_on_select && note.text != '') || initSource == 'clicBouton' || event == 'clicBouton') { xnoteWindow = window.openDialog( 'chrome://xnote/content/xnote-window.xhtml', 'XNote', 'chrome=yes,dependent=yes,resizable=yes,modal=no,left=' + (window.screenX + note.x) + ',top=' + (window.screenY + note.y) + ',width=' + note.width + ',height=' + note.height, note, (initSource == 'clicBouton' || event == 'clicBouton' ? 'clicBouton' : null) ); } initSource = ''; //~ dump('\n<-initialise'); } /** * CALLER XUL * Type: event command element XUL * Id: context-addition * FUNCTION * Creation and modification of notes uses the same function, that is context_modifierNote() */ pub.context_createNote = function () { pub.context_modifyNote(); } /** * CALLER XUL * Type: event command element XUL * Id: context-modif * FUNCTION */ pub.context_modifyNote = function () { initSource = 'clicBouton'; //specifies that the note is created by the user if (gDBView.selection.currentIndex == currentIndex) { //if you right click on the mail stream (one selected) pub.initialise(); } else { gDBView.selection.select(currentIndex); gDBView.selectionChanged(); // The following prevents the previous message selection from // being restored during closing of the context menu. // Variable not present in SeaMonkey --> check to prevent errors. if (xnote.ns.Commons.isInThunderbird) { gRightMouseButtonSavedSelection.realSelection.select(currentIndex); } } } /** * CALLER XUL * Type: event command element XUL <menuitem> * Id: context-suppr * FUNCTION * Delete the note associated with the selected e-mail. */ pub.context_deleteNote = function () { noteForContextMenu.deleteNote(); pub.updateTag(""); setTimeout(xnote.ns.Overlay.initialise); } pub.context_resetNoteWindow = function () { if (gDBView.selection.currentIndex == currentIndex) { xnoteWindow.resizeTo(noteForContextMenu.DEFAULT_XNOTE_WIDTH, noteForContextMenu.DEFAULT_XNOTE_HEIGHT); xnoteWindow.moveTo(noteForContextMenu.DEFAULT_X, noteForContextMenu.DEFAULT_Y) note.modified = true; } else { noteForContextMenu.x = noteForContextMenu.DEFAULT_X; noteForContextMenu.y = noteForContextMenu.DEFAULT_Y; noteForContextMenu.width = noteForContextMenu.DEFAULT_XNOTE_WIDTH; noteForContextMenu.height = noteForContextMenu.DEFAULT_XNOTE_HEIGHT; noteForContextMenu.modified = true; noteForContextMenu.saveNote(); } } /** * Closes the XNote window. */ pub.closeNote = function () { // let xnoteWindow = xnote.ns.Overlay.xnoteWindow; if (xnoteWindow != null && xnoteWindow.document) { xnoteWindow.close(); } } /** * FUNCTION * Applies the XNote tag to the selected message. * (Choice of tag in the preferences.) */ pub.updateTag = function (noteText) { // dump('\n->updateTag'); if (xnote.ns.Commons.useTag) { // If the note isn't empty, if (noteText != '') { // Add the XNote Tag. ToggleMessageTag("xnote", true); } // If the note is empty, else { // Remove the XNote Tag. ToggleMessageTag("xnote", false); } //~ dump('\n<-updateTag'); } } function updateContextMenu() { //debugger; noteForContextMenu = new xnote.ns.Note(pub.getMessageID()); let noteExists = noteForContextMenu.exists(); /* Commented until button will be re-enabled in manifest.json ("message_display_action" removed in earlier commit). if (noteExists) xnote.WL.messenger.messageDisplayAction.disable(); else xnote.WL.messenger.messageDisplayAction.enable(); */ document.getElementById('xnote-context-create').setAttribute('hidden', noteExists); document.getElementById('xnote-context-modify').setAttribute('hidden', !noteExists); document.getElementById('xnote-context-delete').setAttribute('hidden', !noteExists); document.getElementById('xnote-context-separator-after-delete').setAttribute('hidden', !noteExists); document.getElementById('xnote-context-reset-note-window').setAttribute('hidden', !noteExists); var messageArray = gFolderDisplay.selectedMessages; if (messageArray && messageArray.length == 1) { document.getElementById('xnote-mailContext-xNote').setAttribute('disabled', false); } else { document.getElementById('xnote-mailContext-xNote').setAttribute('disabled', true); } } /** * FUNCTION * For right click in message pane: * - Instantiates an object notes for the message on which was clicked * - Functions that are not currently possible are greyed out in the context * menu, e.g., modify or delete a note for a message not containing a note. */ pub.messageListClicked = function (e) { //~ dump('\n->messageListClicked, messageID='+pub.getMessageID()); if (e.button == 2) { updateContextMenu(); } let t = e.originalTarget; if (t.localName == 'treechildren') { let tree = GetThreadTree(); let treeCellInfo = tree.getCellAt(e.clientX, e.clientY); currentIndex = treeCellInfo.row; //console.log(treeCellInfo.col.cycler); //~ dump('\nclicked row = '+currentIndex); } //~ dump('\n<-messageListClicked'); } pub.getCurrentRow = function (e) { //~ dump('\n->messageListClicked, messageID='+pub.getMessageID()); let t = e.originalTarget; if (t.localName == 'treechildren') { let tree = GetThreadTree(); let treeCellInfo = tree.getCellAt(e.clientX, e.clientY); currentIndex = treeCellInfo.row; //console.log(treeCellInfo.col.cycler); //~ dump('\nclicked row = '+currentIndex); } //~ dump('\n<-messageListClicked'); } pub.messagePaneClicked = function (e) { //~ dump('\n->messagePaneClicked, messageID='+pub.getMessageID()); if (e.button == 2) { updateContextMenu(); } currentIndex = gDBView.selection.currentIndex; } /** * Get message id from selected message */ pub.getMessageID = function () { let message = gFolderDisplay.selectedMessage; if (message != null) return message.messageId; return null; } /** * Enable XNote button for a single selected message. * Disable XNote button if no or several mails are selected. */ pub.updateXNoteButton = function () { let messageArray = gFolderDisplay.selectedMessages; let xnoteButton = document.getElementById('xnote_froihofer_net-browserAction-toolbarbutton'); if (messageArray && messageArray.length == 1) { if (xnoteButton) { xnoteButton.setAttribute('disabled', false); } document.getElementById('xnote-mailContext-xNote').setAttribute('disabled', false); } else { if (xnoteButton) { xnoteButton.setAttribute('disabled', true); } document.getElementById('xnote-mailContext-xNote').setAttribute('disabled', true); pub.closeNote(); } } /** * This function checks whether updates are necessary. * For example, it adds the XNote icon at the end of the toolbar if * XNote has been newly installed. */ pub.checkInitialization = function () { if (xnote.ns.Commons.isNewInstallation) { // console.log("First time installation - add the XNote toolbar button."); let toolbox = document.getElementById("mail-toolbox"); let xnoteButtonPresent = false; let toolbars = document.evaluate(".//.[local-name()='toolbar' and @customizable='true']", toolbox, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); let toolbar = toolbars.iterateNext(); while (toolbar && !xnoteButtonPresent) { //~dump("\n\nChecking toolbar '"+toolbar.id+"', currentSet="+toolbar.currentSet); if (toolbar.currentSet.indexOf("xnote_froihofer_net-browserAction-toolbarbutton") > -1) { xnoteButtonPresent = true; //~dump("\nFound XNote button."); } toolbar = toolbars.iterateNext(); } if (!xnoteButtonPresent) try { toolbar = document.getElementById("mail-bar3"); if (!xnote.ns.Commons.isInThunderbird) { toolbar = document.getElementById("msgToolbar"); } let buttons = toolbar.currentSet.split(","); let newSet = ""; for (let i = 0; i < buttons.length; i++) { if (!xnoteButtonPresent && buttons[i] == "spring") { newSet += "xnote_froihofer_net-browserAction-toolbarbutton,"; xnoteButtonPresent = true; } newSet += buttons[i] + ","; } if (xnoteButtonPresent) { newSet = newSet.substring(0, newSet.length - 1); } else { newSet = toolbar.currentSet + ",xnote_froihofer_net-browserAction-toolbarbutton"; } toolbar.currentSet = newSet; toolbar.setAttribute("currentset", newSet); Services.xulStore.persist(toolbar, "currentset"); } catch (e) { console.error("Could not add XNote button.", e) } } } /** * At each boot of the extension, associate events such as selection of mails, * files, or right click on the list of messages. On selection show the associated * note. */ pub.onLoad = function (e) { //console.debug("xnote: overlay.onLoad: "+JSON.stringify(xnote, null, 2)+"\n"); if (String(EnsureSubjectValue).search('extensionDejaChargee') == -1) { let oldEnsureSubjectValue = EnsureSubjectValue; EnsureSubjectValue = function () { //to prevent duplicate registrations: var extensionDejaChargee; oldEnsureSubjectValue(); setTimeout(xnote.ns.Overlay.initialise); }; } try { let tree = document.getElementById('folderTree'); tree.addEventListener('select', pub.closeNote, false); tree.addEventListener('select', pub.updateXNoteButton, false); tree = document.getElementById('threadTree'); tree.addEventListener('contextmenu', pub.messageListClicked, false); tree.addEventListener('select', pub.updateXNoteButton, false); tree.addEventListener('mouseover', pub.getCurrentRow, false); tree.addEventListener('select', xnote.ns.Overlay.initialise, false); let messagePane = document.getElementById("messagepane"); messagePane.addEventListener("contextmenu", pub.messagePaneClicked, false); tree = GetThreadTree(); if (tree) { tree.addEventListener('click', pub.getCurrentRow, false); } } catch (e) { logException(e, false); } //window.addEventListener('DOMAttrModified', xnote.ns.Commons.printEventDomAttrModified, false); pub.checkInitialization(); } pub.onUnload = function (e) { //console.debug("xnote: overlay.onLoad: "+JSON.stringify(xnote, null, 2)+"\n"); try { let tree = document.getElementById('folderTree'); tree.removeEventListener('select', pub.closeNote); tree.removeEventListener('select', pub.updateXNoteButton); tree = document.getElementById('threadTree'); tree.removeEventListener('contextmenu', pub.messageListClicked); tree.removeEventListener('select', pub.updateXNoteButton); tree.removeEventListener('mouseover', pub.getCurrentRow); tree.removeEventListener('select', xnote.ns.Overlay.initialise); let messagePane = document.getElementById("messagepane"); messagePane.removeEventListener("contextmenu", pub.messagePaneClicked); tree = GetThreadTree(); if (tree) { tree.removeEventListener('click', pub.getCurrentRow); } } catch (e) { logException(e, false); } } return pub; }(); /*function toOpenWindowByType(inType, uri) { var winopts = "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"; window.open(uri, "_blank", winopts); } start_venkman();*/ //addEventListener('load', xnote.ns.Overlay.onLoad, true); // dump("xnote: overlay - end: "+JSON.stringify(xnote, null, 2)); chrome/content/xnote-classe.js0000644000000000000000000001343214004402740015444 0ustar rootroot// encoding='UTF-8' /** # File : xnote-classe.xul # Author : Hugo Smadja, Lorenz Froihofer # Description : classe Note permettant d'instancier des notes. */ if (!ExtensionParent) var { ExtensionParent } = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm"); if (!xnoteExtension) var xnoteExtension = ExtensionParent.GlobalManager.getExtension("xnote@froihofer.net"); var {xnote} = ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/xnote.jsm")); if (!xnote.ns) xnote.ns={}; ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/commons.jsm"), xnote.ns); ChromeUtils.import(xnoteExtension.rootURI.resolve("chrome/modules/storage.jsm"), xnote.ns); /** * Constructor for the class Note using a file descriptor during creation of * the note. If the file does not exist, the note is initialized with * default values, otherwise it is initialized with the contents of the file. */ xnote.WL = {}; xnote.ns.Note = function (messageId) { //~ dump('\n->Note'); // --- internal variables ------------------------------------------ var _notesFile = xnote.ns.Storage.getNotesFile(messageId); var _modified = false; //result var pub = { //--- properties ---------------------------------------------------- get modified() { return _modified; }, set modified(value) { _modified = value; } } // Default values for a note window pub.DEFAULT_XNOTE_WIDTH = xnote.ns.Commons.xnotePrefs.width; pub.DEFAULT_XNOTE_HEIGHT = xnote.ns.Commons.xnotePrefs.height; pub.DEFAULT_X_ORIG = (window.outerWidth-pub.DEFAULT_XNOTE_WIDTH)/2; pub.DEFAULT_Y_ORIG =(window.outerHeight-pub.DEFAULT_XNOTE_HEIGHT)/2; pub.DEFAULT_X = xnote.ns.Commons.xnotePrefs.horPos; pub.DEFAULT_Y = xnote.ns.Commons.xnotePrefs.vertPos; //--- Intialisation (either from file or defaults) -------------------------- //~ dump('\n<-Note'); if (!_notesFile || !_notesFile.exists()) { pub.x = pub.DEFAULT_X; pub.y = pub.DEFAULT_Y; pub.width = pub.DEFAULT_XNOTE_WIDTH; pub.height = pub.DEFAULT_XNOTE_HEIGHT; pub.text = ''; pub.modificationDate = ''; //~ dump('\n<-note_charger'); } else { var fileInStream = Components.classes['@mozilla.org/network/file-input-stream;1'].createInstance(Components.interfaces.nsIFileInputStream); var fileScriptableIO = Components.classes['@mozilla.org/scriptableinputstream;1'].createInstance(Components.interfaces.nsIScriptableInputStream); fileInStream.init(_notesFile, 0x01, parseInt("0444", 8), null ); fileScriptableIO.init(fileInStream); pub.x = parseInt(fileScriptableIO.read(4)); pub.y = parseInt(fileScriptableIO.read(4)); pub.width = parseInt(fileScriptableIO.read(4)); pub.height = parseInt(fileScriptableIO.read(4)); pub.modificationDate = fileScriptableIO.read(32); // Changed because of this: // Just one comment - seems like xnote doesnt allow non-latin characters. // I am from Latvia (Letonnie in French I believe) and we have characters // like al�ki which are not preserved when saving a note ... // // this.text = fileScriptableIO.read(_notesFile.fileSize-16); pub.text = decodeURIComponent( fileScriptableIO.read(_notesFile.fileSize-48 )); fileScriptableIO.close(); fileInStream.close(); pub.text = pub.text.replace(/
    /g,'\n'); //~ dump('\n<-note_charger'); } //--- METHOD DEFINITIONS ------------------------------------------------- /** * Save the note in a file with the name of the message-id. If the content * of an existing note is empty, e.g., text was deleted, the note will be * deleted. */ pub.saveNote = function () { //~ dump('\n->saveNote'); if (pub.text=='') { if (_notesFile.exists()) { _notesFile.remove(false); } return false; } pub.text = pub.text.replace(/\n/g,'
    '); let tempFile = _notesFile.parent.clone(); tempFile.append("~"+_notesFile.leafName+".tmp"); // Using 0660 instead of 0600 so that sharing notes accross users // within the same group is possible on Linux. tempFile.createUnique(tempFile.NORMAL_FILE_TYPE, parseInt("0660",8)); let fileOutStream = Components.classes['@mozilla.org/network/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream); with (fileOutStream) { init(tempFile, 2, 0x200, false); // Opens for writing only write(String(pub.x), 4); write(String(pub.y), 4); write(String(pub.width), 4); write(String(pub.height), 4); write(pub.modificationDate, 32); // Changed because of this: // Just one comment - seems like xnote doesnt allow non-latin characters. // I am from Latvia (Letonnie in French I believe) and we have characters // like al�ki which are not preserved when saving a note ... // // fileOutStream.write(pub.text, pub.text.length); let contentencode = encodeURIComponent(pub.text); write(contentencode, contentencode.length); close(); } tempFile.moveTo(null, _notesFile.leafName); pub.modified = false; //~ dump('\n<-saveNote'); return true; } /** * Deletes the note on the disk drive. */ pub.deleteNote = function () { //~ dump('\n->note_supprimer'); if (_notesFile.exists()) { _notesFile.remove(false); //~ dump('\n->note_supprimer'); return true; } else { //~ dump('\n->note_supprimer'); return false; } } pub.toString = function() { return ('\n'+this.x+' ; '+this.y+' ; '+this.width+' ; '+this.height+' ; '+this.text+' ; ') } pub.exists = function() { return _notesFile.exists(); } return pub; } chrome/content/xnote-windowh.xhtml0000644000000000000000000000610014101570160016363 0ustar rootroot &xnote;