Архив за месяц: Июнь 2012

Пишем расширение для Google Chrome

Как и любой современный web обозреватель, Google Chrome поддерживает расширения. В этой статье я покажу общий подход к написанию extension`ов, после чего Вы без труда напишете свой собственный плагин.
Пример, который я буду показывать, будет называться GoogleMark и его назначением будет метка результатов поиска в google,как показано на скриншоте ниже.
Дабы не утомлять тех, кому достаточно прочесть документацию, приведу сразу на неё ссылку. Там Вы найдёте краткий обзор и руководство по быстрому старту. Но, хочу предостеречь читателя о том, что этих вводных данных не достаточно для комфортной работы. Информация о необходимых мелочах раскидана по всей документации. В этой статье я постараюсь дать ссылки на самые важные части документации.
Итак, расширения для Google Chrome пишутся на JavaScript. Все файлы расширения должны лежать в одном каталоге. Я буду называть этот каталог каталогом расширения.
Для начала я приведу скрипт, который будет раскрашивать страничку результатов поиска google. В нём совсем немного специфичного для расширения кода, по большей части -- это простой JavaScript, который Вы используете при обработке любых web-страниц.
googleMarker = {
preparePage: function() {
var links = document.getElementsByClassName("l");
var count = links.length;
for(var i = 0; i < count; ++i) {
links[i].removeAttribute("onmousedown");
}
},

markUrl: function(url, color) {
var block = googleMarker.findMarkingBlockByUrl(url);
googleMarker.markBlock(block, color);
googleMarker.saveMarker(block, color);
},

restoreUrlMark: function(url, color) {
googleMarker.markBlock(googleMarker.findMarkingBlockByUrl(url), color);
},

findMarkingBlockByUrl: function(url) {
var listItems = document.getElementsByClassName("l");
var itemsCount = listItems.length;
for(var i = 0; i < itemsCount; ++i) {
var item = listItems[i];
if(item.href == url) {
return googleMarker.findMarkingBlockByNestedElement(item);
}
}
return null;
},

findMarkingBlockByNestedElement: function(element) {
if(element == null || element == 'undefined') {
return null;
}
if(element.className == 'r') {
return element;
}
return googleMarker.findMarkingBlockByNestedElement(element.parentNode);
},

markBlock: function(block, color) {
if(block == null || block == 'undefined') {
return;
}
var image = googleMarker.getImage(color);
if(image == null) {
return;
}
block.insertBefore(image, block.firstChild);
},

getImage: function(color) {
var url = null;
switch(color) {
case "green":
url = chrome.extension.getURL("green.png");
break;
case "yellow":
url = chrome.extension.getURL("yellow.png");
break;
case "red":
url = chrome.extension.getURL("red.png");
break;
}
if(url == null) {
return null;
}
var image = new Image();
image.src = url;
return image;
},

saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { color: color, url: url } );
}
}
На случай, если Google поменяет разметку страницы результатов, я приведу HTML одного из блока результатов, которые выдаются сейчас.
<div class="vsc" pved="0CBAQkgowAQ" bved="0CBEQkQo" sig="qmv">
<h3 class="r">
<a href="http://brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html" target="_blank" class="l">
<em>Ещё один блог о программировании</em>: Асинхронное <b>...</b></a>
</h3>
<div class="vspib" aria-label="Подробнее..." role="button" tabindex="0">
<div class="vspii">
<div class="vspiic"></div>
</div>
</div>
<div class="s">
<div class="f kv">
<cite>brainstream-dev.<b>blogspot</b>.com/2012/03/c-visual-studio-11.html</cite>
<span class="vshid">
<a href="http://webcache.googleusercontent.com/search?q=cache:wtBn88UhcVYJ:brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html+&amp;cd=2&amp;hl=ru&amp;ct=clnk&amp;gl=ru"
onmousedown="return rwt(this,'','','','2','AFQjCNHoJc76l2Yol_mH3A94wFMHV1Z5AA','t0iOo1MA-N91FmlNaAEdNQ','0CBMQIDAB',null,event)"
target="_blank">

Сохраненная&nbsp;копия
</a>
</span>
<button class="gbil esw eswd"
onclick="window.gbar&amp;&amp;gbar.pw&amp;&amp;gbar.pw.clk(this)"
onmouseover="window.gbar&amp;&amp;gbar.pw&amp;&amp;gbar.pw.hvr(this,google.time())"
g:entity="http://brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html"
g:undo="poS1" title="Рекомендовать эту страницу"
g:pingback="/gen_204?atyp=i&amp;ct=plusone&amp;cad=S1">

</button>
</div>
<div class="esc slp" id="poS1" style="display:none">
Вы уже поставили +1 этой странице.&nbsp;
<a href="#" class="fl">Отменить</a>
</div>
<span class="st">
<span class="f">4 мар 2012 – </span>
<em>Ещё один блог о программировании</em>.
Все статьи этого блога могут быть использованы в соответствии с лицензией GNU FDL.<br>
</span>
</div>
</div>
Добавлять картинки я буду слева от ссылки в блоке "h3" с классом "l". Теперь разберём скрипт чуть подробнее.
Функция preparePage нужна для того, чтобы убрать все обработчики "onmousedown" из ссылок, иначе ссылка изменится чудесным образом и скрипт её уже никогда не узнает.
markUrl и restoreUrlMark делают одно и то же, за исключением того, что markUrl сохраняет метку (об этом чуть позже).
Функция getImage получает картинки из расширения. Об этом тоже чуть позже.
Все остальные функции являются вспомогательными и просто работают с DOM документа. Сохраним файл со скриптом в каталог расширения под названием googleMarker.js.
Итак, я надеюсь, что с тем, как метки будут появляться на страничке, мы разобрались и я перейду непосредственно к теме статьи.

Манифест

Любое расширение Google Chrome должно содержать файл манифеста, определяющий структуру и права Вашего расширения. Манифест для нашего примера будет следующим:
{
"name": "GoogleMark",
"version": "1.0",
"manifest_version": 2,
"description": "Marks google search results",
"content_scripts": [
{
"matches": [
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"js": [
"googleMarker.js"
]
,
"css": [
"googleMarker.css"
]
}
]
,
"background": {
"page": "background.html"
}
,
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"icons": {
"16": "menu-icon-16x16.png"
}
,
"web_accessible_resources": [
"green.png",
"yellow.png",
"red.png"
]
}
Как видно, манифест представляет собой JSON объект. Файл манифеста сохраняется в корневой каталог расширения и должен иметь имя "manifest.json". Разберём структуру объекта примера.
Поля "name", "version", "manifest_version" и "description" понятны без объяснений.
Ваше расширение может встраивать в страницы собственные JavaScript`ы и каскадные таблицы стилей. Всё это описывается в поле "content_scripts", которое представляет собой массив объектов с указанием списка файла JavaScript`ов и файлов CSS. Кроме указания файлов, можно указывать шаблоны URL тех сайтов, в которые данный контент должен быть встроен. Спецификацию шаблонов можно найти в документации.
Каждое расширение может иметь свою теневую страницу. Эта страница загружается при запуске дополнения и может не иметь никакой разметки, кроме подключения JavaScript`ов. Теневая страница указывается в поле "background".
Вы можете явно задать HTML страницу в параметре "page" или, если Вам не нужна страница, то она может быть сгенерирована автоматически, если Вы укажете список скриптов в параметре "scripts".
Права, которыми обладает расширение вообще и теневая страница в частности, перечисляются в поле "permissions". В это поле вносятся шаблоны URL, к которым есть доступ у скриптов и список разрешённых модулей API.
В поле "icons" перечислены иконки расширения разных размеров. В частности, иконка 16x16 будет использоваться в меню (см. скриншот в начале статьи).
В списке "web_accessible_resources" перечисляются те ресурсы дополнения, которые могут быть доступны из страницы браузера. В частности, те скрипты, которые определены в поле "content_scripts" могут получить только те ресурсы, которые здесь перечислены. Функция getImage, в скрипте, приведённом в начале статьи, загружает файлы "green.png", "yellow.png" и "red.png" используя функцию chrome.extension.getURL для получения URL на эти ресурсы. URL ресурсов дополнения выглядит так:
chrome-extension://[ID пакета дополнения]/[путь]
Полную спецификацию манифеста можно найти в документации.

