понедельник, 11 апреля 2016 г.

CustomButtons часть 9-я. Код, сохранение параметров и отдельные приложения.


  1. Структура кода
  2. Кнопки с настраиваемыми параметрами.
  3. Создание отдельного приложения для тестирования инструментов
  4. Кнопки с самомодифицирующимся кодом

Структура кода



Оверлей является наиболее удобным механизмом создания кнопок, вносящих изменение в структуру интерфейса. Тем не менее, используя оверлеи, надо понимать некоторые нюансы их работы, чтобы можно было без проблем отлаживать и использовать их код.

Когда оверлеи добавляются из расширений и загружаются с использованием встроенных механизмов браузера, то некоторых проблем, с которыми можно столкнуться при использовании описанных здесь приемов, не возникает. Проблемы могут возникать при повторной загрузке оверлея. Если разместить код во вкладке инициализации кнопки, то нельзя гарантировать, что инициализация выполнится только один раз за сессию, хотя бы даже потому, что она выполняется всякий раз при сохранении кнопки. Кроме того, если кнопка установлена более одного раза или за сессию она удалялась и после этого устанавливалась снова, то всякий раз будет происходить ее инициализация, а стало быть загрузка оверлея. Более того, во время отладки оверлей приходится загружать многократно после каждого редактирования.

Во многих случаях при каждой последующей загрузке изменения просто вступают в силу, не производя никаких побочных эффектов, но это далеко не всегда так. Обычно "безвредными" оказываются оверлеи, добавляющие какие-нибудь элементы меню или другие одиночные элементы. Они просто перезаписываются и все. Проблемы возникают в тех случаях, когда добавляемые элементы сами имеют какое-то содержимое (например как в случае с панелями), в этой ситуации при повторной загрузке содержимое просто дублируется, то есть к ранее добавленному содержимому оно добавляется еще раз. Даже одиночные элементы могут иметь в этом смысле проблемы, например если редактирование заключалось в том, что были убраны атрибуты, то вероятно при повторной загрузке они все-таки останутся, поскольку оверлей ничего не убирает.

Каждый раз в такой ситуации перезапускать браузер - идея плохая, поэтому об этом надо позаботиться при написании оверлеев. В предыдущей статье я использовал в примерах следующий подход: все добавляемые элементы имели атрибут data-del(имя атрибута можно подобрать какое нравится), которому я присваивал значение, ассоциируемое именно с этим оверлеем, а код скрипта оверлея начинался с того, что в документе ищутся элементы у которых есть такой атрибут с таким значением и они все удаляются. Естественно, добавлять атрибут надо только к добавляемым элементам верхнего уровня, поскольку при их удалении содержимое тоже будет удаляться.
Кликните здесь для просмотра всего текста
Код xmlВыделить
<?xml version="1.0" encoding="utf-8" ?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/javascript">
    <![CDATA[
// Удаляем элементы, добавленные этим оверлеем при предыдущей загрузке
for(old of document.querySelectorAll("[data-del='myOverlay']")) old.parentElement.removeChild(old);

// Остальной код
    ]]>
  </script>
  <popupset id="mainPopupSet">
    <panel id="myPanel" data-del="myOverlay">
      <vbox>
        <hbox></hbox>
        <vbox></vbox>
      </vbox>
    </panel>
  </popupset>
  <menupopup id="placesContext">
    <menuitem label="blablabla" data-del="myOverlay"/>
  </menupopup>
</overlay>


Кроме того, не будет лишним иметь еще кнопочку с кодом удаляющим из документа все, что добавлено всеми оверлеями, где эти элементы метились атрибутом data-del
Код javascriptВыделить
for(old of document.querySelectorAll("[data-del]")) old.parentElement.removeChild(old);
Со скриптами проблем обычно не возникает, тем не менее все-таки лучше глобальное пространство имен не загромождать и весь код упаковывать в один объект.

Загрузка оверлея осуществляется не совсем так, как это происходит на веб-странице, где достаточно расположить скрипт ниже элемента к которому он обращается и это обеспечит доступность элемента из этого скрипта, поскольку выполняться он будет после создания элемента. Здесь можно разместить скрипт внизу, но он все равно будет выполнен до создания элементов. Поэтому, если надо из скрипта обратиться к элементу и выполнить его первоначальную инициализацию(например подписаться на событие), этот код надо разместить в специальной функции и вызывать ее после загрузки, для этого надо испольлзовать второй параметр метода Document.loadOverlay() - Интерфейсы веб API | MDN.
Кликните здесь для просмотра всего текста
Код xmlВыделить
<?xml version="1.0" encoding="utf-8" ?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/javascript">
    <![CDATA[
for(old of document.querySelectorAll("[data-del='myOverlay']")) old.parentElement.removeChild(old);

// Остальной код
var myOverlay =
{
    init: function ()
    {
        document.getElementById("myPanel").addEventListener("popupshowing", evt => alert("А вот и панелька."), false);
    }
}
    
    ]]>
  </script>
  <popupset id="mainPopupSet">
    <panel id="myPanel" data-del="myOverlay">
      <vbox>
        <hbox></hbox>
        <vbox></vbox>
      </vbox>
    </panel>
  </popupset>
  <menupopup id="placesContext">
    <menuitem label="blablabla" data-del="myOverlay"/>
  </menupopup>
</overlay>


А загружать этот оверлей будем таким кодом.
Код javascriptВыделить
document.loadOverlay(pathToOverlay, {
    observe: function (subj, topic, data)
    {
        myOverlay.init();
    }
});

Кроме того, во время отладки удобно сделать кнопку загружающую оверлей из конкретного расположения, и назначить для нее сочетание клавиш, тогда можно будет, сохранив изменения оверлее, сразу переходить к окну браузера, нажать назначенное сочетание клавиш и тестировать изменения.

