Написание Firefox дополнения в боевых условиях, или как писался 4PDA Инспектор

Написание Firefox дополнения в боевых условиях, или как писался 4PDA Инспектор

iSergium

Привет, читатель!

В этой статье мы разберем написание Firefox дополнения и затронем некоторые интересные темы на примере 4PDA-Инспектора. Суть дополнения в мониторинге форума 4pda.ru. Сразу скажу, что на некоторых неинтересных или слишком мелких вещах мы останавливаться не будем, а полный код расширения будет доступен в конце каждой части.

v.0.1. Начальные настройки и базовый функционал

Цель: парсинг количества избранных тем с новыми ответами и вывод информации на кнопку дополнения в панели навигации.

Парсинг – это синтаксический анализ сайтов, который автоматически производится парсером – специальной программой или скриптом. Характер парсинга определяется заданием получить определенную информацию со страниц сайта, параметры анализа заранее задаются.
© webeffector.ru

Создание ничего не делающего дополнения мы разобрали в прошлой статье (вот в этой), из него и эволюционировал 4PDA Инспектор. Первым делом были изменены значения в install.rdf и chrome.manifest. Пакет был назван "inspector" (впоследствии пришлось изменить это название на "4pdainspector", так как "inspector" использовался другим расширением, потому во избежание конфликтов настоятельно рекомендуется использовать уникальные названия), id дополнения стало 4pda_inspector@coddism.com. Также с помощью манифеста был добавлен стиль в customizeToolbar (настройка панелей инструментов):

style chrome://global/content/customizeToolbar.xul chrome://4pdainspector/content/css/main.css

Файл стилей main.css тоже довольно мал и содержит 2 стиля для одной кнопки разных размеров:

#inspector-button
{
   list-style-image: url("chrome://4pdainspector/content/icons/icon_22x_out.png");
}

toolbar[iconsize="small"]#inspector-button
{
    list-style-image: url("chrome://4pdainspector/content/icons/icon_22x_out.png");
}

Перейдем к overlay.xul. Первым делом избавляемся от мусора myfirstextension в виде панели в статус баре. Вторым делом мы добавляем кнопку на панель инструментов.

<toolbarpalette id="BrowserToolbarPalette">
<toolbarbutton

    id="inspector-button"
    label
="4PDA Инспектор"
    tooltiptext
="4PDA Инспектор"
    oncommand
='inspectorToolbar.buttonClick()'
    />
</toolbarpalette>

Здесь toolbarpalette id="BrowserToolbarPalette" – это и есть сама панель инструментов, внутрь неё мы и добавляем нашу кнопку toolbarbutton id="inspector-button". Параметр label — это заголовок кнопки, tooltiptext — всплывающий при наведении текст, oncommand — действие при клике.

Третьим делом укажем пути к js-файлам:

<script type='text/javascript' src='chrome://inspector/content/js/utils.js'></script>
<script type='text/javascript' src='chrome://inspector/content/js/contentscript.js'></script>
<script type='text/javascript' src='chrome://inspector/content/js/toolbar.js'></script>

Utils.js создан для хранения класса отладки, в нем всего одна функция:

var utils = {
  consoleService: null,
  log: function(msg) {
    if (this.consoleService == null) {
      this.consoleService = Components.classes["@mozilla.org/consoleservice;1"];
      this.consoleService = this.consoleService.getService(Components.interfaces.nsIConsoleService);
    }
    this.consoleService.logStringMessage(msg);
  }
};

С её помощью мы пишем в лог — всегда полезная функция. Для просмотра лога понадобится консоль ошибок Console² (скачать можно на addons.mozilla.org).

Полное описание компонента nsIConsoleService можно прочитать здесь.

Toolbar.js хранит объект inspectorToolbar, в котором находится функция клика по кнопке buttonClick().

var inspectorToolbar = {
  buttonClick: function()
  {
    var tBrowser = top.document.getElementById("content");
    // открыть новую вкладку со списком избранных тем
    var tab = tBrowser.addTab(inspectorContentScript.favUrl);
    // делаем вкладку выделенной
    tBrowser.selectedTab = tab;
  }
};

Здесь мы просто открываем новую вкладку со списком избранных тем, адрес которого хранится в inspectorContentScript.favUrl. Далее разберем основной скрипт дополнения — contentscript.js. Для начала полный его код:

var inspectorContentScript = {

  updateTimer: 0,
  winobj: null,
  favUrl: 'http://4pda.ru/forum/index.php?autocom=favtopics',

  init: function()
  {
    var obj = document.getElementById("navigator-toolbox");
    this.winobj = (obj)?window.document:window.opener.document;
    inspectorContentScript.getNewCount();    // Первый запрос

    this.updateTimer = setInterval(function() {  // Последующие запросы
       inspectorContentScript.getNewCount();     // каждые 5 сек
    }, 5000);
  },

  getNewCount: function() //запрос кода страницы
  {
    var req = new XMLHttpRequest();
    req.onreadystatechange = function()
    {
      if (req.readyState != 4) return;  // убедиться, что
      if (req.status == 200)            // запрос прошел успешно
      {
        if (req.responseText)           // и по нему что-то пришло
        {
          inspectorContentScript.printCount(inspectorContentScript.getFavCount(req.responseText));
          return;
        }
      }
      inspectorContentScript.printLogout();
    }

    req.onerror = function() {           // или что-то пошло не так
      inspectorContentScript.printLogout();
    }

    req.open("GET", inspectorContentScript.favUrl, true);
    req.send(null);
  },

  getFavCount: function(text) // парсинг количества непрочитанных избранных тем
  {
    if (!text)
      return 0;

    var regexp = /(http://s.4pda.ru/forum/style_images/1/newpost.gif)/ig;
    var favs = text.match(regexp);

    if (typeof favs == 'object' && favs != null)
      return favs.length;
      else
      return 0;
  },

  printCount: function(count, mesCount) // вывод количества на кнопку
  {
    var btn = this.winobj.getElementById('inspector-button');

    if (!btn) return false; // если нет кнопки, то зачем выводить количество?

    var canvas = document.getElementById("inspector_button_canvas");
    canvas.setAttribute("width", 24);
    canvas.setAttribute("height", 24);
    var ctx = canvas.getContext("2d"); // забираем канвас, задаем размеры и..

    var img = new Image();
    img.onload = function()    // ...и после загрузки изображения начинаем на нем рисовать
    {
      ctx.textBaseline = "top";
      ctx.font = "bold 9px tahoma";
      ctx.clearRect(0, 0, canvas.width, canvas.height);  // чистим весь канвас
      ctx.drawImage(img, 1, 0, img.width, img.height);  // перерисовываем в него изображение с отступом 1пк слева

      var w = ctx.measureText(count).width;  // ширина
      var h = 9;                             // и высота текста (выше задана)

      var x = canvas.width – w;         // координаты
      var y = canvas.height - h – 1;    // числа

      ctx.fillStyle = "#d62f2f";
      ctx.fillRect(x-1, y, w+2, h+5);    // прямоугольный фон для числа
      ctx.fillStyle = "#fff";
      ctx.fillText(count, x, y+1);       // и само число

      btn.image = canvas.toDataURL("image/png");  // и выход в свет
    };

    img.src = "chrome://inspector/content/icons/icon_22x.png";
    btn.setAttribute('tooltiptext', '4PDA - В сети');
  },

  printLogout: function() // вывод информации о неавторизованности
  {
    var btn = this.winobj.getElement ById('inspector-button');
    if (btn)
    {
      btn.image = "chrome://inspector/content/icons/icon_22x_out.png";
      btn.setAttribute('tooltiptext', '4PDA - Не в сети');
    }
  }
};

inspectorContentScript.init(); //запуск механизма

  1. init()
    запускаем таймер.
  2. getNewCount()
    Забираем содержимое страницы избранных тем. Запросы на удаленный сервер позволяет производить объект XMLHttpRequest. Присваиваем объекту функции req.onreadystatechange для удачного выполнения и req.onerror для обработки ошибки. Адрес, тип (в нашем случае GET и inspectorContentScript.favUrl ) запроса указываются в req.open, также в нем указываем асинхронность запроса третьим параметром true.
    При удачном запросе — readyState 4 (Interactive) и req.status 200 (HTTP/1.0 200 OK) — парсим из текста ответа req.responseText количество непрочитанных тем (getFavCount) и выводим количество на кнопке (printCount), в ином случае вызываем printLogout — делаем кнопку серой и устанавливаем всплывающий текст «4PDA - Не в сети».
  3. getFavCount()
    Темы с новыми ответами сопровождаются на форуме картинкой http://s.4pda.ru/forum/style_images/1/newpost.gif, следовательно, чтобы узнать количество необходимых нам тем, мы должны подсчитать количество этих картинок. Парсим результат запроса регуляркой /(http:\/\/s.4pda.ru\/forum\/style_images\/1\/newpost.gif)/ig и запрашиваем длину получившегося массива — одно из самых простых реализаций в данном дополнении
    Избранные темы 4pda
  4. printCount()
    Вывод полученной информации на кнопке. Почти вся работа здесь — это работа с html canvas. Именно для этого мы создавали скрытый html:canvas в overlay.xul. Задаем стиль текста, подстраиваем под него прямоугольную область и выводим её в правом нижнем углу. 24х24px - стандартные размеры иконки в Firefox для Linux, для остальных систем стандартным является 16х16px, универсализация будет через несколько версий.

Вот и всё, количество тем с новыми ответами высвечивается в панели — первая версия готова.

4pda инспектор v.0.1

Скачать 4pda_inspector v.0.1: zip, xpi

v.0.2.x Парсинг и вывод количества новых сообщений

Цель: парсинг количества новых сообщений и вывод его на кнопку дополнения в панели навигации.

Количество непрочитанных тем — это, конечно, хорошо, но количество новых сообщений — еще лучше.

Ссылка на сообщения

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

if (req.responseText)
{
    count = inspectorContentScript.getFavCount(req.responseText);
    mesCount = inspectorContentScript.getMesCount(req.responseText);
    if (count === false || mesCount === false)
        inspectorContentScript.printLogout();
    else
        inspectorContentScript.printCount(count, mesCount);
    return;
}

Напишем функцию getMesCount.

getMesCount: function(text)
{
    if (!text)
        return 0;
    ff = text.match(/<a href="http://4pda.ru/forum/index.php?act=Msg&amp;CODE=01">.*?: (d+?)</a>/);

    if (typeof (ff) == 'object' && ff != null && (typeof ff[1] != 'undefined'))
        return ff[1];
    else
        return false;
}

Ищем ссылку http://4pda.ru/forum/index.php?act=Msg&CODE=01 и берем в ней цифры после двоеточия. Если нет ссылки, то пользователь не авторизован и надо вызвать printLogout. Осталось добавить вывод количества сообщений на кнопке, для этого добавим несколько строк в функцию printCount:

var w = ctx.measureText(mesCount).width;
ctx.fillStyle = "#d62f2f";
ctx.fillRect(0, y, w+2, h+5);
ctx.fillStyle = "#fff";
ctx.fillText(mesCount, 1, y+1);

Всё, количество сообщений выводится в левом нижнем углу кнопки.

Инспектор v.0.2.1

Скачать 4pda_inspector v.0.2.1: zip, xpi

v.0.3.x Настраиваем настройки

Цель: сделать настройку интервала обновления.

Показ информации — это уже неплохо, но 5 секунд для обновления кому-то (примерно всем) может показаться слишком частым — необходимо дать пользователям выбор. Приступим.

Первым делом нужно задать настройки по умолчанию. Чтобы это сделать, нужно создать js-файл (его название, в принципе, не важно, лишь бы на латинице; обычно его называют defaults.js) в папке defaults/preferences. Создадим inspector.js и запишем в него настройку:

pref("extensions.4pda-inspector.interval", 5);

Принимаются настройки трех типов: int, string, bool. Настройки должны (но не обязаны) быть именованы как extensions.*уникальное_имя_дополнения*.*название_настройки*.

Далее нужно создать сам интерфейс окна настроек - создадим settings.xul. Название может быть любым, но опять же на латинице и иметь расширение xul =).

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://inspector/content/css/settings.css" type="text/css"?>
<prefwindow
  id="settings-window"
  buttons="accept,cancel"
  title="Настройки - 4PDA Инспектор"
  xmlns:html="http://www.w3.org/1999/xhtml"
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<prefpane id="inspector_pane" label="Telefum Settings">
  <preferences>
    <preference id="pref_interval" name="extensions.4pda-inspector.interval" type="int"/>
  </preferences>
  <hbox>
    <vbox>
      <image src='chrome://inspector/content/icons/icon_128.png' width='128' height='128' align="left"/>
    </vbox>
    <vbox class="inspector_allSettingsBox">
      <html:div class="inspector_allSettingsBoxHeading">Общие настройки:</html:div>
      <vbox class="inspector_settingsBox">
        <label value="Интервал обновления (сек.):"/>
        <textbox preference="pref_interval" id="inspector_intervalInput" type="number" min="1"/>
      </vbox>
    </vbox>
  </hbox>