Теневая страница

Непосредственно в теневой странице запрещено исполнение JavaScript политикой безопасности, поэтому код HTML страницы будет представлять из себя только подключение скриптовых файлов и структур HTML для хранения отмеченных страниц. Естественно, что такая организация сохранения высосана из пальца только для того, чтобы продемонстрировать работу с теневой страницей. Вот её разметка.
<html>
<head>
<script type="text/javascript" src="jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="background.js"></script>
</head>
<body>
<ul id="green"></ul>
<ul id="yellow"></ul>
<ul id="red"></ul>
</body>
</html>
Как видно, на теневой странице мы можем использовать любые скрипты, в частности, я подключил jQuery.

Контекстное меню

Для работы с контекстным меню в Google Chrome API служет модуль contextMenus. Для его работы нужно добавить строку "contextMenus" в поле "permissions" манифеста. Добавим в файл background.js функцию для создания меню. Внимание, большинство API функций Google Chrome являются асинхронными и возвращают управление сразу после вызова. Для обработки результатов все асинхронные вызовы опционально принимают функцию обратного вызова.
function createMenu() {
var urls = [
"*://*.google.com/*",
"*://*.google.ru/*"
];

var root = chrome.contextMenus.create({
title: "Mark as",
contexts: [ "link" ],
documentUrlPatterns: urls
});

chrome.contextMenus.create({
title: "Green",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'green')"
});
}
});

