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

CustomButtons часть 8-я. Улучшаем инструменты разработки

  1. Внесение изменений в редактор кода
  2. Воспользуемся встроенным в браузер редактором JavaScript
  3. Получение и изменение кода кнопки
  4. Привязка редактора к кнопке
  5. Редактирование букмарклета с помощью встроенного редактора
  6. Использование внешнего редактора для редактирования кода.

Внесение изменений в редактор кода



Редактор кода в CustomButtons, мягко говоря, оставляет желать лучшего. По большому счету для более-менее сложного кода его лучше вообще не использовать, а вместо этого воспользоваться функцией редактирования во внешнем редакторе. Тем не менее иногда его использовать проще, в силу того, что он встроен в расширение и легко запускается, при этом можно сохранить код (Ctrl+s) и тут же пользоваться им. Поэтому было бы неплохо иметь возможность дополнить его каким-то своими элементами также как мы можем это делать в главном окне браузера. Но как получить доступ к DOM другого окна?

Для решения этой задачи мы создадим наблюдателя, который будет следить за появлением и сокрытием XUL-окон xul-window-visible. Кроме того нам нужно будет найти открытые окна, для этого понадобится nsIWindowMediator, пример перебора окна можно посмотреть здесь. Для того, чтобы найти именно то окно, которое надо, необходимо знать о нем что-то что можно будет проверить у всех окон при переборе. Я уже сделал предварительную работу и код окна получил, выглядит он так
Кликните здесь для просмотра всего текста
Код xml Выделить

<?xml version="1.0" encoding="utf-8" ?>

<?xml-stylesheet href="chrome://global/skin/"?>

<?xml-stylesheet href="chrome://custombuttons/content/codeeditor.css" type="text/css"?>