</prefpane>
</prefwindow>
Окно настроек

В плане xul-вёрстки ничего сложного — логически она почти ничем не отличается от html, подробно о xul-элементах можно узнать в документации по этой ссылке. Остановлюсь лишь на привязке настроек к элементам. Как не сложно заметить, name совпадает с названием настройки.

<preference id="pref_interval" name="extensions.4pda-inspector.interval" type="int"/>

И устанавливаем настройку в элемент с помощью атрибута preference по указанному id:

<textbox preference="pref_interval" id="inspector_intervalInput" type="number" min="1"/>

Textbox - это текстовое поле наподобие <input type='text'>, установленный type='number' делает его числовым и добавляет updown, минимальное число устанавливаем в 1 — интервал обновления 0 секунд как-то неразумен.

Чтобы добавить ссылку (кнопку?) на настройки в списке управления дополнениями (открывается по ctrl+shift+a, адресу about:addons и через меню), необходимо добавить строчку в install.rdf:

<em:optionsURL>chrome://inspector/content/settings.xul</em:optionsURL> 

При применении настроек (в Windows и Linux нужно нажать на «ОК», в MacOS просто закрыть окно) настройки сохранятся без каких-либо дополнительных скриптов. Все настройки браузера и всех дополнений доступны по адресу about:config. Список дополняется текстовым полем для поиска, что заметно упрощает нахождение необходимых настроек.

about:config

Вывод и сохранение настроек - это круто, но их нужно как-нибудь задействовать, иначе зачем они? Создаем content/js/default.js, добавляем его в overlay.xul