chrome.contextMenus.create({
title: "Yellow",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'yellow')"
});
}
});

chrome.contextMenus.create({
title: "Red",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'red')"
});
}
});
}

createMenu();
Всё элементарно. Вызывая функцию chrome.contextMenus.create и передавая в неё детальное описание, мы получаем новый пункт меню. Полное описание объекта, описывающего пункт меню можно посмотреть в документации к функции, а я опишу лишь самые основные.
В параметре documentUrlPatterns перечисляются шаблоны URL адресов, на которых пункт меню будет появляться.
В списке contexts перечисляются типы элементов, при правом щелчке на которые, будет появляться данный пункт меню. Доступные варианты такие: "all", "page", "frame", "selection", "link", "editable", "image", "video", "audio".
Параметр parentId является дескриптором родительского пункта меню. Каждый вызов функции chrome.contextMenus.create возвращает такой дескриптор.
Наиболее интересным, для нас, является поле onclick. Сюда помещается обработчик выбора пункта меню.

Вкладки

В функцию обратного вызова onclick элемента меню, помимо информации об элементе, для которого было вызвано контекстное меню, передаётся информация о вкладке, в которой отображается документ с указанным элементом.
Одной из самых полезных функций модуля tabs (не забудьте указать значение "tabs" в поле "permissions", в манифесте) является функция chrome.tabs.executeScript. Как ясно из названия, она позволяет выполнить скрипт в документе, который находится во вкладке, идентификатор которой передаётся первым параметром. Вторым параметром передаётся объект, содержащий код скрипта или имя файла со скриптом. Работу этой функции и демонстрируют обработчики onclick пунктов меню.

Запросы

Для синхронизации работы встраиваемых скриптов с теневой страницей Google Chrome API предоставляют возможность скриптам обмениваться информацией посредством запросов. Напомню, как выглядит функция сохранения метки в файле googleMarker.js
saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { color: color, url: url } );
}
В этой функции отсылается запрос с объектом, описывающим маркер для сохранения. Вообще говоря, Вы можете передавать любой объект в качестве запроса в функцию chrome.extension.sendRequest. Кроме того, этой функции можно передать идентификатор дополнения и функцию прослушки ответов.
Для работы с функциями из модуля extension не требуется никаких указаний в манифесте.
Чтобы наше расширение смогло реагировать на запросы, следует подписаться на их прослушивание. В нашем случае это будет выгладить так:
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
var $list = $("#" + request.color);
if($list.lengh == 0) {
return;
}
$list.append($("<li>").append(request.url));
}
);
Параметры функции обработки события chrome.extension.onRequest довольно очевидны; это запрос, отправитель запроса (идентификаторы вкладки и дополнения) и функция отправки ответа.
В нашем случае, мы, воспользовавшись jQuery, добавляем в теневую страницу новый элемент списка.