Кнопки с настраиваемыми параметрами.



Тут можно рассматривать два варианта: изменение параметров, действующее во время сессии (до ближайшего перезапуска приложения); и изменения, которые каким-то образом должны быть сохранены и доступны и при следующем запуске, а то, может быть и на синхронизируемых устройствах. Первая задача особых сложностей не вызывает, тут можно и меню с чекбоксами использовать и панели, которые при сокрытии никуда не деваются и могут держать информацию, да и сохранять ее в какую-то переменную. А вот если требуется настройка с сохранением, то тут уже надо остановиться подробнее.

Первое, что приходит в голову - это сохранение в файл. Но тут же встает вопрос о расположении файла. На другом компьютере может вообще не быть каталога, в который предполагается сохранение и даже его создание может оказаться невозможным (например в windows и linux адресация отличается). Здесь есть возможность создать папку кнопки в папке CustomButtons. Вот так можно получить путь к этой папке
Код javascriptВыделить
var propertiesService = Cc["@mozilla.org/file/directory_service;1"]
                        .getService(Ci.nsIProperties);
var currProfD = propertiesService.get("ProfD", Ci.nsIFile);
var profileDir = currProfD.path;
var cbfolder = profileDir + "\\custombuttons";

Можно создать в этой папке папку с именем кнопки и все данные скидывать туда. Однако здесь недостаток в том, что при удалении кнопки папка с данными никуда не денется, да и сохраненные данные не будут синхронизироваться.

Другой вариант: создать ветку в конфигурации, настройки сохранять в JavaScript-объект и отправлять его в формате JSON в эту ветку. Параметры конфигурации (насколько я знаю) синхронизируются, таким образом одна проблема решается, но удалять ветку видимо придется все-таки вручную или в меню кнопки можно предусмотреть возможность удаления ветки.

Пожалуй самый экзотический и в то же время, наиболее приемлемый, в силу того, что удовлетворяет задачам, вариант: кнопки с самомодифицирующимся кодом. Естественно, изменять программно логику программы не придется, но, учитывая, что в код кнопок можно включать не только логику, но и данные, то вот именно их мы и попробуем изменить. С сохранением кода кнопок мы уже имели дело, когда рассматривали редактирование кода во встроенном редакторе (Scratchpad). Здесь принцип тот же, только для редактирования данных мы не будем использовать редактор кода, а вместо этого соорудим инструмент, удобный именно для тех данных, которые нам понадобится редактировать. Для того, чтобы создать инструмент редактирования, нам потребуется среда, в которой мы могли бы его испытывать в процессе создания, поэтому, прежде чем переходить к примеру, рассмотрим еще один вопрос.

Создание отдельного приложения для тестирования инструментов



В некоторых случаях (например когда требуется создать объект со сложным интерфейсом или скрипт со сложной логикой и все это испытать и отладить) целесообразно писать код оверлеев и испытывать его в отдельных приложениях. Я уже упоминал XulRunner, который можно использовать для этих целей, но скачивать его совсем не обязательно. Для запуска приложений написанных для Mozilla вполне сгодится установленный Firefox. В тех случаях, когда нельзя гарантировать, что приложение будет работать на машинах, где установлен Farifox, XulRunner необходимо включать в дистрибутив, но для наших целей, когда приложения будут использоваться для отладки компонентов, предназначенный для включения в файрфокс, всегда можно гарантировать, что браузер в системе присутствует.

В качестве примера создадим небольшое приложение, в котором будем тестировать небольшую панель, созданную для редактирования списка автозамены. В дальнейшем мы используем эту панель для создания самомодифицирующейся кнопки, которая, собственно, и будет привносить в браузер функцию автозамены.

Для начала создадим папку приложения, с именем приложения(у меня это имя MyTestApp). Содержимое папки будет следующим:
  • MyTestApp
    • chrome
      • content
        • autochangeOverlay.xul
        • autochange.js
        • main.xul
    • defaults
      • preferences
        • prefs.js
    • application.ini
    • chrome.manifest

Содержимое файлов:
Кликните здесь для просмотра всего текста

application.ini
Цитата:
[App]
Vendor=diadiavova
Name=My Test Application
Version=1.0
BuildID=20160327
Copyright=Copyright (c) 2016
ID=MyTestApp@diadiavova.org

[Gecko]
MinVersion=1.9.1
MaxVersion=200.*
chrome.manifest
Цитата:
content MyTestApp file:chrome/content/
prefs.js
Цитата:
pref("toolkit.defaultChromeURI", "chrome://MyTestApp/content/main.xul");

Остальные файлы - это уже окно, оверлей и скрипт, которые и будут содержимым нашего приложения.
У основного окна main.xul интерфейс можно сделать минимальным. В нем должны присутствовать все элементы, указанные в оверлее в качестве цели слияния. Ну, например, мы будем в оверлее создавать панель, которую предполагается разместить в <popupset id="mainPopupSet">, естественно такой элемент надо добавить в окно. Или, если мы хотим создать сочетание клавиш быстрого доступа к какой-нибудь команде, то нам придется в главном окне разместить элемент keyset с нужным id. Для отображения панели создадим тулбар и кнопку, кроме того, надо указать, что должен быть загружен оверлей, да вот, в общем-то, и все. Таким образом имеем файлы следующего вида
Кликните здесь для просмотра всего текста
main.xul
Код xmlВыделить
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xul-overlay href="autochangeOverlay.xul"?>
<window id="main" title="Тестовое окно" width="700" height="500"
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   
  <keyset id="mainKeyset">
  </keyset>

  <popupset id="mainPopupSet">
     
  </popupset>
  <toolbar>
    <toolbarbutton popup="editAutochangeListPanel" label="Показать панель"/>
  </toolbar>
   