<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="custombuttonsEditor" title="Редактирование кнопки: modal dialog example" persist="width height screenX screenY sizemode" buttons="extra1,extra2,accept,cancel" buttonlabelextra1="Внешний редактор…" buttonlabelextra2="Сохранить" ondialogextra1="edit_button()" ondialogextra2="editor.updateButton()" buttonaccesskeyextra1="E" onload="editor.init();" ondialogaccept="return editor.onAccept();" ondialogcancel="return editor.onCancel();" onbeforeunload="editor.destroy()" onunload="editor.destroy();" screenX="0" screenY="0" width="450" height="450" sizemode="maximized" chromehidden="menubar toolbar location directories status extrachrome " defaultButton="accept">

  <script type="application/x-javascript" src="editor2.js"/>

  <script type="application/x-javascript" src="editExternal.js"/>

  <commandset id="custombuttonsEditorCommandSet">

    <command id="cbUpdateButtonCommand" oncommand="editor.updateButton()"/>

    <command id="cbExecuteCode" oncommand="editor.execute_oncommand_code()"/>

    <command id="cbFullScreen" oncommand="editor.fullScreen()"/>

    <command id="cbCloseDialog" oncommand="editor.acceptDialog()"/>

  </commandset>

  <keyset id="custombuttonsEditorKeySet">

    <key id="cbUpdateButtonKey" command="cbUpdateButtonCommand" key="s" modifiers="control"/>

    <key id="cbExecuteCode" command="cbExecuteCode" keycode="VK_F9"/>

    <key id="cbFullScreen" command="cbFullScreen" keycode="VK_F11"/>

    <key id="cbCloseDialog" command="cbCloseDialog" keycode="VK_RETURN" modifiers="control"/>

  </keyset>

  <grid id="urlfield">

    <columns>

      <column/>

      <column flex="1"/>

    </columns>

    <rows>

      <row align="center">

        <label value="URL кнопки:" accesskey="U" control="urlfield-textbox"/>

        <textbox id="urlfield-textbox" flex="1"/>

      </row>

    </rows>

  </grid>

  <grid>

    <columns>

      <column/>

      <column flex="1"/>

    </columns>

    <rows>

      <row align="center">

        <label value="Имя:" tooltiptext="Задаёт надпись на кнопке и всплывающую подсказку" accesskey="N" control="name"/>

        <textbox id="name" flex="1" focused="true"/>

      </row>

      <row align="center">

        <label value="Изображение:" accesskey="m" control="image"/>

        <hbox>

          <menulist id="image" class="menulist-iconic" flex="1" onselect="editor.imageChanged()" sizetopopup="pref" value="custombuttons-stdicon-1" src="">

            <menupopup>

              <menuitem value="custombuttons-stdicon-1"/>

              <menuitem value="custombuttons-stdicon-2"/>

              <menuitem value="custombuttons-stdicon-3"/>

              <menuitem value="custombuttons-stdicon-4"/>

            </menupopup>

          </menulist>

          <button label="Обзор…" accesskey="o" oncommand="editor.selectImage();"/>

          <button label="в‡’ base64" accesskey="b" oncommand="editor.convert_image();" tooltiptext="Преобразовать изображение в формат base64 (data:image/png;base64,...)"/>

        </hbox>

      </row>

    </rows>

  </grid>

  <tabbox flex="1" id="custombuttons-editbutton-tabbox" handleCtrlTab="true" handleCtrlPageUpDown="true" persist="selectedIndex" selectedIndex="0">

    <tabs oncommand="editor.tabSelect(event);" orient="horizontal" value="">

      <tab id="code-tab" accesskey="C" cbcontrol="code" label="Код" tooltiptext="Выполняется при нажатии на кнопку" first-tab="true" selected="true" visuallyselected="true"/>

      <tab id="init-tab" label="Инициализация" accesskey="I" cbcontrol="initCode" tooltiptext="Выполняется при открытии окна приложения" afterselected="true"/>

      <tab label="Справка" accesskey="H" cbcontrol="help" tooltiptext="Справка"/>

      <tab label="Настройки кнопки" accesskey="s" tooltiptext="Панель настроек кнопки" last-tab="true"/>

    </tabs>

    <tabpanels flex="1" selectedIndex="0">

      <cbeditor class="custombuttons-editor-codeBox" id="code" multiline="true" flex="1" onclick="gmon_edit_mouseclick(event);" value="/*CODE*/"/>

      <cbeditor class="custombuttons-editor-codeBox" id="initCode" multiline="true" flex="1" onclick="gmon_edit_mouseclick(event);" value="/*Initialization Code*/"/>

      <cbeditor class="custombuttons-editor-codeBox" id="help" multiline="true" flex="1" onclick="gmon_edit_mouseclick(event);" value=""/>

      <vbox>

        <groupbox flex="1">

          <caption label="&quot;Горячая&quot; клавиша"/>

          <grid>

            <columns>

              <column/>

              <column/>

            </columns>

            <rows>

              <row align="center">

                <label value="Сочетание клавиш:"/>

                <textbox id="accelkey" flex="1"/>

              </row>

            </rows>

          </grid>

          <checkbox id="disableDefaultKeyBehavior" label="Запретить стандартное действие для указанного сочетания клавиш"/>

        </groupbox>

      </vbox>

    </tabpanels>

  </tabbox>

</dialog>

 

Описывать процесс получения кода не буду, поскольку сериализации уже уделял внимание, так что при переборе окон выполнить эту работу несложно. Здесь мы можем видеть, что id окна имеет значение custombuttonsEditor, вот этим обстоятельством и воспользуемся для идентификации окна при переборе открытых окон. Итак код кнопки
Кликните здесь для просмотра всего текста
Код javascript Выделить

var topicV = "xul-window-visible";