<script type='text/javascript' src='chrome://inspector/content/js/default.js'></script>

В сам default.js пишем:

var inspectorDefaultStorage = {

  interval: 5000, //интервал по умолчанию, надо же чему-нибудь присвоить

  prefs: {}, //объект, куда запишутся все настройки

  getPrefs: function() //пишем настройки в prefs
  {
    this.prefs = Components.classes["@mozilla.org/preferences-service;1"]
      .getService(Components.interfaces.nsIPrefService)
      .getBranch("extensions.4pda-inspector.");
    this.prefs.QueryInterface(Components.interfaces.nsIPrefBranch);
    this.prefs.addObserver("", this, false);

    this.resetStorage();
  },

  resetStorage: function() //считать настройки
  {
    try {
      inspectorDefaultStorage.interval = inspectorDefaultStorage.prefs.getIntPref("interval") * 1000;
    } catch (e){}
  }
}

inspectorDefaultStorage.getPrefs();

То есть, храним в объекте inspectorDefaultStorage все настройки и методы их добывания. Значение умножаем на 1000, т. к. интервал в setTimeout задается в миллисекундах.

Осталось изменить метод newIteration() в объекте inspectorContentScript:

newIteration: function()
{
  inspectorDefaultStorage.getPrefs();  // взять настройки
  this.updateTimer = setTimeout(function() {
    inspectorContentScript.getNewCount();
  }, inspectorDefaultStorage.interval);  // применить настройки
}

На том и закончим.

Скачать 4pda_inspector v.0.3.0.2: zip, xpi

v.0.4.x Всплываем и показываем

Как бы то ни было, но в рамках 24x24 px (и, тем более, в 16х16 px) много информации не покажешь. Оповещение о новых ответах в темах это хорошо, но удобно же было бы сразу просмотреть список тем и при желании перейти на какие-нибудь из них. Или, кликнув на кнопку, открыть сразу все эти темы в новых вкладках. Это и есть задача для версии 0.4.x.

Добавим всплывающую панель добавив в overflow.xul её верстку:

<toolbox id="navigator-toolbox">
  <panel type="arrow" id="inspectorPanel" align="left" onpopuphiding="inspectorToolbar.hidePanel()">
  <vbox>
    <vbox id="inspectorPanel_themesList">
    </vbox>

    <vbox id="inspector_middleVBox">
      <label value="Перейти к списку избранных тем" id="inspector_goToFavs"/>
      <label value="Открыть все непрочитанные темы в новых вкладках" id="inspector_openAllFavs"/>
    </vbox>

    <hbox id="inspector_messagesHBox">
      <label value="Новых сообщений: "/>
      <label value="0" id="inspector_unreadMessageCount" />
    </hbox>
    <hbox class="inspector_messagesHBox">
      <label value="Настройки" id="inspector_openSettings" />
    </hbox>
  </vbox>
  </panel>
</toolbox>

Дадим этой панели id "inspectorPanel" и событие по скрытию панели (onpopuphiding) inspectorToolbar.hadePanel(). Элемент vbox id="inspectorPanel_themesList" создан для списка тем. О назначении остальных элементов можно узнать из их заголовков.

Подробнее об элементе panel можно узнать здесь.

Далее toolbar.js. Он изменился полностью, если не считать названия.