</window>

autochangeOverlay.xul
Код xmlВыделить
<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript" src="chrome://MyTestApp/content/autochange.js"></script>

  <keyset id="mainKeyset">
    <key key="A" modifiers="alt" oncommand="autochange.addRow();"/>
  </keyset>
   
  <popupset id="mainPopupSet">
    <panel id="templates" data-del="editAutochange">
      <grid hidden="true">
        <columns>
          <column/>
          <column/>
        </columns>
        <rows id="rowForClone">
          <row >
            <checkbox/>
            <textbox/>
            <textbox/>
          </row>
        </rows>
      </grid>
    </panel>
    <panel id="editAutochangeListPanel" titlebar="normal" backdrag="true" label="Редактировать список автозамены" noautohide="true" close="true" data-del="editAutochange">
      <menubar>
        <menu label="Список">
          <menupopup>
            <menuitem label="Добавить строку" oncommand="autochange.addRow();"/>
            <menuitem label="Удалить выделенные строки" oncommand="autochange.deleteSelectedRows();"/>
            <menuitem label="Удалить пустые строки" oncommand="autochange.deleteEmptyRows();"/>
            <menuitem label="Очистить выделенные строки" oncommand="autochange.clearSelectedRows();"/>
            <menuitem label="Выделить все" oncommand="autochange.selectAllRows();"/>
            <menuitem label="Снять выделение" oncommand="autochange.deselectAllRows();"/>
            <menuitem label="Обратить выделение" oncommand="autochange.invertSelection();"/>
            <menuitem label="Показать результат" oncommand="autochange.showResult();"/>
          </menupopup>
        </menu>
        <menu label="Файл">
          <menupopup>
            <menuitem label="Сохранить список" oncommand="autochange.saveJson();"/>
            <menuitem label="Загрузить список" oncommand="autochange.loadJson(true);"/>
            <menuitem label="Добавить из файла" oncommand="autochange.loadJson(false);"/>
            <menuitem label="Изменить код кнопки" oncommand="autochange.saveChanges();"/>
          </menupopup>
        </menu>
      </menubar>
      <vbox style="overflow: auto;height:300px;">
        <hbox>
          <checkbox style="visibility:hidden;"/>
          <label flex="1" align="center">Сочетание символов</label>
          <label flex="3" align="center">Текст автозамены</label>
        </hbox>
        <grid width="500">
          <columns>
            <column/>
            <column flex="1"></column>
            <column flex="3"></column>
          </columns>
          <rows>
            <row>
              <checkbox/>
              <textbox/>
              <textbox/>
            </row>
          </rows>
        </grid>
      </vbox>
    </panel>
     
     
  </popupset>
</overlay>

autochange.js
Код javascriptВыделить
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");

var autochange =
{
    addRow: function ()
    {
        return document.querySelector("#editAutochangeListPanel rows").appendChild(document.querySelector("#rowForClone row").cloneNode(true));
    },

    deleteSelectedRows: function ()
    {
        for(sr of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            if (sr.querySelector("checkbox").checked)
            {
                sr.parentElement.removeChild(sr);
            }
        }
    },

    clearSelectedRows: function ()
    {
        for(sr of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            if (sr.querySelector("checkbox").checked)
            {
                for(tb of sr.querySelectorAll("textbox")) tb.value = "";
            }
        }
    },

    selectAllRows: function ()
    {
        for(chb of document.querySelectorAll("#editAutochangeListPanel checkbox"))
        {
            chb.checked = true;
        }
    },

    deselectAllRows: function ()
    {
        for(chb of document.querySelectorAll("#editAutochangeListPanel checkbox"))
        {
            chb.checked = false;
        }
    },

    invertSelection: function ()
    {
        for(chb of document.querySelectorAll("#editAutochangeListPanel checkbox"))
        {
            chb.checked = !chb.checked;
        }
    },

    deleteEmptyRows: function ()
    {
        for(sr of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            let tbs = sr.querySelectorAll("textbox");
            if (tbs[0].value == "" || tbs[1].value == "")
            {
                sr.parentElement.removeChild(sr);
            }
        }
    },

    getAutochangeObj()
    {
        let rv = {};
        for(row of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            let tbs = row.querySelectorAll("textbox");
            rv[tbs[0].value] = tbs[1].value;
        }
        return rv;
    },

    setAutochangObj: function (ao)
    {

    },

    showResult: function ()
    {
        alert(JSON.stringify(this.getAutochangeObj()));
    },

    saveJson: function ()
    {
        const nsIFilePicker = Components.interfaces.nsIFilePicker;
        var fp = Components.classes["@mozilla.org/filepicker;1"]
                 .createInstance(Components.interfaces.nsIFilePicker);
        fp.init(window, "Сохранение списка автозамены", nsIFilePicker.modeSave);
        fp.appendFilter("Файлы JSON", "*.json");
        fp.defaultExtension = "json";
        var rv = fp.show();
        if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)
        {
            var file = fp.file;
            var path = fp.file.path;
            var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].
               createInstance(Components.interfaces.nsIFileOutputStream);
            foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
            var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"].
                            createInstance(Components.interfaces.nsIConverterOutputStream);
            converter.init(foStream, "UTF-8", 0, 0);
            converter.writeString(JSON.stringify(this.getAutochangeObj()));
            converter.close();
        }

    },

    loadJson: function (delRows)
    {
        const nsIFilePicker = Components.interfaces.nsIFilePicker;
        var fp = Components.classes["@mozilla.org/filepicker;1"]
                 .createInstance(Components.interfaces.nsIFilePicker);
        fp.init(window, "Сохранение списка автозамены", nsIFilePicker.modeOpen);
        fp.appendFilter("Файлы JSON", "*.json");
        fp.defaultExtension = "json";
        var rv = fp.show();
        if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)
        {
            var file = fp.file;
            var path = fp.file.path;
            var data = "";
            var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
                          createInstance(Components.interfaces.nsIFileInputStream);
            var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
                          createInstance(Components.interfaces.nsIConverterInputStream);
            fstream.init(file, -1, 0, 0);
            cstream.init(fstream, "UTF-8", 0, 0);

            let str = {};
            {
                let read = 0;
                do
                {
                    read = cstream.readString(0xffffffff, str);
                    data += str.value;
                } while (read != 0);
            }
            cstream.close();
            var obj = JSON.parse(data);
            if (delRows)
            {
                this.selectAllRows();
                this.deleteSelectedRows();
            }
            for (let k in obj)
            {
                let tbs = this.addRow().querySelectorAll("textbox");
                tbs[0].value = k;
                tbs[1].value = obj[k];
            }
        }
    },
    saveChanges: function ()
    {

    }
}