var observer =

    {

        observe: function (subject, topic, data)

        {

            var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]

                              .getService(Components.interfaces.nsIWindowMediator);

            var enumerator = wm.getEnumerator(null);

            while (enumerator.hasMoreElements())

            {

                var win = enumerator.getNext();

                if (win.document.documentElement.getAttribute("id") == "custombuttonsEditor")

                {

                    var btn = win.document.querySelector("#myButton");

                    if (!btn)

                    {

                        btn = win.document.createElement("button");

                        win.document.documentElement.appendChild(btn);

                        btn.setAttribute("label", "Моя кнопка");

                        btn.id = "myButton";

                        btn.onclick = function ()

                        {

                            win.alert("Та вообще все мое.");

                        }

                    }

                }

            }

 

            this.unregister()

        },

        register: function ()

        {

            var observerService = Components.classes["@mozilla.org/observer-service;1"]

                                  .getService(Components.interfaces.nsIObserverService);

            observerService.addObserver(this, topicV, false);

        },

        unregister: function ()

        {

            var observerService = Components.classes["@mozilla.org/observer-service;1"]

                                    .getService(Components.interfaces.nsIObserverService);

            observerService.removeObserver(this, topicV);

 

        }

    }

observer.register();

 

Здесь мы регистрируем наблюдателя, а в коде наблюдения сначала проверяем наличие созданного нами элемента (для примера там просто кнопка под полем для редактирования кода, но может быть что угодно и в любом месте, ну или почти в любом), если его нет, то он создается и добавляется. Эта проверка нужна в связи с тем, что метод будет срабатывать всякий раз когда окно переходит на первый план, разворачивается из панели задач и так далее. Соответственно надо позаботиться о том, чтобы дополнительные элементы появлялись только один раз.

Воспользуемся встроенным в браузер редактором JavaScript



Редактор JavaScript Scratchpad - достаточно неплохой, с точки зрения функциональности, редактор. В нем есть подсветка синтаксиса, автозавершение, всплывающие справочные сведения, возможность немедленного выполнения кода, а так же(что особенно важно для рассматриваемой темы) возможность выполнения chrome-кода в контексте браузера. Кроме того можно загружать, сохранять код и еще куча мелких приятностей. В этом смысле он намного лучше встроенного редактора CustomButtons, а потому "оседлать" этот редактор было бы совсем недурственно.

Редактор можно открыть в главном меню браузера Веб-разработка > Простой редактор JavaScript или сочетанием клавиш Shift+F4. Следует заметить, что по-умолчанию запуск кода в контексте браузера недоступен, а для того, чтобы сделать его доступным надо открыть инструменты разработки Веб-разработка > Инструменты разработки (или Ctrl+Shift+I), в верхнем правом углу панели разработки клацнуть иконку с шестеренкой, чтобы открыть настройки инструментов, в разделе Дополнительные параметры настроек надо установить флажок на пункте Включить инструменты отладки browser chrome и дополнений. Если после этого вызвать редактор, то в его главном меню появится меню Окружение, в котором можно будет выбрать контекст исполнения кода. Функции автозавершения при выборе режима Браузер, к сожалению, останутся такими как и были и подсказки будут выдаваться такие же, но выполняться будет chrome-код в контексте окна браузера. Более подробное описание редактора можно найти здесь
Черновик - Инструменты разработчика Firefox | MDN

Теперь начнем подбираться к самому редактору. Сначала мы скопируем код пункта меню, вызывающего редактор.
Код javascript Выделить

gClipboard.write(new XMLSerializer().serializeToString(document.querySelector("menuitem[label^='Простой редактор']")));

 

Нас в этом элементе больше всего интересует атрибут oncommand, в котором содержится код, вызывающий редактор
Код javascript Выделить
Scratchpad.openScratchpad();
Если выполнить этот код с помощью кнопки (или самого редактора) - эффект будет тем же. Но тут больше интересно другое: если мы посмотрим что возвращает эта функция, то можно будет убедиться, что возвращает она объект ChromeWindow. Это как раз и есть окно редактора, который был вызван с помощью этой функции, стало быть для того, чтобы отловить это окно нам не понадобятся никакие наблюдатели. В коде окна наиболее интересным элементом является следующий фрагмент
Код xml Выделить