События навигации

Google Chrome API предоставляют возможность отслеживать события навигации с помощью модуля webNavigation. Для его использования следует добавить в Ваши "permissions" элемент "webNavigation" в файле манифеста. Модуль предоставляет несколько полезных событий и функций, я же продемонстрирую использование события chrome.webNavigation.onDOMContentLoaded, наступающего, как и следует из названия, после загрузки дерева элементов документа. Мы воспользуемся этим событием для подготовки страницы (удаления атрибута onmousedown) и восстановления сохранённых меток.
chrome.webNavigation.onDOMContentLoaded.addListener(
function(details) {
if(details.frameId != 0) {
return;
}
chrome.tabs.executeScript(details.tabId, {
code: "googleMarker.preparePage()"
}
);
var colors = ["green", "yellow", "red"];
for(var i = 0; i < colors.length; ++i) {
var $urls = $("#" + colors[i] + " li");
if($urls.length == 0) {
continue;
}

$urls.each(function() {
chrome.tabs.executeScript(details.tabId, {
code: "googleMarker.restoreUrlMark('" +
this.innerText + "', '" + colors[i] + "')"
}
);
});
}
}
);
В функцию обработки события приходит объект, содержащий идентификатор вкладки, идентификатор фрейма, URL и время окончания загрузки дерева. Идентификатор фрейма равен нулю, если дерево было построено в корневом документе вкладки, иначе -- это уникальный (в пределах вкладки) идентификатор фрейма HTML страницы. Всё остальное из фрагмента кода, приведённого чуть выше, нам уже знакомо.

Завершение

Последнее, что нам осталось сделать -- это добавить файлы googleMarker.css, menu-icon-16x16.png, green.png, yellow.png и red.png для отображения всех необходимых элементов.
.r img {
margin-right: 15px;
}
Теперь плагин готов к установке. Для этого переходим в Google Chrome в меню -> Tools -> Extesions и ставим галку на Developer mode. Нажимаем Load unpacked extension и выбираем каталог с расширением.
Теперь мы можем протестировать новое расширение и отладить его. Все встраиваемые скрипты доступны из стандартного инструментария (F12), а чтобы отладить скрипты теневой страницы достаточно нажать на соответствующую ссылку в менеджере дополнений.

Иконка и popup

Итак, убедившись, что всё работает, мы хотим добавить немного интерактивности в виде иконки расширения и всплывающего окна с кнопкой для очистки всех маркеров.
Для этого добавляем файл иконки (icon.png) и два файла для представления popup окна: popup.html и popup.js. Заносим новые сведения в файл манифеста, добавляя поле browser_action
{
"name": "GoogleMark",
"version": "1.0",
"manifest_version": 2,
"description": "Marks google search results",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}
,