Теперь, для того, чтобы все это дело запустить, в командной строке надо выполнить следующую команду.
Код unknownВыделить
Само собой, вместо указанных путей надо ввести реальные пути к файлам firefox.exe и applicati
"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"  -app "C:\Users\diadiavova\Documents\Visual Studio 2015\Projects\MozillaProjects\MozillaProjects\MyTestApp/application.ini"
on.ini. Правда, если запускать приложение таким образом, то скрипты будут кэшироваться, а это значит, что если после первого запуска будут внесены изменения в скрипты, то они уже никак не повлияют на результат. Наиболее надежным способом избежать этого является добавление еще одного аргумента в строку запуска
Код unknownВыделить
"C:\Program Files (x86)\Mozilla Firefox\firefox.exe" -app "C:\Users\diadiavova\Documents\Visual Studio 2015\Projects\MozillaProjects\MozillaProjects\MyTestApp/application.ini" -purgecaches
Кроме того, об отладке XUL-приложений можно почитать здесь
Debugging a XULRunner Application - Archive of obsolete content | MDN. Правда не факт, что это будет работать, поскольку что-то там могло устареть. Например использование prefs для предотвращение кеширования скриптов не помогает, поэтому лучше использовать -purgecaches при запуске.

Запускать приложение из командной строки - неудобно, поэтому лучше создать ярлык. Недостаток ярлыка в том, что путь к application.ini должен быть абсолютным, а если папка будет перенесена, то и ярлык придется редактировать. Это неудобно, например, если приложение пишется на нескольких компьютерах, которые синхронизируются. Пути на этих компьютерах могут отличаться. Да и путь к firefox.exe тоже может быть разным, а если это не windows, то и называться исполняемый файл будет иначе (во всяком случае без .exe). Здесь проблему можно решить использованием программы или скрипта, написанной на любом языке, которая определяет пути и выполняет запуск. Для примера представлю такой скрипт, написанный на языке VBScript
runApp.vbs
Код vbВыделить
Dim WshShell
Set WshShell = CreateObject("WScript.Shell")
Dim version
version = WshShell.RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Mozilla\Mozilla Firefox\CurrentVersion")
Dim ffpath
ffpath = WshShell.RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Mozilla\Mozilla Firefox\" & version & "\Main\PathToExe")
Dim runstr
runstr = """" & ffpath & """ -app """ & WshShell.CurrentDirectory & "/application.ini"" -purgecaches"
WshShell.Run(runstr)

Файл надо расположить в корневом каталоге приложения (там же где и application.ini) и запускать его. Правда, в сравнении с ярлыком, здесь есть маленький недостаток. На панели задач приложение будет открываться как одно из окон файрфокса, вместо того, чтобы отображаться отдельным значком. Вроде бы мелочь, но когда есть отдельный значок, его можно закрепить и во время редактирования его файлов, такое приложение будет легко запускать. Но и тут нам может помочь небольшой скрипт.
createShortcut.vbs
Код vbВыделить
Dim WshShell
Set WshShell = CreateObject("WScript.Shell")
Dim version
version = WshShell.RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Mozilla\Mozilla Firefox\CurrentVersion")
Dim ffpath
ffpath = WshShell.RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Mozilla\Mozilla Firefox\" & version & "\Main\PathToExe")
Dim lnk
Set lnk = WshShell.CreateShortcut(WshShell.CurrentDirectory & "/runApp.lnk")
lnk.TargetPath =  ffpath
lnk.Arguments = " -app """ & WshShell.CurrentDirectory & "/application.ini"" -purgecaches"
lnk.Save

Этот файл можно расположить там же где и предыдущий, только при запуске он будет создавать ярлык с нужными параметрами, а если ярлык с таким именем в файле уже существует, то он будет исправлен и с его помощью можно буедт запускать приложение.

Кнопки с самомодифицирующимся кодом



В принципе все вопросы, необходимые для создания такой кнопки, мы уже рассмотрели, так что теперь осталось только собрать все воедино и еще кое-что дописать.

Рассмотрим пример с автозаменой. Такой скрипт для GreaseMonkey описан здесь, но для Custombuttons придется его немного модифицировать. Кроме того, нам понадобится код код отсюдаhttp://www.cyberforum.ru/blogs/55761..._executeonload
Код javascriptВыделить
if (!gBrowser.loadHandlers)
{
    gBrowser.loadHandlers = [];

    gBrowser.addEventListener("load", function (evt)
    {
        gBrowser.loadHandlers.forEach(function (h) { h(evt); });
    }, true);

}

gBrowser.loadHandlers.pop();
gBrowser.loadHandlers.push(function (evt)
{

});