<script type="application/javascript" src="chrome://devtools/content/scratchpad/scratchpad.js"/>

 

Это скрипт, в котором заключен основной функционал редактора. Просмотреть код скрипта можно, как обычно, введя в адресную строку браузера view-source:chrome://devtools/content/scratchpad/scratchpad.js, можно после этого сохранить скрипт и изучить его в более подходящем для анализа кода инструменте чем окно браузера.

Как можно запустить окно редактора и получить ссылку на объект этого окна - мы выяснили, но это не совсем то, что нам надо. Если просмотреть код функции, вызывающей редактор, ну например так
Код javascript Выделить
alert(Scratchpad.openScratchpad);
мы получим такой код
Код javascript Выделить

function SP_openScratchpad()

{

    return this.ScratchpadManager.openScratchpad();

}

 

Теперь мы выяснили, что есть объект ScratchpadManager и у него тоже есть функция openScratchpad
Код javascript Выделить

function SPM_openScratchpad(aState)

{

    "use strict";

 

    let params = Cc["@mozilla.org/embedcomp/dialogparam;1"]

                 .createInstance(Ci.nsIDialogParamBlock);

 

    params.SetNumberStrings(2);

    params.SetString(0, this.createUid());

 

    if (aState)

    {

        if (typeof aState != 'object')

        {

            return;

        }

 

        params.SetString(1, JSON.stringify(aState));

    }

 

    let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",

                                     SCRATCHPAD_WINDOW_FEATURES, params);

 

    // Only add the shutdown observer if we've opened a scratchpad window.

    ShutdownObserver.init();

 

    return win;

}

 

Но эта функция уже принимает аргумент aState. Дальнейшее изучение исходников позволило мне выяснить, что объект aState может иметь четыре параметра: text, filename, executionContext и saved.
text - это код, который будет находиться в открытом окне для редактирования.

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

saved - если он имеет значение true, значит редактор можно будет закрыть, ничего не меняя, и при этом он не будет запрашивать сохранение. В случае, если параметр не задан, имеет значение false или если код был изменен, но не сохранен, при закрытии будет появляться окно с запросом на сохранение.

executionContext - по умолчанию имеет значение 1 и это означает, что исполнение кода будет производиться в контексте документа, если задать значение 2, код будет выполняться в контексте браузера.

Таким образом мы имеем следующий код запуска редактора
Код javascript Выделить

var aState = { text: "/* Здесь будет код кнопки, которую мы собираемся редактировать. */", executionContext: 2, saved: true, };

var win = Scratchpad.ScratchpadManager.openScratchpad(aState);

 

Получение и изменение кода кнопки



Для того, чтобы понять, где расположен код кнопки, надо ее более внимательно изучить. У нас есть два инструмента для сериализации кнопки: XMLSerializer и JSON. Первый позволяет получить XML-код кнопки, с помощью второго можно сериализовать кнопку как JavaScript-объект. Изучив все это "хозяйство" можно прийти к трем способам получения кода кнопки.
Код javascript Выделить

code = this.cbCommand;

Код javascript Выделить

code = this.parameters.code;

Код javascript Выделить

code = this.getAttribute("cb-oncommand");

 

С кодом инициализации - такая же история
Код javascript Выделить

init = this.cbInitCode;

 

Код javascript Выделить

init = this.paraneters.initCode;

 

Код javascript Выделить

init = this.getAttribute("cb-init");

 

Естественно вместо this придется использовать ссылку на ту кнопку, код которой надо получить, а эти примеры получают код текущей кнопки, в которой они записаны.

С сохранением, к сожалению все не так просто. Точнее можно задать атрибуту cb-oncommand значение в виде кода, и это даже будет работать, но при открытии окна редактирования эти изменения там не отобразятся, а при перезапуске браузера и вовсе будут потеряны. Изучение исходников CustomButtons привело меня к следующему коду
Код javascript Выделить

this.setAttribute("cb-oncommand", this.cbCommand + "\n\n/* Добавленный код */\n\nalert('Текст сообщения добавленного кода');");