var inspectorToolbar = {

  winobj: null,
  panel: null,
  list: null,
  unreadThemes: [],

  link_favTopics: 'http://4pda.ru/forum/index.php?autocom=favtopics',
  link_messages: 'http://4pda.ru/forum/index.php?act=Msg&CODE=01',

  init: function()
  {
    var obj = document.getElementById("navigator-toolbox");
    this.winobj = (obj)?window.document:window.opener.document;
    this.panel = this.winobj.getElementById('inspectorPanel');

    // «Перейти к списку избранных тем»
    this.winobj.getElementById('inspector_goToFavs').addEventListener('click', function(){
      inspectorToolbar.openPage(inspectorToolbar.link_favTopics);
      inspectorToolbar.handleHidePanel();
    });

    // «Открыть все непрочитанные темы в новых вкладках»
    this.winobj.getElementById('inspector_openAllFavs').addEventListener('click', function(){
      if (inspectorToolbar.openAll())
        inspectorToolbar.handleHidePanel();
    });

    // «Открыть все непрочитанные темы в новых вкладках»
    this.winobj.getElementById('inspector_messagesHBox').addEventListener('click', function(){
      inspectorToolbar.openPage(inspectorToolbar.link_messages);
      inspectorToolbar.handleHidePanel();
    });
    // Открыть окно с настройками
    this.winobj.getElementById('inspector_openSettings').addEventListener('click', function(){
      inspectorToolbar.handleHidePanel();
      window.openDialog('chrome://inspector/content/settings.xul', 'inspectorSettingWindow', 'chrome, centerscreen, dependent, dialog, titlebar, modal', inspectorContentScript);
    });
  },

  // клик по кнопке в панели инструментов
  buttonClick: function(parent)
  {
    switch (inspectorDefaultStorage.click_action)  // проверка настройки действия при клике
    {
      case 1:                // показать панель
        inspectorToolbar.showPanel(parent);
        break;
      case 2:                // открыть страницу с избранными темами
        inspectorToolbar.openPage(inspectorToolbar.link_favTopics);
        break;
      case 3:                // открыть все непрочитанные  
        inspectorToolbar.parseThemes();
        inspectorToolbar.openAll();
        break;
      default:                // wtf?
        alert(inspectorDefaultStorage.click_action + ' is uncorrect value');
    }
  },

  // сбор информации по непрочитанным темам
  parseThemes: function()
  {
    inspectorToolbar.unreadThemes = [];
    if (!inspectorContentScript.lastResponseText)
      return false;
    var themes = inspectorContentScript.lastResponseText.match(/\<a href\=[\"\']([\w\=\&\?\.\/\;\:]+)[\"\']\>\<img.+?src=[\"\']http\:\/\/s\.4pda.ru\/forum\/style_images\/1\/newpost\.gif[\"\'].+?\>\<\/a\>.*?\<a.*?href=[\"\']http\:\/\/4pda\.ru\/forum\/index\.php\?showtopic\=[0-9]+[\"\'].*?\>(.+?)\<\/a\>.*?/ig);

    if (themes)
    {
      for (var i = 0; i<themes.length; i++)
      {
        theme = themes[i].match(/\<a.+href\=\".*?(\d+)\".*?\>(.+)?\<\/a\>/i)
        if (theme)
        {
          inspectorToolbar.unreadThemes.push({
            id: theme[1],
            caption: theme[2]
          });
        }
      };
    }
  },

  // показ всплывающей панели
  showPanel: function(parent)
  {
    if (!this.panel)
      this.init();

    if (this.panel)
    {
      inspectorToolbar.parseThemes();

      // блок для списка тем
      this.list = this.winobj.getElementById('inspectorPanel_themesList');

      if (inspectorToolbar.unreadThemes.length)
      {
        for (var i = 0; i<inspectorToolbar.unreadThemes.length; i++)
        {
          // создаётся надпись, задается текст - название темы, вешается клик - открытие темы и удаление надписи
          var newElem = document.createElement('label');
          newElem.setAttribute('value', '>> '+inspectorToolbar.unreadThemes[i].caption);
          newElem.setAttribute('data-theme', inspectorToolbar.unreadThemes[i].id);
          newElem.addEventListener('click', function(){
            inspectorToolbar.openTheme(this.getAttribute('data-theme'));
            (this).parentNode.removeChild(this);
          });
          this.list.appendChild(newElem);
        };
        this.winobj.getElementById('inspector_openAllFavs').disabled = false;
      }
      else
      // если непрочитанных тем нет, то "открыть все темы..." придется сделать неактивной
      this.winobj.getElementById('inspector_openAllFavs').disabled = true;

      // вывод количества непрочитанных сообщений
      this.winobj.getElementById('inspector_unreadMessageCount').value = inspectorContentScript.unreadMessageCount;

      // показать панель
      this.panel.openPopup(parent, 'after_start', 0, 0, false, true);
    }
  },

  // скрыть панель
  handleHidePanel: function()
  {
    if (this.panel)
      this.panel.hidePopup();
  },

  // выполняется при скрытии панели
  hidePanel: function()
  {
    this.list = this.winobj.getElementById('inspectorPanel_themesList');
    var labels = this.list.getElementsByTagName('label');

    for (var i = labels.length - 1; i >= 0; i--) {
      this.list.removeChild(labels[i]);
    };
  },

  // открыть страницу
  openPage: function(page)
  {
    var tBrowser = top.document.getElementById("content");
    var tab = tBrowser.addTab(page);
    tBrowser.selectedTab = tab;
  },

  // открыть тему (исключительно для удобства)
  openTheme: function(id)
  {
    if (id)
      inspectorToolbar.openPage('http://4pda.ru/forum/index.php?showtopic='+id+'&view=getnewpost');
  },

  // открыть все непрочитанные темы в новых вкладках
  openAll: function ()
  {
    if (!inspectorToolbar.unreadThemes.length)
      return false;

    for (var i = 0; i < inspectorToolbar.unreadThemes.length; i++)
    {
      inspectorToolbar.openTheme(inspectorToolbar.unreadThemes[i].id);
    };

    return true;
  }

};

Как можно заметить, buttonClick() стал учитывать настройку click_action — это действие при нажатии на кнопку, по умолчанию включено 1 — показ всплывающей панели. К этим настройкам вернемся позже, сейчас на очереди показ панели — функция showPanel.

Если панели нет — мы её инициализируем (this.init()) - привязываем к переменной panel панель из overlay.xul и привязываем клики к ссылкам. Далее parseThemes() - забираем со страницы избранных тем id и названия тем и выводим их, привязывая к ним клик — открытие этой темы в новой вкладке. Далее выводим количество непрочитанных сообщений и показываем панель - this.panel.openPopup(parent, 'after_start', 0, 0, false, true);

Чтобы прояснить эту команду: openPopup(anchor , position , x , y , isContextMenu, attributesOverride, triggerEvent ), полное описание здесь.

А теперь вернемся к настройке клика по кнопке. Во-первых, /defaults/preferences/inspector.js:

pref("extensions.4pda-inspector.click_action", 1);

Во-вторых, settings.xul:

<vbox class="inspector_settingsBox last_settingsBox">
 <label value="Действие при нажатии на кнопку:"/>
 <radiogroup preference="pref_click_action">
  <radio value="1" label="Всплывающее окно"/>
  <radio value="2" label="Перейти к списку избранных тем"/>
  <radio value="3" label="Открыть все непрочитанные темы в новых вкладках"/>
 </radiogroup>
</vbox>

И добавляем стандартный стиль, без него, например, radio будут иметь иконку только в MacOS.

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

О чем еще стоит упомянуть — более детальный парсинг непрочитанных тем — необходимо взять их id и название.

Показателем темы с непрочитанными сообщениями является img src='http://s.4pda.ru/forum/style_images/1/newpost.gif', которое находится внутри ссылки. Заголовок темы находится в следующей ближайшей ссылке на тему (в приведенном примере это http://4pda.ru/forum/index.php?showtopic=374598). Ввиду небольшой ограниченности регулярок в JavaScript придется брать весь этот отрезок html кода и парсить полученные результаты еще раз.

  1. От a до /a.
    /\<a href\=[\"\']([\w\=\&\?\.\/\;\:]+)[\"\']\>\<img.+?src=[\"\']http\:\/\/s\.4pda.ru\/forum\/style_images\/1\/newpost\.gif[\"\'].+?\>\<\/a\>.*?\<a.*?href=[\"\']http\:\/\/4pda\.ru\/forum\/index\.php\?showtopic\=[0-9]+[\"\'].*?\>(.+?)\<\/a\>.*?/ig
  2. При успешном первом парсинге получим массив из кусков кода — изображение с ссылкой, промежуточный код и необходимая нам ссылка с id и заголовком. Её мы и забираем с помощью /\<a.+href\=\".*?(\d+)\".*?\>(.+)?\<\/a\>/i. Первое выделенное скобками числовое значение будет id темы, второе — заголовком. Остается только успевать записывать их в свой массив inspectorToolbar.unreadThemes.

Весь этот процесс происходит в функции parseThemes().

всплывающая панель

Всплывает, показывает - готово.

Скачать 4pda_inspector v.0.4.1.8: zip, xpi

v.0.5.x Больше кастомизаций, локализаций и универсализаций

Основными изменениями этой версии стали кастомизация кнопки, добавление парсинга qms-сообщений, локализация и несколько прочих мелочей

1. Начнем с самого глобального из этого списка — локализации.

Локализация — это такой способ для перевода дополнений. Суть в том, что вместо строки в xul и js файлы пишется не сама строка, а текстовая константа, значение которой берется в зависимости от текущего языка. Файлы с переводами хранятся в папке /locale. Внутри папки locale находятся промежуточные папки, которые группируют переводы по языкам, например, папки en-US и ru-RU (названия могут быть и другими) для английской и русской локализации. В каждой из этих папок должны находиться файлы с одинаковым названием, их расширение должно быть dtd.

Пути к файлам с указанием языка прописываются в chrome.manifest:

locale inspector ru-RU locale/ru-RU/

Сами файлы dtd имеют следующий вид (отрывок из /locale/ru-RU/overlay.dtd)

<!ENTITY toolbarbutton_title "4PDA Инспектор">
<!ENTITY toolbarbutton_tooltiptext "4PDA Инспектор">
<!ENTITY inspector_goToFavs "Перейти к списку избранных тем">

К xul-файлам библиотека подключается следующим образом, в начале файла (отрывок из overlay.xul):

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE overlay SYSTEM "chrome://inspector/locale/overlay.dtd">

Overlay здесь — это корневой элемент в overlay.xul.

С использованием этих констант описание кнопки в панели инструментов стало таким:

<toolbarbutton
 id="inspector-button"
 class="toolbarbutton-1 chromeclass-toolbar-additional"
 label="&toolbarbutton_title;"
 tooltiptext="&toolbarbutton_tooltiptext;"
 oncommand='inspectorToolbar.buttonClick(this)'
 />

Если нужно добавить английскую (как и любую другую) локализацию, то:

  1. Создаем папку внутри папки /locale. Название может быть любым, но для удобства назовем её en-US.
  2. Создаем внутри этой папки overlay.dtd и пишем в неё переводы строк на английский язык <!ENTITY toolbarbutton_title "4PDA Inspector">
    <!ENTITY toolbarbutton_tooltiptext "4PDA Inspector">
    ...
  3. В chrome.manifest добавляем строку locale inspector en-US locale/en-US/
  4. ???
  5. PROFIT!

2. Парсинг количества QMS сообщений

При наличии QMS сообщений внизу страницы появляется вот такой блок

4PDA QMS сообщения

Количество сообщений выводится в span с id «events_count_val», потому регулярка будет простейшей:

qmsBlockRegExp: /\<span id\=\"events_count_val\"\>(\d+)\<\/span\>/

Да и функция, работающая с ней, тоже простая, почти ничем не отличающаяся от getMesCount():

getQmsCount: function(text)
{
 if (!text)
  return 0;

 var ff = text.match(inspectorContentScript.qmsBlockRegExp);

 if (typeof (ff) == 'object' && ff != null && (typeof ff[1] != 'undefined'))
  return ff[1];
  else
  return 0;
}

Ну и вывод. Overlay.xul:

<hbox id="inspector_unreadQms">
 <label value="&inspector_unreadQmsCount;: "/>
 <label value="0" id="inspector_unreadQmsCount" />
</hbox>

И toolbar.js, функция showPanel():

this.winobj.getElementById('inspector_unreadQmsCount').value = inspectorContentScript.unreadQmsCount;

3. Кнопка становится более настраиваемой — можно изменять размер и цвета текста — сам цвет текста и фоновый.

Настройки, как уже известно, первым делом пишутся в /defaults/preferences

pref("extensions.4pda-inspector.button_fontsize", 8);
pref("extensions.4pda-inspector.button_bgcolor", '#4474C4');
pref("extensions.4pda-inspector.button_color", '#FFFFFF');

А затем добавляются в окно настроек:

<preference id="pref_button_fontsize" name="extensions.4pda-inspector.button_fontsize" type="int"/>
<preference id="pref_button_bgcolor" name="extensions.4pda-inspector.button_bgcolor" type="string"/>
<preference id="pref_button_color" name="extensions.4pda-inspector.button_color" type="string"/>
...
<vbox>
<vbox style="border-bottom: 1px solid #4474C4">
 <label value="&fontSize; (px):" control="inspector_button_fontsize"/>
 <textbox preference="pref_button_fontsize" id="inspector_button_fontsize" type="number" min="1"/>
</vbox>
<hbox>
 <vbox style="border-right: 1px solid #4474C4; margin-right:5px; padding-right:5px;">
  <label value="&fontColor;:" control="inspector_button_fontcolor" id="inspector_button_fontcolor_label"/>
  <colorpicker type="button" preference="pref_button_color" id="inspector_button_fontcolor"/>
 </vbox>
 <vbox>
  <label value="&areaColor;:" control="inspector_button_fontbgcolor" id="inspector_button_fontbgcolor_label"/>
  <colorpicker type="button" preference="pref_button_bgcolor" id="inspector_button_fontbgcolor"/>
 </vbox>
</hbox>
</vbox>
Окно настроек

С textbox мы уже сталкивались, а что такое colorpicker несложно догадаться по скриншоту выше, type="button" им дописан для того, чтобы они были сворачиваемы. Результат они возвращают в HEX-формате, как нам и нужно, так что никаких дополнительных действий можно не предпринимать.

3.1. Кнопка стала двухразмерной — 26х24 и 20х16.

Всё из-за стандартов — кнопка в Firefox под Linux имеет стандарт 24х24, под windows и mac os — 16х16 (подробнее здесь).

Операционная система узнается через такую несложную операцию:

this.osString = Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS;\

Вызываем её в inspectorContentScript.init() и необходимое нам значение хранится inspectorContentScript.osString (не забыть создать её). Переписываем printCount() с учетом ОС:

printCount: function(count, mesCount)
{
 var btn = this.winobj.getElementById('inspector-button');

 if (!btn)
  return false;

// учет ОС и buttun_big при выводе
if (this.osString == 'Linux' || inspectorDefaultStorage.button_big)
 {
  var canvas_width = 26;
  var canvas_height = 24;
  var canvas_img = "chrome://inspector/content/icons/icon_22x.png";
  var title_padding = 0;
  var fontSize = inspectorDefaultStorage.button_big_fontsize;
 }
 else
 {
  var canvas_width = 20;
  var canvas_height = 16;
  var canvas_img = "chrome://inspector/content/icons/icon_16x.png";
  var title_padding = 2;
  var fontSize = inspectorDefaultStorage.button_fontsize;
 }

 var canvas = this.winobj.getElementById("inspector_button_canvas");
 canvas.setAttribute("width", canvas_width);
 canvas.setAttribute("height", canvas_height);
 var ctx = canvas.getContext("2d");
 
 var img = new Image();
 img.onload = function()
 {
  ctx.textBaseline = 'top';
  ctx.font = 'bold '+fontSize+'px tahoma,arial';
  ctx.clearRect(0, 0, canvas_width, canvas_height);
  ctx.drawImage(img, 2, 0, img.width, img.height);

  var w = ctx.measureText(count).width;
  var h = fontSize + title_padding;

  var x = canvas_width - w;
  var y = canvas_height - h;

  ctx.fillStyle = inspectorDefaultStorage.button_bgcolor;
  ctx.fillRect(x-1, y, w+1, h);
  ctx.fillStyle = inspectorDefaultStorage.button_color;
  ctx.fillText(count, x, y+1);

  var w = ctx.measureText(mesCount).width;
  ctx.fillStyle = inspectorDefaultStorage.button_bgcolor;
  ctx.fillRect(0, y, w+2, h);
  ctx.fillStyle = inspectorDefaultStorage.button_color;
  ctx.fillText(mesCount, 1, y+1);

  btn.image = canvas.toDataURL("image/png");
 };

 img.src = canvas_img;

 // информатизируем tooltip кнопки
 
btn.setAttribute('tooltiptext', '4PDA - В сети'+
  '\nНовых сообщений: '+mesCount+
  '\nИзменений в темах: '+count+
  '\nНовых QMS сообщений: '+inspectorContentScript.unreadQmsCount
 );
}

В конце меняем tooltiptext (всплывающий текст) кнопки — выводим краткую информацию.

tooltiptext

Но что же за inspectorDefaultStorage.button_big, спросите вы? Это отдельный размер текста для большой кнопки, ибо без изменения этой настройки шрифт в 8px на кнопке 26х24px смотрится ущербно. Также в этом отрезке можно заметить недокументированную настройку button_big, созданную для тестирования (да и не только для тестирования) большой кнопки под windows и mac os. Добавляются в /defaults/preferences/inspector.js:

pref("extensions.4pda-inspector.button_big_fontsize", 12);
pref("extensions.4pda-inspector.button_big", false);

ОС еще учитывается в функции printLogout(), а именно при задании изображения кнопки:

btn.image = 'chrome://inspector/content/icons/icon_'+((this.osString == 'Linux')?'22':'16')+'x_out.png';

С окном настроек поинтереснее — два поля ввода могут ввести заблуждение, да и ни к чему они одновременно. Потому одно из них нужно скрывать. Settings.xul:

<script type='text/javascript' src='chrome://inspector/content/js/default.js'></script>
<script type='text/javascript' src='chrome://inspector/content/js/settings.js'></script>
...
<preference id="pref_button_big_fontsize" name="extensions.4pda-inspector.button_big_fontsize" type="int"/>
...
<textbox preference="pref_button_big_fontsize" id="inspector_button_big_fontsize" type="number" min="1"/>
...
<script type="text/javascript">
 inspectorSettings.init();
</script>

Settings.js:

var inspectorSettings = {

 init: function()
 {
  var osString = Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS;

  if (osString == 'Linux' || inspectorDefaultStorage.button_big)
   document.getElementById('inspector_button_fontsize').style.display = 'none';
  else
   document.getElementById('inspector_button_big_fontsize').style.display = 'none';
 }

}

То есть, узнаем ОС и режим и в зависимости от них скрываем ненужное нам поле.

4. Удаление ссылок после перехода и т. п.

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

newElem.addEventListener('click', function(){
  var dataTheme = this.getAttribute('data-theme');
  inspectorToolbar.openTheme(dataTheme);
  delete inspectorToolbar.unreadThemes[dataTheme];
  this.parentNode.removeChild(this);
  inspectorContentScript.visitedThemes.push(dataTheme);
  inspectorContentScript.printCount(Object.keys(inspectorToolbar.unreadThemes).length, inspectorContentScript.unreadQmsCount);
});

То есть, при клике:

  1. открывается новая вкладка с темой
  2. удаляется элемент массива unreadThemes, хранящий тему
  3. удаляется сама ссылка на тему
  4. тема добавляется в массив visitedThemes — этот массив нужен для учета прочитанных между обновлениями тем, чтобы не выводить при выводе панели прочитанные темы. Очищается при каждом обновлении.
  5. пересчитывается и выводится количество непрочитанных тем на кнопке.

5. Применение настроек при закрытии окна

Предназначается для MacOS и некоторых дистрибутивов Linux из-за отсутствия в диалоговых окнах firefox для них кнопки «ОК».

настройки 4pda инспектор на mac os

Для этого нужно задать функцию закрытия диалогового окна — ondialogcancel.

<prefwindow

 ondialogcancel="inspectorContentScript.settingsAccept()"

>

Скачать 4pda_inspector v.0.5.3.12: zip, xpi

UPD:

В апреле 2013 года на форуме 4pda.ru были отключены личные сообщения, из-за этого проверка авторизации не работает в версиях с 0.2 по 0.5. Чтобы вновь заставить дополнение работать достаточно изменить функцию getMesCount() внутри contentscript.js:

getMesCount: function(text)
{
   return 0;
}

В таком случае проверка if (mesCount === false) в функции getNewCount() не будет возвращать true, и вывод остальных данных не будет прерван.

P.S.