Ну и само собой - панелька из предыдущего параграфа для редактирования словаря автозамены. Скрипт в панели мы использовали отдельно, но, учитывая способ, которым мы загружаем оврелеи с помощью кнопок, его надо вставить в код оверлея. Кроме того надо дописать код сохранения изменений в кнопке, который было описан здесь. Ну и кнопочка для создание кода кнопки из оверлея, само собой, тоже понадобится.

При нажатии на кнопку у нас должна появляться панель. Поскольку панель изменяет код кнопки, в ней должна содержаться ссылка на вызвавшую ее кнопку. То есть код команды будет таким
Код javascriptВыделить
/*CODE*/
var panel = document.getElementById("editAutochangeListPanel");
panel.button = this;
panel.openPopupAtScreen(200, 200, false);

На вкладке Инициализация нам потребуется на событие load объекта gBrowser и выполнить код из примера для GreaseMonkey, с учетом изменений среды. Кроме того там же надо выполнить сохранение и загрузку оверлея. После этого панель надо инициировать текущим состоянием словаря.

Для модификации кода будем использовать простой прием. Словарь автозамены в коде хранится в переменной objectData, для того, чтобы его легко можно было найти программно, просто пометим его специальными дескрипторами
Код javascriptВыделить
var objectData = /*startobjectdata*/{}/*endobjectdata*/;

Таким образом, для изменения кода нам понадобится просто искать эти дескрипторы и заменять в коде все, что между ними.
Оверлей с панелькой имеет следующий вид
Кликните здесь для просмотра всего текста
Код xmlВыделить
<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript">
    <![CDATA[
<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript">
<![CDATA[
for(old of document.querySelectorAll("[data-del='editAutochange']")) old.parentElement.removeChild(old);
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");

var autochange =
{
    init: function (dict)
    {
        this.selectAllRows();
        this.deleteSelectedRows();
        for (let k in dict)
        {
            let tbs = this.addRow().querySelectorAll("textbox");
            tbs[0].value = k;
            tbs[1].value = dict[k];
        }
    },

    addRow: function ()
    {
        return document.querySelector("#editAutochangeListPanel rows").appendChild(document.querySelector("#rowForClone row").cloneNode(true));
    },

    deleteSelectedRows: function ()
    {
        for(sr of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            if (sr.querySelector("checkbox").checked)
            {
                sr.parentElement.removeChild(sr);
            }
        }
    },

    clearSelectedRows: function ()
    {
        for(sr of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            if (sr.querySelector("checkbox").checked)
            {
                for(tb of sr.querySelectorAll("textbox")) tb.value = "";
            }
        }
    },

    selectAllRows: function ()
    {
        for(chb of document.querySelectorAll("#editAutochangeListPanel checkbox"))
        {
            chb.checked = true;
        }
    },

    deselectAllRows: function ()
    {
        for(chb of document.querySelectorAll("#editAutochangeListPanel checkbox"))
        {
            chb.checked = false;
        }
    },

    invertSelection: function ()
    {
        for(chb of document.querySelectorAll("#editAutochangeListPanel checkbox"))
        {
            chb.checked = !chb.checked;
        }
    },

    deleteEmptyRows: function ()
    {
        for(sr of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            let tbs = sr.querySelectorAll("textbox");
            if (tbs[0].value == "" || tbs[1].value == "")
            {
                sr.parentElement.removeChild(sr);
            }
        }
    },

    getAutochangeObj()
    {
        let rv = {};
        for(row of document.querySelectorAll("#editAutochangeListPanel row"))
        {
            let tbs = row.querySelectorAll("textbox");
            rv[tbs[0].value] = tbs[1].value;
        }
        return rv;
    },

    showResult: function ()
    {
        alert(JSON.stringify(this.getAutochangeObj()));
    },

    saveJson: function ()
    {
        const nsIFilePicker = Components.interfaces.nsIFilePicker;
        var fp = Components.classes["@mozilla.org/filepicker;1"]
                 .createInstance(Components.interfaces.nsIFilePicker);
        fp.init(window, "Сохранение списка автозамены", nsIFilePicker.modeSave);
        fp.appendFilter("Файлы JSON", "*.json");
        fp.defaultExtension = "json";
        var rv = fp.show();
        if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)
        {
            var file = fp.file;
            var path = fp.file.path;
            var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].
               createInstance(Components.interfaces.nsIFileOutputStream);
            foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
            var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"].
                            createInstance(Components.interfaces.nsIConverterOutputStream);
            converter.init(foStream, "UTF-8", 0, 0);
            converter.writeString(JSON.stringify(this.getAutochangeObj()));
            converter.close();
        }

    },

    loadJson: function (delRows)
    {
        const nsIFilePicker = Components.interfaces.nsIFilePicker;
        var fp = Components.classes["@mozilla.org/filepicker;1"]
                 .createInstance(Components.interfaces.nsIFilePicker);
        fp.init(window, "Сохранение списка автозамены", nsIFilePicker.modeOpen);
        fp.appendFilter("Файлы JSON", "*.json");
        fp.defaultExtension = "json";
        var rv = fp.show();
        if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)
        {
            var file = fp.file;
            var path = fp.file.path;
            var data = "";
            var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
                          createInstance(Components.interfaces.nsIFileInputStream);
            var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
                          createInstance(Components.interfaces.nsIConverterInputStream);
            fstream.init(file, -1, 0, 0);
            cstream.init(fstream, "UTF-8", 0, 0);

            let str = {};
            {
                let read = 0;
                do
                {
                    read = cstream.readString(0xffffffff, str);
                    data += str.value;
                } while (read != 0);
            }
            cstream.close();
            var obj = JSON.parse(data);
            if (delRows)
            {
                this.selectAllRows();
                this.deleteSelectedRows();
            }
            for (let k in obj)
            {
                let tbs = this.addRow().querySelectorAll("textbox");
                tbs[0].value = k;
                tbs[1].value = obj[k];
            }
        }
    },

    changeButtonInitCode: function (code)
    {
        return code.replace(/\/\*startobjectdata\*\/.+\/\*endobjectdata\*\//, "/*startobjectdata*/" + JSON.stringify(this.getAutochangeObj()) + "/*endobjectdata*/");
    },

    saveChanges: function ()
    {
        var panel = document.querySelector("#editAutochangeListPanel");
        var init = panel.button.getAttribute("cb-init");
        panel.button.setAttribute("cb-init", this.changeButtonInitCode(init));
        custombuttons.cbService.parseButtonURI(panel.button);
        var link = "custombutton://buttons/Firefox/update/" + panel.button.id;
        custombuttons.cbService.updateButton(link, panel.button.URI);
    }

}

  ]]>
  </script>

  <popupset id="mainPopupSet">
    <panel id="templates" data-del="editAutochange">
      <grid hidden="true">
        <columns>
          <column/>
          <column/>
        </columns>
        <rows id="rowForClone">
          <row >
            <checkbox/>
            <textbox/>
            <textbox/>
          </row>
        </rows>
      </grid>
    </panel>
    <panel id="editAutochangeListPanel" titlebar="normal" backdrag="true" label="Редактировать список автозамены" noautohide="true" close="true" data-del="editAutochange">
      <menubar>
        <menu label="Список">
          <menupopup>
            <menuitem label="Добавить строку" oncommand="autochange.addRow();"/>
            <menuitem label="Удалить выделенные строки" oncommand="autochange.deleteSelectedRows();"/>
            <menuitem label="Удалить пустые строки" oncommand="autochange.deleteEmptyRows();"/>
            <menuitem label="Очистить выделенные строки" oncommand="autochange.clearSelectedRows();"/>
            <menuitem label="Выделить все" oncommand="autochange.selectAllRows();"/>
            <menuitem label="Снять выделение" oncommand="autochange.deselectAllRows();"/>
            <menuitem label="Обратить выделение" oncommand="autochange.invertSelection();"/>
            <menuitem label="Показать результат" oncommand="autochange.showResult();"/>
          </menupopup>
        </menu>
        <menu label="Файл">
          <menupopup>
            <menuitem label="Сохранить список" oncommand="autochange.saveJson();"/>
            <menuitem label="Загрузить список" oncommand="autochange.loadJson(true);"/>
            <menuitem label="Добавить из файла" oncommand="autochange.loadJson(false);"/>
            <menuitem label="Изменить код кнопки" oncommand="autochange.saveChanges();"/>
          </menupopup>
        </menu>
      </menubar>
      <vbox style="overflow: auto;height:300px;">
        <hbox>
          <checkbox style="visibility:hidden;"/>
          <label flex="1" align="center">Сочетание символов</label>
          <label flex="3" align="center">Текст автозамены</label>
        </hbox>
        <grid width="500">
          <columns>
            <column/>
            <column flex="1"></column>
            <column flex="3"></column>
          </columns>
          <rows>
            <row>
              <checkbox/>
              <textbox/>
              <textbox/>
            </row>
          </rows>
        </grid>
      </vbox>
    </panel>
  </popupset>