this.name = "Новое название";

custombuttons.cbService.parseButtonURI(this);

var link = "custombutton://buttons/Firefox/update/" + this.id;

custombuttons.cbService.updateButton(link, this.URI);

 

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

Привязка редактора к кнопке



Привязка, это, конечно, громко сказано, но всеми предварительными знаниями для использования Scratchpad'а для редактирования кнопки мы уже обладаем, осталось только собрать все воедино.

Вызов редактора лучше всего осуществлять через контекстное меню. Если мы посмотрим код любой кнопки, то там есть атрибут context, его значение(custombuttons-contextpopup) - это и есть id контекстного меню кнопок. Туда мы и будем добавлять элементы.

Создадим оверлей следующего содержания
Кликните здесь для просмотра всего текста
Код 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[

function cbCtxMenu_editWithScratchpad(mitem)

{

    var srcObj = document.querySelector("#custombuttons-contextpopup").triggerNode;

    var code, codeType;

    switch (mitem.getAttribute("id"))

    {

        case "editCommandWIthScratchpad":

            code = srcObj.getAttribute("cb-oncommand");

            codeType = "command";

            break;

        case "editInitWIthScratchpad":

            code = srcObj.getAttribute("cb-init")

            codeType = "init";

            break;

        default:

 

    }

 

    var aState = { text: code, executionContext: 2, saved: true };

    var win = Scratchpad.ScratchpadManager.openScratchpad(aState);

    var spObserver =

    {

        onReady: function (sp)

        {

            sp._initialWindowTitle = "Редактирование кода " + (codeType == "command" ? "команды" : "инициализации") + " кнопки: " + srcObj.name;

            sp._updateTitle();

            sp.removeObserver(spObserver);

        }

    }

    win.onload = function (evt)

    {

        win.Scratchpad.addObserver(spObserver);

        var toolbar = win.document.querySelector("#sp-toolbar");

        var saveBtn = win.document.createElement("toolbarbutton");

        saveBtn.setAttribute("label", "Сохранить код кнопки");

        toolbar.appendChild(saveBtn);

        saveBtn.onclick = function ()

        {

            srcObj.setAttribute(codeType == "command" ? "cb-oncommand" : "cb-init", win.Scratchpad.getText());

            custombuttons.cbService.parseButtonURI(srcObj)

            var link = "custombutton://buttons/Firefox/update/" + srcObj.id;

            custombuttons.cbService.updateButton(link, srcObj.URI);

            win.Scratchpad.dirty = false;

        }

    }

}

    ]]>

  </script>

  <menupopup id="custombuttons-contextpopup">

    <menu label="Scratchpad" id="scratchpadMenu"  position="1">

      <menupopup>

        <menuitem label="Редактировать код команды" id="editCommandWIthScratchpad" onclick="cbCtxMenu_editWithScratchpad(this);"/>

        <menuitem label="Редактировать код инициализации" id="editInitWIthScratchpad" onclick="cbCtxMenu_editWithScratchpad(this);"/>

      </menupopup>

    </menu>

  </menupopup>

</overlay>

 

Теперь осталось его только загрузить и в контекстном меню появится подменю Scratchpad с элементами меню для редактирования кода команды и кода инициализации. В тулбар самого редактора я добавил кнопку "Сохранить код кнопки", при ее нажатии появляется окошко подтверждения сохранения изменений, после подтверждения код кнопки будет изменен.