"content_scripts": [
{
"matches": [
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"js": [
"googleMarker.js"
]
,
"css": [
"googleMarker.css"
]
}
]
,
"background": {
"page": "background.html"
}
,
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"icons": {
"16": "menu-icon-16x16.png"
}
,
"web_accessible_resources": [
"green.png",
"yellow.png",
"red.png"
]
}
Разметка popup.html очень проста
<html>
<body>
<button id="clear-btn">Clear markers</button>
<script src="popup.js" type="text/javascript"></script>
</body>
</html>
Здесь только кнопка и подключение скрипта. Выполнение inline скриптов снова отключено по соображениям безопасности.
В файле popup.js мы подписываемся на нажатие кнопки и делаем запрос к фоновой странице.
var button = document.getElementById("clear-btn");
button.addEventListener("click", function() {
chrome.extension.sendRequest( { action: "clear" } );
});
Так как у нас появляется второе событие, то вводим поле action в запрос. По этой причине дописываем значение "save" в запросе на сохранение маркера (файл googleMarker.js)
saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { action: "save", color: color, url: url } );
}
Осталось немного поправить обработчик запросов:
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
switch(request.action) {
case "save":
var $list = $("#" + request.color);
if($list.lengh == 0) {
return;
}
$list.append($("<li>").append(request.url));
break;
case "clear":
$("ul").empty();
break;
}
}
);
Всё готово! Жмём кнопку Reload в менеджере расширений и у нас появляется значок в верхнем правом углу браузера. Теперь Вы можете упаковать ваше расширение в файл *.crx нажав кнопку Pack extestion в менеджере расширений или залить его в Chrome Web Store.
Скачать исходный код примера можно здесь.

1с 8.1. Превышение максимального количества субконто

При обновлении конфигурации 1с 8.1 возникла ошибка: Превышение максимального количества субконто по счету 07.2 
Эта ошибка не позволяет принять обновления базы данных.
Проблема поставила меня в тупик, но  оказалось, что решается просто.



Какой-то пользователь добавил в плане счетов в счет 07.2  третье субконто: "Договоры". 

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

Я зашёл в базе в план счетов и удалил субконто "Договоры" со счета 07.2. Конфигурация обновилась без проблем 

OpenOffice Base — что же дальше

Я понимаю, что читатель истосковался по более сложному коду. И он скорo будет опубликован. Мое направление сейчас - обернуть функционалом стандартный котнрол Диалогов - ComboBox.

Напомню, что Combobox Диалогов это не совсем тот же элемент, что и Combobox Формы. Но он нам очень пригодился бы для реализации ввода во "всплывающие" и модальное диалоговое окно, где загружать форму было бы слишком неюзабельно. Пока.

Объекты Connection, Statement, ResultSet в OpenOffice

Для решения простых задач достаточно декларативно задать объекты данных в Мастерах-построителях или в Палитрах свойств объектов Формы. Если бы на этом возможности OpenOffice заканчивались, то кроме игр с данными ему сложно было бы найти применение. Вспоминаю сколько раз я раскрывал Star/OpenOffice, создавал подключение к серверу баз данных и с грустью закрывал, полистав весьма скудный (и до сих пор это так) Help.

Но время не стоит на месте. В Интернете начала накапливаться полезная информация о макросах OpenOffice и вот мы здесь.

Для начала работы следует получить объект Connection. Его можно получить несколькими способами. В частности из свойства ActiveConnection объекта Form или по символическому имени DataSource, которое можно задавать из Главного меню|OpenOffice Tools|Options|OpenOffice.org Base|Databases. Если вынести функцию GetConnection в скрипт из раздела My Macros, всегда будет достаточно изменить код ровно в одном месте чтобы измененить подключение.

Function GetConnection() As Variant

  Dim DataSource As Variant
  Dim Connection As Variant
  DatabaseContext = CreateUnoService("com.sun.star.sdb.DatabaseContext")
  DataSource = DatabaseContext.GetByName("sourcename")
  GetConnection = DataSource.GetConnection()

End Function

Получив Connection надо создать Satement или PreparedStatement.

oStatement = GetConnection().CreateStatement
oResultSet = Statement.ExecuteQuery("select * from ...")
lCount = Statement.ExecuteUpdate("update ...")

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

pStatement = GetConnection().PrepareStatement("update ... set ... = ? where ... = ?")
pStatement.SetDouble(1, 123.456)
pStatement.SetString(2, "789" )
                                                    ' pStatement.ExecuteQuery
pStatement.ExecuteUpdate


Объект ResultSet, полученный в результате запроса поддерживает удобную (привычную) навигацию и может быть изменяемым. То есть все плюшки, известные по ODBC/JDBC/DAO/ADO  и иже с ними налицо. Впрочем это все уже можно прочитать в Интернете. Я же хотел дать зацепки для поиска нужных моментов. Пока.