</overlay>

А код инициализации кнопки, будет таким
Кликните здесь для просмотра всего текста
Код javascriptВыделить
/*Initialization Code*/
if (!gBrowser.loadHandlers)
{
    gBrowser.loadHandlers = [];

    gBrowser.addEventListener("load", function (evt)
    {
        gBrowser.loadHandlers.forEach(function (h) { h(evt); });
    }, true);
}


var objectData = /*startobjectdata*/{}/*endobjectdata*/;
gBrowser.loadHandlers.pop();
gBrowser.loadHandlers.push(function (loadEvt)
{
    var shortcutKey = {
        alt: false,
        ctrl: true,
        shift: false,
        charcode: 32
    };

    function changeText(textarea, dataObj)
    {
        var words = textarea.value.substring(0, textarea.selectionStart).split(/\s+/);
        var lastword = words[words.length - 1].toLowerCase();
        if (dataObj[lastword])
        {
            var startSelection = textarea.selectionStart - lastword.length;
            var changeText = dataObj[lastword];
            var newselpos = startSelection + changeText.length;
            var textbefore = textarea.value.substr(0, startSelection);
            var textafter = textarea.value.substring(textarea.selectionStart);
            textarea.value = textbefore + dataObj[lastword] + textafter;
            textarea.selectionStart = textarea.selectionEnd = newselpos;
        }
    };

    var textareas = loadEvt.target.getElementsByTagName("textarea");

    loadEvt.target.body.addEventListener("keypress", function (evt)
    {
        if ((evt.target.tagName == "TEXTAREA" || (evt.target.tagName == "INPUT" && evt.target.type == "text")) &&
            evt.altKey == shortcutKey.alt &&
            evt.ctrlKey == shortcutKey.ctrl &&
            evt.shiftKey == shortcutKey.shift &&
            evt.charCode == shortcutKey.charcode)
        {
            evt.stopPropagation();
            evt.preventDefault();
            changeText(evt.target, objectData);
        }
    }, false);

});