Несколько пояснений по коду. В общем и целом код достаточно прост, а большинство мест, которые могли бы вызывать вопросы, я уже объяснил ранее. Самым непонятным здесь, по всей видимости, является использование объекта Scratchpad. В контексте окна редактора это не такой же самый объект, что и одноименный объект в контексте главного окна. Полный код этого объекта можно посмотреть, перейдя в файрфоксе по адресу
Цитата:
view-source:chrome://devtools/content/scratchpad/scratchpad.js
Там есть коментарии к коду, с помощью которых несложно разобраться что к чему. Я здесь использовал свойство dirty, которое имеет значение противоположное параметру saved, задаваемому при вызове окна, то есть значение false задается для того, чтобы при закрытии окна не появлялся запрос на сохранение, поскольку код сохранен не в файл, а в кнопку. Кроме того, для установки заголовка окна мне понадобилось создать наблюдателя с помощью метода addObserver. Простое назначение атрибута title здесь не поможет, поскольку заголовок окна формируется где-то в коде и если назначить свой заголовок, то после этого он будет заменен стандартным. Наблюдатели же вызываются уже после того, как будет выполнен весь стандартный код, поэтому в них можно описывать те действия, которые должны выполняться после инициализации интерфейса редактора.

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

Редактирование букмарклета с помощью встроенного редактора



В принципе применить описанное ранее к букмарклетам - совсем несложно, тем не менее небольшой пример не помешает. Я взял оверлей, использовавшийся ранее для копирования закладки как BB-код, добавил в него модифицированный код из предыдущего примера, а также фильтр по протоколам. Для фильтрации я использовал дополнительный атрибут data-protocols, в его значение можно заносить через запятую список протоколов адресов, для которых элемент будет виден. Формат значения такой: "http:,https:,javascript:" . Элементы, для которых задан этот атрибут будут видны только для соответствующих закладок, остальные - будут видны везде.
Кликните здесь для просмотра всего текста
Код 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[

function copyAsBB(src)

{

    var node = src.parentElement.triggerNode;

    var pnode = node._placesNode;

    gClipboard.write("[URL=\"" + pnode.uri + "\"]" + pnode.title + "[/URL]");

}

 

function placesCtxMenu_editWithScratchpad(placesNode)

{

    var code = decodeURIComponent(placesNode.uri.substring("javascript:".length));

    var aState = { text: code, saved: true };

    var win = Scratchpad.ScratchpadManager.openScratchpad(aState);

    var spObserver =

    {

        onReady: function (sp)

        {

            sp._initialWindowTitle = "Редактирование кода букмарклета: " + placesNode.title;

            sp._updateTitle();

            sp.removeObserver(spObserver);

        }

    }

    win.onload = function (evt)

    {

        win.Scratchpad.addObserver(spObserver);

        var toolbar = win.document.querySelector("#sp-toolbar");

        var saveBtn = win.document.createElement("toolbarbutton");

        saveBtn.setAttribute("label", "Сохранить код букмарклета");

        toolbar.appendChild(saveBtn);

        saveBtn.onclick = function ()

        {

            var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]

                            .getService(Components.interfaces.nsINavBookmarksService);

            var ios = Components.classes["@mozilla.org/network/io-service;1"]

                          .getService(Components.interfaces.nsIIOService);

            var newUri = ios.newURI("javascript:" + encodeURIComponent(win.Scratchpad.getText()), null, null);

            bmsvc.changeBookmarkURI(placesNode.itemId, newUri);

            win.Scratchpad.dirty = false;

        }

    }

}

 

 

function editBookmarklet(src)

{

    var pn = document.querySelector("#placesContext").triggerNode._placesNode;

    placesCtxMenu_editWithScratchpad(pn);

}

 

 

function placesContext_onpopupshowing(src)

{

    var pn = src.triggerNode._placesNode;

    var mitems = src.querySelectorAll("menuitem");

    for(mi of mitems)

    {

        var dprotocol = mi.getAttribute("data-protocols");

        if (dprotocol && dprotocol.length > 0)

        {

            var protocols = dprotocol.split(",");

            var contains = (protocols.indexOf(pn.uri.split(":")[0] + ":") > -1);

            mi.setAttribute("hidden", !contains);

        } else mi.setAttribute("hidden", false);

    }

}

 

]]>

  </script>

  <menupopup id="placesContext" onpopupshowing="placesContext_onpopupshowing(this);" data-del="editBookmarkletWithScratchpad">

    <menuitem label="Копировать как BB-код" onclick="copyAsBB(this);" id="copyAsBB"/>

    <menuitem label="Редактировать букмарклет" id="editBookmarklet" onclick="editBookmarklet(this);" data-protocols="javascript:"/>

  </menupopup>

</overlay>

 


Как и в прошлый раз, на тулбаре редактора появится новая кнопка для сохранения результата(обновления кода закладки).

Использование внешнего редактора для редактирования кода.



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

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

Для реализации нашей задачи, код букмарклета мы просто будем сохранять во временный файл, далее откроем этот файл во внешнем приложении, а когда приложение сохранит файл, при переходе к окну браузера, мы прочитаем содержимое временного файла и обновим им код букмарклета. Нам нужно позаботиться о том, чтобы для букмарклета существовал режим редактирования, в котором и будет происходить отслеживание изменений. В примере ниже, я создал специальную панель, которая содержит сведения о редактируемой закладке (id и title), адрес временного файла, кнопку для поиска внешнего приложения и кнопку для запуска. Когда панель появляется, в систему уведомлений добавляется наблюдатель топика xul-window-visible, который будет обновлять код букмарклета кодом временного файла. При сокрытии панели - наблюдателя убираем из системы уведомлений. Панель вызывается из контекстного меню букмарклета, при этом получает все данные о нужной закладке, после запуска внешнего приложения и сохранения изменений можно переходить к окну браузера и клацать по закладке.
Кликните здесь для просмотра всего текста
Код 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(var old of document.querySelectorAll("[data-del = 'x01']")) old.parentElement.removeChild(old);

Components.utils.import("resource://gre/modules/FileUtils.jsm");

Components.utils.import("resource://gre/modules/NetUtil.jsm");

 

 

function placesContext_onpopupshowing(src)

{

    var pn = src.triggerNode._placesNode;

    var mitems = src.querySelectorAll("menuitem");

    for(mi of mitems)

    {

        var dprotocol = mi.getAttribute("data-protocols");

        if (dprotocol && dprotocol.length > 0)

        {

            var protocols = dprotocol.split(",");

            var contains = (protocols.indexOf(pn.uri.split(":")[0] + ":") > -1);

            mi.setAttribute("hidden", !contains);

        } else mi.setAttribute("hidden", false);

    }

}

 

function bookmarkletEditPanelSaveChanges()

{

    var panel = document.querySelector("#bookmarkletEditPanel");

    var file = Components.classes["@mozilla.org/file/local;1"].

           createInstance(Components.interfaces.nsILocalFile);

    file.initWithPath(panel.tempFile);

    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); // you can use another encoding here if you wish

    let str = {}

    {

        let read = 0;

        do

        {

            read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value

            data += str.value;

        } while (read != 0);

    }

    cstream.close(); // this closes fstream

    var uriTxt = "javascript:" + encodeURIComponent(data);

    var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]

                    .getService(Components.interfaces.nsINavBookmarksService);

    var ios = Components.classes["@mozilla.org/network/io-service;1"]

                  .getService(Components.interfaces.nsIIOService);

    var newUri = ios.newURI(uriTxt, null, null);

    bmsvc.changeBookmarkURI(panel.itemId, newUri);

}

 

 

 

function editBookmarkletWithExternalEditor()

{

    var panel = document.querySelector("#bookmarkletEditPanel");

    var idLabel = document.querySelector("#labelBookmarkId");

    var titleLabel = document.querySelector("#labelBookmarkTitle");

    var pathTextbox = document.querySelector("#textboxExternalEditorPath");

    var pn = document.querySelector("#placesContext").triggerNode._placesNode;

    idLabel.textContent = pn.itemId;

 

    var file = FileUtils.getFile("TmpD", ["bookmarkletForEdit.js"]);

    file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);

    var ostream = FileUtils.openSafeFileOutputStream(file);

    var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].

                    createInstance(Components.interfaces.nsIScriptableUnicodeConverter);

    converter.charset = "UTF-8";

    var istream = converter.convertToInputStream(decodeURIComponent(pn.uri.substring("javascript:".length)));

    NetUtil.asyncCopy(istream, ostream, function (status)

    {

        if (!Components.isSuccessCode(status))

        {

            // Handle error!

            return;

        }

    });

 

 

 

    panel.tempFile = file.path;

    panel.itemId = pn.itemId;

    idLabel.textContent = pn.itemId;

    titleLabel.textContent = pn.title;

    panel.openPopupAtScreen(200, 200, false)

}

 