var overlayCode = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<overlay xmlns=\"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\">\r\n  <script type=\"application/x-javascript\">\r\n<![CDATA[\r\nfor(old of document.querySelectorAll(\"[data-del='editAutochange']\")) old.parentElement.removeChild(old);\r\nComponents.utils.import(\"resource://gre/modules/NetUtil.jsm\");\r\nComponents.utils.import(\"resource://gre/modules/FileUtils.jsm\");\r\n\r\nvar autochange =\r\n{\r\n    init: function (dict)\r\n    {\r\n        this.selectAllRows();\r\n        this.deleteSelectedRows();\r\n        for (let k in dict)\r\n        {\r\n            let tbs = this.addRow().querySelectorAll(\"textbox\");\r\n            tbs[0].value = k;\r\n            tbs[1].value = dict[k];\r\n        }\r\n    },\r\n\r\n    addRow: function ()\r\n    {\r\n        return document.querySelector(\"#editAutochangeListPanel rows\").appendChild(document.querySelector(\"#rowForClone row\").cloneNode(true));\r\n    },\r\n\r\n    deleteSelectedRows: function ()\r\n    {\r\n        for(sr of document.querySelectorAll(\"#editAutochangeListPanel row\"))\r\n        {\r\n            if (sr.querySelector(\"checkbox\").checked)\r\n            {\r\n                sr.parentElement.removeChild(sr);\r\n            }\r\n        }\r\n    },\r\n\r\n    clearSelectedRows: function ()\r\n    {\r\n        for(sr of document.querySelectorAll(\"#editAutochangeListPanel row\"))\r\n        {\r\n            if (sr.querySelector(\"checkbox\").checked)\r\n            {\r\n                for(tb of sr.querySelectorAll(\"textbox\")) tb.value = \"\";\r\n            }\r\n        }\r\n    },\r\n\r\n    selectAllRows: function ()\r\n    {\r\n        for(chb of document.querySelectorAll(\"#editAutochangeListPanel checkbox\"))\r\n        {\r\n            chb.checked = true;\r\n        }\r\n    },\r\n\r\n    deselectAllRows: function ()\r\n    {\r\n        for(chb of document.querySelectorAll(\"#editAutochangeListPanel checkbox\"))\r\n        {\r\n            chb.checked = false;\r\n        }\r\n    },\r\n\r\n    invertSelection: function ()\r\n    {\r\n        for(chb of document.querySelectorAll(\"#editAutochangeListPanel checkbox\"))\r\n        {\r\n            chb.checked = !chb.checked;\r\n        }\r\n    },\r\n\r\n    deleteEmptyRows: function ()\r\n    {\r\n        for(sr of document.querySelectorAll(\"#editAutochangeListPanel row\"))\r\n        {\r\n            let tbs = sr.querySelectorAll(\"textbox\");\r\n            if (tbs[0].value == \"\" || tbs[1].value == \"\")\r\n            {\r\n                sr.parentElement.removeChild(sr);\r\n            }\r\n        }\r\n    },\r\n\r\n    getAutochangeObj()\r\n    {\r\n        let rv = {};\r\n        for(row of document.querySelectorAll(\"#editAutochangeListPanel row\"))\r\n        {\r\n            let tbs = row.querySelectorAll(\"textbox\");\r\n            rv[tbs[0].value] = tbs[1].value;\r\n        }\r\n        return rv;\r\n    },\r\n\r\n    showResult: function ()\r\n    {\r\n        alert(JSON.stringify(this.getAutochangeObj()));\r\n    },\r\n\r\n    saveJson: function ()\r\n    {\r\n        const nsIFilePicker = Components.interfaces.nsIFilePicker;\r\n        var fp = Components.classes[\"@mozilla.org/filepicker;1\"]\r\n                 .createInstance(Components.interfaces.nsIFilePicker);\r\n        fp.init(window, \"Сохранение списка автозамены\", nsIFilePicker.modeSave);\r\n        fp.appendFilter(\"Файлы JSON\", \"*.json\");\r\n        fp.defaultExtension = \"json\";\r\n        var rv = fp.show();\r\n        if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)\r\n        {\r\n            var file = fp.file;\r\n            var path = fp.file.path;\r\n            var foStream = Components.classes[\"@mozilla.org/network/file-output-stream;1\"].\r\n               createInstance(Components.interfaces.nsIFileOutputStream);\r\n            foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);\r\n            var converter = Components.classes[\"@mozilla.org/intl/converter-output-stream;1\"].\r\n                            createInstance(Components.interfaces.nsIConverterOutputStream);\r\n            converter.init(foStream, \"UTF-8\", 0, 0);\r\n            converter.writeString(JSON.stringify(this.getAutochangeObj()));\r\n            converter.close();\r\n        }\r\n\r\n    },\r\n\r\n    loadJson: function (delRows)\r\n    {\r\n        const nsIFilePicker = Components.interfaces.nsIFilePicker;\r\n        var fp = Components.classes[\"@mozilla.org/filepicker;1\"]\r\n                 .createInstance(Components.interfaces.nsIFilePicker);\r\n        fp.init(window, \"Сохранение списка автозамены\", nsIFilePicker.modeOpen);\r\n        fp.appendFilter(\"Файлы JSON\", \"*.json\");\r\n        fp.defaultExtension = \"json\";\r\n        var rv = fp.show();\r\n        if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)\r\n        {\r\n            var file = fp.file;\r\n            var path = fp.file.path;\r\n            var data = \"\";\r\n            var fstream = Components.classes[\"@mozilla.org/network/file-input-stream;1\"].\r\n                          createInstance(Components.interfaces.nsIFileInputStream);\r\n            var cstream = Components.classes[\"@mozilla.org/intl/converter-input-stream;1\"].\r\n                          createInstance(Components.interfaces.nsIConverterInputStream);\r\n            fstream.init(file, -1, 0, 0);\r\n            cstream.init(fstream, \"UTF-8\", 0, 0);\r\n\r\n            let str = {};\r\n            {\r\n                let read = 0;\r\n                do\r\n                {\r\n                    read = cstream.readString(0xffffffff, str);\r\n                    data += str.value;\r\n                } while (read != 0);\r\n            }\r\n            cstream.close();\r\n            var obj = JSON.parse(data);\r\n            if (delRows)\r\n            {\r\n                this.selectAllRows();\r\n                this.deleteSelectedRows();\r\n            }\r\n            for (let k in obj)\r\n            {\r\n                let tbs = this.addRow().querySelectorAll(\"textbox\");\r\n                tbs[0].value = k;\r\n                tbs[1].value = obj[k];\r\n            }\r\n        }\r\n    },\r\n\r\n    changeButtonInitCode: function (code)\r\n    {\r\n        return code.replace(/\\/\\*startobjectdata\\*\\/.+\\/\\*endobjectdata\\*\\//, \"/*startobjectdata*/\" + JSON.stringify(this.getAutochangeObj()) + \"/*endobjectdata*/\");\r\n    },\r\n\r\n    saveChanges: function ()\r\n    {\r\n        var panel = document.querySelector(\"#editAutochangeListPanel\");\r\n        var init = panel.button.getAttribute(\"cb-init\");\r\n        panel.button.setAttribute(\"cb-init\", this.changeButtonInitCode(init));\r\n        custombuttons.cbService.parseButtonURI(panel.button);\r\n        var link = \"custombutton://buttons/Firefox/update/\" + panel.button.id;\r\n        custombuttons.cbService.updateButton(link, panel.button.URI);\r\n    }\r\n\r\n}\r\n\r\n  ]]>\r\n  </script>\r\n  \r\n  <popupset id=\"mainPopupSet\">\r\n    <panel id=\"templates\" data-del=\"editAutochange\">\r\n      <grid hidden=\"true\">\r\n        <columns>\r\n          <column/>\r\n          <column/>\r\n        </columns>\r\n        <rows id=\"rowForClone\">\r\n          <row >\r\n            <checkbox/>\r\n            <textbox/>\r\n            <textbox/>\r\n          </row>\r\n        </rows>\r\n      </grid>\r\n    </panel>\r\n    <panel id=\"editAutochangeListPanel\" titlebar=\"normal\" backdrag=\"true\" label=\"Редактировать список автозамены\" noautohide=\"true\" close=\"true\" data-del=\"editAutochange\">\r\n      <menubar>\r\n        <menu label=\"Список\">\r\n          <menupopup>\r\n            <menuitem label=\"Добавить строку\" oncommand=\"autochange.addRow();\"/>\r\n            <menuitem label=\"Удалить выделенные строки\" oncommand=\"autochange.deleteSelectedRows();\"/>\r\n            <menuitem label=\"Удалить пустые строки\" oncommand=\"autochange.deleteEmptyRows();\"/>\r\n            <menuitem label=\"Очистить выделенные строки\" oncommand=\"autochange.clearSelectedRows();\"/>\r\n            <menuitem label=\"Выделить все\" oncommand=\"autochange.selectAllRows();\"/>\r\n            <menuitem label=\"Снять выделение\" oncommand=\"autochange.deselectAllRows();\"/>\r\n            <menuitem label=\"Обратить выделение\" oncommand=\"autochange.invertSelection();\"/>\r\n            <menuitem label=\"Показать результат\" oncommand=\"autochange.showResult();\"/>\r\n          </menupopup>\r\n        </menu>\r\n        <menu label=\"Файл\">\r\n          <menupopup>\r\n            <menuitem label=\"Сохранить список\" oncommand=\"autochange.saveJson();\"/>\r\n            <menuitem label=\"Загрузить список\" oncommand=\"autochange.loadJson(true);\"/>\r\n            <menuitem label=\"Добавить из файла\" oncommand=\"autochange.loadJson(false);\"/>\r\n            <menuitem label=\"Изменить код кнопки\" oncommand=\"autochange.saveChanges();\"/>\r\n          </menupopup>\r\n        </menu>\r\n      </menubar>\r\n          <vbox style=\"overflow: auto;height:300px;\">\r\n            <hbox>\r\n              <checkbox style=\"visibility:hidden;\"/>\r\n              <label flex=\"1\" align=\"center\">Сочетание символов</label>\r\n              <label flex=\"3\" align=\"center\">Текст автозамены</label>\r\n            </hbox>\r\n            <grid width=\"500\">\r\n              <columns>\r\n                <column/>\r\n                <column flex=\"1\"></column>\r\n                <column flex=\"3\"></column>\r\n              </columns>\r\n              <rows>\r\n                <row>\r\n                  <checkbox/>\r\n                  <textbox/>\r\n                  <textbox/>\r\n                </row>\r\n              </rows>\r\n            </grid>\r\n          </vbox>\r\n    </panel>\r\n  </popupset>\r\n</overlay>".replace('#current_custom_button_id', this.getAttribute('id'));
function saveAndLoadOverlay(code)
{
    Components.utils.import("resource://gre/modules/FileUtils.jsm");

    var file = FileUtils.getFile("TmpD", ["temp_overlay_file.xul"]);
    file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
    var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].
                   createInstance(Components.interfaces.nsIFileOutputStream);
    foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
    var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"].
                    createInstance(Components.interfaces.nsIConverterOutputStream);
    converter.init(foStream, "UTF-8", 0, 0);
    converter.writeString(code);
    converter.close();
    document.loadOverlay("file:///" + file.path, {
        observe: function (sybj, topic, data)
        {
            autochange.init(objectData);
        }
    });
}; saveAndLoadOverlay(overlayCode);

Здесь код, сформированный кнопкой, создающей код из оверлея, пришлось немного изменить, чтобы вызвать autochange.init при загрузке оверлея и передать ей текущее состояние словаря.

Вложение содержит файлы приложения, описанного в этой статье (папка MyTestApp), HTML-страница со ссылками-кнопками, описанными в сатьях(теми, которые я сохранил), и папка с черновиками, где содержатся те файлы, код которых я приводил ранее. Не обязательно они совпадают с описанным, я просто собрал все в кучу и упаковал в архив.