function buttonExternalEditorFilePicker_click()

{

    const nsIFilePicker = Components.interfaces.nsIFilePicker;

    var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);

    fp.init(window, "Выбор внешнего редактора", nsIFilePicker.modeOpen);

    fp.appendFilter("Исполняемые файлы", "*.exe");

    var rv = fp.show();

    if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace)

    {

        var file = fp.file;

        var path = fp.file.path;

        document.querySelector("#textboxExternalEditorPath").value = path;

    }

}

 

function buttonRunExternalEditor_click()

{

    var file = Components.classes["@mozilla.org/file/local;1"]

                         .createInstance(Components.interfaces.nsILocalFile);

    file.initWithPath(document.querySelector("#textboxExternalEditorPath").value);

    var process = Components.classes["@mozilla.org/process/util;1"]

                        .createInstance(Components.interfaces.nsIProcess);

    process.init(file);

    var args = [document.querySelector("#bookmarkletEditPanel").tempFile];

    process.run(false, args, args.length);

}

 

var editBookmarkletObserver = {

    observe: function (subject, topic, data)

    {

        bookmarkletEditPanelSaveChanges();

    },

    register: function ()

    {

        var observerService = Components.classes["@mozilla.org/observer-service;1"]

                              .getService(Components.interfaces.nsIObserverService);

        observerService.addObserver(this, "xul-window-visible", false);

    },

    unregister: function ()

    {

        var observerService = Components.classes["@mozilla.org/observer-service;1"]

                                .getService(Components.interfaces.nsIObserverService);

        observerService.removeObserver(this, "xul-window-visible");

 

    }

};

  

  ]]>

  </script>

  <menupopup id="placesContext" onpopuphiding="placesContext_onpopupshowing();">

    <menuitem label="Редактировать букмарклет во внешнем редакторе" data-del="x01" data-protocols="javascript:" id="editWithExternalEditor" onclick="editBookmarkletWithExternalEditor();"/>

  </menupopup>

  <popupset id="mainPopupSet">

    <panel id="bookmarkletEditPanel" onpopupshowing="editBookmarkletObserver.register()" onpopuphiding="editBookmarkletObserver.unregister()" noautohide="true" titlebar="normal" data-del="x01" type="drag" backdrag="true" label="Редактирование букмарклета" close="true">

      <html:table xmlns:html="http://www.w3.org/1999/xhtml" border="1" cellpadding="0" cellspacing="0">

        <html:tr>

          <html:td>

            <label>ID</label>

          </html:td>

          <html:td>

            <label id="labelBookmarkId"/>

          </html:td>

        </html:tr>

        <html:tr>

          <html:td>

            <label>Название</label>

          </html:td>

          <html:td>

            <label id="labelBookmarkTitle"></label>

          </html:td>

        </html:tr>

      </html:table>

      <vbox>

        <hbox>

          <textbox id="textboxExternalEditorPath" width="300"/>

          <button label="Обзор..." id="buttonExternalEditorFilePicker" onclick="buttonExternalEditorFilePicker_click();"/>

        </hbox>

        <hbox>

          <button label="Запустить" id="buttonRunExternalEditor" onclick="buttonRunExternalEditor_click();"/>

        </hbox>

      </vbox>

    </panel>

  </popupset>

</overlay>

 


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

Комментариев нет :

Отправить комментарий