Делаем первый плагин для OutWiker

Немного рекламы

Оглавление

Введение

Одна из главных особенностей OutWiker - это расширение базовых возможностей с помощью плагинов, причем плагины писать довольно легко.

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

Структура папок плагинов

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

Под Windows 7 / 8.x это будет:

C:\Users\USERNAME\AppData\Roaming\outwiker\plugins

Под Ubuntu Linux, если у вас не установлена переменная окружения $XDG_CONFIG_HOME, папка с плагинами может иметь такой путь:

/home/USERNAME/.config/outwiker/plugins

Вот, например, как у меня выглядит папка с настройками:

outwiker
├── plugins
│   ├── changepageuid
│   ├── counter
│   ├── diagrammer
│   ├── export2html
│   ├── externaltools
│   ├── htmlheads
│   ├── lightbox
│   ├── livejournal
│   ├── sessions
│   ├── source
│   ├── spoiler
│   ├── statistics
│   ├── style
│   ├── tableofcontents
│   ├── thumbgallery
│   └── updatenotifier
├── styles
└── outwiker.ini

Все плагины для OutWiker представляют собой пакеты Python, т.е. это папка, в которой располагается файл __init__.py, наборы файлов *.py, и, возможно, вложенные пакеты. В качестве примера рассмотрим структуру плагина Sessions

sessions
├── images
│   ├── remove.png
│   └── rename.png

├── locale
│   ├── en
│   │   └── LC_MESSAGES
│   │       ├── sessions.mo
│   │       └── sessions.po
│   ├── it_IT
│   │   └── LC_MESSAGES
│   │       ├── sessions.mo
│   │       └── sessions.po
│   ├── ru_RU
│   │   └── LC_MESSAGES
│   │       ├── sessions.mo
│   │       └── sessions.po
│   ├── uk_UA
│   │   └── LC_MESSAGES
│   │       ├── sessions.mo
│   │       └── sessions.po
│   └── sessions.pot

├── __init__.py
├── editaction.py
├── guicreator.py
├── i18n.py
├── misc.py
├── plugincontroller.py
├── saveaction.py
├── sessioncontroller.py
├── sessions.py
└── sessionstorage.py

Внутри папки плагина располагается обязательный файл __init__.py и набор py-файлов (про их содержимое, а также о том, какой из них считается главным будет сказано ниже). Кроме того, в папке плагина есть вложенные папки: images с картинками для кнопок и папка locale с локализациями плагина. Для данного плагина имеется три локализации - для русского (ru_RU), украинского (uk_UA) и итальянского (it_IT) языков. Конечно, плагин можно делать не локализуемым, но лучше предусмотреть возможность для локализации. Забегая вперед скажу, что локализация осуществляется с помощью gettext, локализации будет посвящен отдельный раздел.

Что касается множества файлов *.py, то, конечно, можно весь текст плагина уместить в одном файле, это уже вопрос стиля программирования.

Процесс загрузки плагинов. Базовый класс плагинов

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

Класс, который занимается загрузкой плагинов - outwiker.core.pluginsloader.PluginsLoader, он располагается в папке /src/outwiker/core. Сейчас подробно рассматривать внутренности этого класса не будем, рассмотрим процесс загрузки плагина в целом. Идеологически загрузка плагинов работает так же, как описано в статье Делаем плагины на Python, но есть некоторые особенности.

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

Поиск главного модуля с плагином осуществляется следующим образом: последовательно импортируются все модули *.py, в успешно импортированных модулях ищутся классы, удовлетворяющие следующим двум критериям:

  • Класс должен быть производным от outwiker.core.pluginbase.Plugin.
  • Имя класса должно начинаться с "Plugin..."

Для того, чтобы нам самим было проще искать модуль с главным классом плагина, рекомендуется называть этот модуль также, как называется сам плагин, но используя символы только нижнего регистра. Например, для плагина Source главный класс называется PluginSource, а располагается он в файле source.py.

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

Для наглядности ниже приведена сокращенная версия класса Plugin, где видны все методы и свойства, которые необходимо переопределить в производном класса (сам класс Plugin является абстрактным).

class Plugin (object):
    """
    Базовый класс для плагинов
    """

    __metaclass__ = ABCMeta

    def __init__ (self, application):
        self._application = application

    ###################################################
    # Свойства и методы, которые необходимо определить
    ###################################################

    @abstractproperty
    def name (self):
        """
        Свойство должно возвращать имя плагина
        """

        pass


    @abstractproperty
    def description (self):
        """
        Свойство должно возвращать описание плагина
        """

        pass


    @abstractproperty
    def version (self):
        """
        Свойство должно возвращать строку, описывающую версию плагина в формате "x.y.z"
        """

        pass


    @property
    def url (self):
        return None


    @abstractmethod
    def initialize (self):
        """
        Этот метод вызывается, когда плагин прошел все проверки.
        Именно здесь плагин может начинать взаимодействовать с программой
        """

        pass


    @abstractmethod
    def destroy (self):
        """
        Этот метод вызывается при отключении плагина
        """

        pass

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

Среди свойств класса Plugin есть одно, которое переопределять в производном классе не обязательно - это свойство url, оно предназначено для указания ссылки на страницу плагина. Если у плагина нет своей страницы в интернете, то это свойство будет возвращать None.

Первый плагин

Сказанного выше достаточно для создания первого плагина для OutWiker, который, правда, пока ничего не будет делать, но зато он будет красоваться в списке установленных плагинов.

Пусть наш плагин будет называться MyFirstPlugin.

Для этого переходим в папку с плагинами и создаем там папку myfirstplugin, а в ней пустой файл __init__.py, а также файл myfirstplugin.py, который будет содержать исходный код плагина.

plugins
└── myfirstplugin
    ├── __init__.py
    └── myfirstplugin.py

Для того, чтобы плагин определялся программой, достаточно создать класс, производный от Plugin, и определить все абстрактные методы и свойства. Таким образом, минимальный плагин может содержать только такой код:

# -*- coding: UTF-8 -*-

from outwiker.core.pluginbase import Plugin

__version__ = u"0.0.1"


class PluginMyFirst (Plugin):
    """
    Простейший плагин
    """

    @property
    def name (self):
        return u"MyFirstPlugin"


    @property
    def description (self):
        return u'''This is first plugin'''


    @property
    def url (self):
        return u"http://example.com"


    @property
    def version (self):
        return __version__


    def initialize(self):
        pass


    def destroy (self):
        pass

В этом коде используется переменная '_version_' для задания номера версии. В принципе, строку u"0.0.1" можно было бы возвращать непосредственно из свойства version, но в приведенном коде в случае обновления номера версии удобнее искать, к тому же, переменную '_version_' часто используют для нумерации версии модулей.

Весь текст мы пишем на английском языке, поскольку в будущем будем добавлять локализации для плагина.

Если теперь мы запустим OutWiker и выберем пункт меню "Правка - Параметры", а затем выберем раздел "Плагины", то среди установленных плагинов увидим MyFirstPlugin:

myfirstplugin_01.png: 800x528, 101k (02.10.2014 22:18)

Исходный код плагина на данной стадии можно скачать здесь.

Проверка версии OutWiker

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

Проверку на версию проще всего осуществлять следующим образом: объявлять главный класс плагина только в том случае, если он импортируется в OutWiker подходящей версии. Изменим вышеприведенный код в файле myfirstplugin.py, а потом рассмотрим его работу.

# -*- coding: UTF-8 -*-

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion
from outwiker.core.version import Version


__version__ = u"0.0.2"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    # Здесь импортируем специфичные для плагина модули

    class PluginMyFirst (Plugin):
        """
        Простейший плагин
        """

        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return u'''This is a first plugin'''


        @property
        def url (self):
            return u"http://example.com"


        @property
        def version (self):
            return __version__


        def initialize(self):
            pass


        def destroy (self):
            pass

В начале модуля мы импортировали функцию getCurrentVersion из модуля outwiker.core.commands, а также класс Version из модуля outwiker.core.version. Функция getCurrentVersion возвращает экземпляр класса Version, описывающий текущую версию OutWiker. В коде мы создаем отдельный экземпляр класса Version с минимально требуемой версией программы и сравниваем два экземпляра класса. Если текущая версия меньше требуемой, то класс PluginLoader не увидит главный класс плагина, а во время импорта пользователю будет выведена строка:

MyFirstPlugin. OutWiker version requirement: 1.8.0.742

Да, большинство пользователей эту строку не увидит, для них новый плагин просто не будет найден, но эта строка попадет в лог-файл outwiker.log под Windows или ее можно будет увидеть, запустив OutWiker в консоли под Linux.

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

Если вы используете версию OutWiker, взятую из github, то можете воспользоваться тегами (команда git tag), чтобы откатиться на одну из более старых версий, помеченных тегом, чтобы проверить работу плагина в более старых версиях. На момент написания этих строк команда git tag выдает следующий список версий OutWiker:

$ git tag

unstable_1.8.0.688
unstable_1.8.0.690
unstable_1.8.0.730
unstable_1.8.0.732_beta
unstable_1.8.0.734_beta
unstable_1.8.0.736_beta
unstable_1.8.0.740_RC
unstable_1.8.0.742_RC

Функция getCurrentVersion считывает текущую версию программы из файла version.txt, содержимое его выглядит следующим образом:

1.8.0
742
RC

Первая строка - основной номер версии, вторая строка - номер сборки, третья строка - формальный статус сборки (стабильная версия, находящаяся в активной разработке, альфа-, бета-версия и т.д.) Список строк, описывающий статус сборки можно найти в классе StatusSet внутри модуля outwiker.core.version.

В большинстве случаев этот статус избыточен, он полезен в первую очередь в окне "О программе", чтобы пользователь мог посмотреть, с насколько стабильной версией он имеет дело. Проверять статус сборки в плагине не обязательно, все равно он меняется вместе с номером сборки.

Забегая вперед скажу, что модуль outwiker.core.commands содержит множество высокоуровневых функций, которые могут пригодиться при разработке, функциями из этого метода мы будем активно пользоваться.

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

Глобальная переменная Application

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

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

Переменная Application - это интерфейс для доступа ко всем основным частям программы. Эта переменная хранится в модуле outwiker.core.application и представляет собой экземпляр класса ApplicationParams, описанный в том же модуле.

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

  • mainWindow - доступ к главному окну программы.
  • wikiroot - доступ к текущему открытому дереву заметок.
  • selectedPage - доступ к текущей выбранной странице.
  • config - доступ к настройкам программы OutWiker.
  • actionController - доступ к действиям (actions) программы.

По историческим причинам во многих частях программы доступ к переменной Application осуществляется следующим образом:

from outwiker.core.application import Application

...

# Использование переменной Application

Таким образом все будет работать, в новом коде лучше не импортировать переменную Application таким образом, а передавать ее в нужные классы явно, например, через конструктор.

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

class ApplicationParams (object):
    def __init__ (self):
        ...
        # Создать экземпляры событий

        # Открытие вики
        # Параметр: root - корень новой вики (возможно, None)
        self.onWikiOpen = Event()

        # Закрытие вики
        # Параметр: root - корень закрываемой вики (возможно, None)
        self.onWikiClose = Event()

        # Обновление страницы
        # Параметры:
        #     sender
        #     **kwargs
        # kwargs содержит значение 'change', хранящее флаги того, что изменилось
        self.onPageUpdate = Event()

        # Создание страницы
        # Параметры: sender
        self.onPageCreate = Event()

        # Обновление дерева
        # Параметры: sender - из-за кого обновляется дерево
        self.onTreeUpdate = Event()

        # Выбор новой страницы
        # Параметры: новая выбранная страница
        self.onPageSelect = Event()

        # Пользователь хочет скопировать выбранные файлы в страницу
        # Параметры: fnames - выбранные имена файлов (basename без путей)
        self.onAttachmentPaste = Event()

        # Изменение списка закладок
        # Параметр - экземпляр класса Bookmarks
        self.onBookmarksChanged = Event()

        # Удаление страницы
        # Параметр - удаленная страница
        self.onPageRemove = Event()

        # Переименование страницы.
        # Параметры: page - переименованная страница, oldSubpath - старый относительный путь до страницы
        self.onPageRename = Event()

        # Начало сложного обновления дерева
        # Параметры: root - корень дерева
        self.onStartTreeUpdate = Event()

        # Конец сложного обновления дерева
        # Параметры: root - корень дерева
        self.onEndTreeUpdate = Event()

        # Начало рендеринга HTML
        # Параметры:
        # page - страница, которую рендерят
        # htmlView - окно, где будет представлен HTML
        self.onHtmlRenderingBegin = Event()

        # Завершение рендеринга HTML
        # Параметры:
        # page - страница, которую рендерят
        # htmlView - окно, где будет представлен HTML
        self.onHtmlRenderingEnd = Event()

        # Изменение порядка страниц
        # Параметры: page - страница, положение которой изменили
        self.onPageOrderChange = Event()

        # Событие на принудительное сохранение состояния страницы
        # Например, при потере фокуса приложением или по таймеру.
        # Параметры: нет
        self.onForceSave = Event()

        # Событие вызывается после создания википарсера (Parser), но до его использования
        # Параметры: экземпляр Parser
        self.onWikiParserPrepare = Event ()

        # Событие вызывается, когда создается диалог с настройками
        # Параметры: экземпляр класса outwiker.gui.preferences.prefdialog.PrefDialog
        self.onPreferencesDialogCreate = Event()

        # Событие вызывается, когда закрывается диалог с настройками
        # Параметры: экземпляр класса outwiker.gui.preferences.prefdialog.PrefDialog
        self.onPreferencesDialogClose = Event()

        # Событие вызывается после (!) создания представления страницы в CurrentPagePanel
        # Параметры: page - новая выбранная страница,
        self.onPageViewCreate = Event()

        # Событие вызывается перед (!) удалением представления страницы в CurrentPagePanel
        # Параметры: page - текущая выбранная страница,
        self.onPageViewDestroy = Event()

        # Событие вызывается в конце создания всплывающего меню при нажатии правой кнопки на дереве заметок
        # Параметр: menu - созданное всплывающее меню,
        # page - страница, соответствующая заметке, на которую нажали правой кнопкой мыши
        self.onTreePopupMenu = Event()

        # Событие вызывается в конце создания всплывающего меню при нажатии правой кнопки на иконку в трее
        # Параметр: menu - созданное всплывающее меню,
        # tray - экземпляра класса OutwikerTrayIcon
        self.onTrayPopupMenu = Event()

        # Событие вызывается, когда изменяется список фабрик для создания страниц.
        # Параметры:
        #     newfactory - новая фабрика, если она добавляется, если фабрика
        #         удаляется, то этот параметр равен None
        self.onPageFactoryListChange = Event()

        # Событие вызывается до генерации HTML для викинотации и HTML-страниц.
        # Порядок вызова обработчиков препроцессинга не регламентируется
        # Параметры:
        #    page - страница, для которой генерится код
        #    result - список из одного строкового элемента, куда помещается
        #        сгенерированный код HTML. Его могут менять обработчики событий
        self.onPreprocessing = Event()


        # Событие вызывается после генерации HTML для викинотации и HTML-страниц.
        # Порядок вызова обработчиков постпроцессинга не регламентируется
        # Параметры:
        #    page - страница, для которой генерится код
        #    result - список из одного строкового элемента, куда помещается
        #        сгенерированный код HTML. Его могут менять обработчики
        #        событий, в этом случае в качестве результата, который увидит
        #        пользователь, будет эта строка после всех правок.
        self.onPostprocessing = Event()

Этот список будет постепенно дополняться, если для написания плагина вам не хватает какого-то события, пишите автору.

Добавление пункта меню (устаревший способ)

Давайте теперь добавим какое-нибудь действие для нашего плагина, по закону жанра это должна быть надпись "Hello world!".

OutWiker написан с использованием wxPython, поэтому от его использования не уйти, далее будем считать, что этой библиотекой мы пользоваться умеем, и я не буду акцентировать внимание на ее использовании.

Создадим новое меню "MyFirstPlugin", в котором будет единственный пункт "Hello". Сначала мы это сделаем устаревшим способом, который использовался до версии OutWiker 1.8, затем поговорим о том, чем этот способ плох, а в следующем разделе сделаем пункт меню новым способом с помощью Action.

Добавим в метод плагина initialize код создания нового меню с единственным пунктом, а в метод destroy код для уничтожения меню. Для простоты пусть пока надпись "Hello World" выводится в консоль.

Новый код выглядит следующим образом:

# -*- coding: UTF-8 -*-

import wx

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion
from outwiker.core.version import Version


__version__ = u"0.0.3"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    class PluginMyFirst (Plugin):
        """
        Простейший плагин
        """

        def __init__ (self, application):
            super (PluginMyFirst, self).__init__ (application)
            self.ID_HELLO = wx.NewId()
            self._menu = None


        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return u'''This is a first plugin'''


        @property
        def url (self):
            return u"http://example.com"


        @property
        def version (self):
            return __version__


        def initialize (self):
            # Проверим, что главное окно создано
            if self._application.mainWindow is not None:
                # Создадим меню "MyFirstPlugin" с единственным пунктом - "Hello"
                self._menu = wx.Menu()
                self._menu.Append (self.ID_HELLO, u"Hello")
                self._application.mainWindow.Bind (wx.EVT_MENU, self._onHello, id = self.ID_HELLO)

                self._application.mainWindow.mainMenu.Insert (0, self._menu, u"MyFirstPlugin")


        def destroy (self):
            # Проверим, что главное окно создано
            if self._application.mainWindow is not None:
                # Найдем позицию нашего меню и удалим его
                pos = self._application.mainWindow.mainMenu.FindMenu (self._menu.GetTitle())
                if pos != wx.NOT_FOUND:
                    self._application.mainWindow.mainMenu.Remove (pos)


        def _onHello (self, event):
            """
            Действие на выбор пункта меню "Hello"
            """

            print u"Hello World!"

В этом примере мы импортировали модуль wx, поскольку начинаем работать с библиотекой wxPython. Затем в коде мы несколько раз обращаемся к свойству self._application.mainWindow, которое хранит указатель на класс главного окна (экземпляр класса outwiker.gui.mainwindow.MainWindow). Этот класс является производным от wx.Frame и содержит множество свойств и членов для облегчения доступа к различным элементам интерфейса главного окна.

Один из таких членов - mainMenu, содержит указатель на экземпляр класса outwiker.gui.mainmenu.MainMenu. Этот класс является производным от wx.MenuBar и предназначен в первую очередь для облегчения доступа к стандартным пунктам меню:

  • fileMenu - меню "Файл";
  • editMenu - меню "Редактирование";
  • treeMenu - меню "Дерево";
  • toolsMenu - меню "Инструменты";
  • bookmarksMenu - меню "Закладки";
  • viewMenu - меню "Вид";
  • helpMenu - меню "Справка".

Поскольку мы создавали новое подменю в главном меню, то ни одним из этих членов мы не пользовались.

В приведенном выше коде в методах initialize и destroy осуществляется проверка на то, что главное окно создано (self._application.mainWindow is not None). Во время работы программы плагины на данный момент инициализируются всегда, когда главное окно уже создано, и может показаться, что такая проверка бесполезна. Однако это не так, если вы будете писать тесты для плагина без создания главного окна (тестам будет посвящена отдельная статья).

Остальной код относится непосредственно к созданию меню на wxPython, и не имеет каких-то особенностей, относящихся к OutWiker.

Теперь вы можете запустить OutWiker и убедиться, что перед меню "Файл" появился пункт меню "MyFirstPlugin".

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

Немного модернизируем предыдущий пример, чтобы "Hello world" выводилось не в консоль, которую видят лишь разработчики, а в диалоговое окно. Далее приведен новая версия кода (изменились только список импорта и метод _onHello).

# -*- coding: UTF-8 -*-

import wx

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion, MessageBox
from outwiker.core.version import Version


__version__ = u"0.0.3"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    class PluginMyFirst (Plugin):
        """
        Простейший плагин
        """

        def __init__ (self, application):
            super (PluginMyFirst, self).__init__ (application)
            self.ID_HELLO = wx.NewId()
            self._menu = None


        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return u'''This is a first plugin'''


        @property
        def url (self):
            return u"http://example.com"


        @property
        def version (self):
            return __version__


        def initialize (self):
            # Проверим, что главное окно создано
            if self._application.mainWindow is not None:
                # Создадим меню "MyFirstPlugin" с единственным пунктом - "Hello"
                self._menu = wx.Menu()
                self._menu.Append (self.ID_HELLO, u"Hello...")
                self._application.mainWindow.Bind (wx.EVT_MENU, self._onHello, id = self.ID_HELLO)

                self._application.mainWindow.mainMenu.Insert (0, self._menu, u"MyFirstPlugin")


        def destroy (self):
            # Проверим, что главное окно создано
            if self._application.mainWindow is not None:
                # Найдем позицию нашего меню и удалим его
                pos = self._application.mainWindow.mainMenu.FindMenu (self._menu.GetTitle())
                if pos != wx.NOT_FOUND:
                    self._application.mainWindow.mainMenu.Remove (pos)


        def _onHello (self, event):
            """
            Действие на выбор пункта меню "Hello"
            """

            MessageBox (u"Hello world",
                        u"Hello",
                        wx.OK)

Здесь нужно обратить внимание, что используется функция MessageBox из модуля outwiker.core.commands. Эта функция работает аналогично одноименной функции из wxPython, но всегда нужно пользоваться именно функцией из outwiker.core.commands по нескольким причинам, одна из которых - возможность тестирования, о чем будет сказано в одной из следующий статей. MessageBox принимает те же самые параметры, что и библиотечная функция MessageBox.

Теперь при выборе пункта меню "Hello..." будет появляться диалоговое окно:

Исходный код прагина на данной стадии можно скачать здесь

Actions. Современный способ добавления пунктов меню

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

Для того, чтобы пользователи могли настраивать горячие клавиши, нужно воспользоваться так называемыми Actions, которые появились в OutWiker 1.8. Для этого нужно выполнить следующие шаги:

  • Создать класс, производный от outwiker.gui.baseaction.BaseAction, и определить необходимые абстрактные методы и свойства.
  • Зарегистрировать новое действие с помощью класса ActionController, который хранится в Application.actionController.
  • Создать пункт меню (а при желании и кнопку на панели инструметов), используя класс ActionController.
  • По окончании работы плагина удалить меню, используя класс ActionController.

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

# -*- coding: UTF-8 -*-

from abc import ABCMeta, abstractmethod, abstractproperty


class BaseAction (object):
    __metaclass__ = ABCMeta

    @abstractproperty
    def title (self):
        """
        Надпись, отображаемая в меню и на всплывающих подсказках на кнопках
        """

        pass


    @abstractproperty
    def description (self):
        """
        Короткое описание, показываемое в настройках горячих клавиш
        """

        pass


    @abstractmethod
    def run (self, params):
        """
        Метод, выполняемый при активации действия
        params - параметры, зависящие от типа кнопки/меню.
        Для обычной кнопки всегда равно None, для зажимаемой (залипающей) кнопки указывает,
        кнопка нажата или отжата
        """

        pass

Кроме перечисленных свойств и методов в новом action нужно определить член класса (не член экземпляра) с именем stringId. Это должна быть уникальная строка, используемая для двух задач:

  • Поиска нужного action (используется как ключ в ассоциативном массиве).
  • Для хранения горячей клавиши в файле настроек (используется как имя параметра в секции [hotkeys] в файле outwiker.ini).

Для плагинов рекомендуется в качестве stringId использовать строку вида "PluginName_ActionName".

Добавим в наш плагин файл actions.py со следующим содержимым:

# -*- coding: UTF-8 -*-

import wx

from outwiker.gui.baseaction import BaseAction
from outwiker.core.commands import MessageBox


class HelloAction (BaseAction):
    """
    Hello world
    """

    stringId = u"MyFirstPlugin_Hello"

    def __init__ (self, application):
        self._application = application


    @property
    def title (self):
        return u"Hello..."


    @property
    def description (self):
        return u"Hello world"


    def run (self, params):
        MessageBox (u"Hello world",
                    u"Hello",
                    wx.OK)

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

Изменим файл плагина, чтобы он использовал HelloAction.

# -*- coding: UTF-8 -*-

import wx

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion
from outwiker.core.version import Version

# Импортируем action
from actions import HelloAction


__version__ = u"0.0.4"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    class PluginMyFirst (Plugin):
        """
        Простейший плагин
        """

        def __init__ (self, application):
            super (PluginMyFirst, self).__init__ (application)
            self.ID_HELLO = wx.NewId()
            self._menu = None


        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return u'''This is a first plugin'''


        @property
        def url (self):
            return u"http://example.com"


        @property
        def version (self):
            return __version__


        def initialize (self):
            # Регистрируем HelloAction
            self._application.actionController.register (
                HelloAction (self._application),
                None)

            # Проверим, что главное окно создано
            if self._application.mainWindow is not None:
                # Создадим меню "MyFirstPlugin"
                self._menu = wx.Menu()
                self._application.mainWindow.mainMenu.Insert (0, self._menu, u"MyFirstPlugin")

                # Cоздаем пункт меню для HelloAction
                self._application.actionController.appendMenuItem (
                    HelloAction.stringId,
                    self._menu)


        def destroy (self):
            # Проверим, что главное окно создано
            if self._application.mainWindow is not None:
                # Удалим созданный пункт меню
                self._application.actionController.removeMenuItem (HelloAction.stringId)

                # Найдем позицию нашего меню и удалим его
                pos = self._application.mainWindow.mainMenu.FindMenu (self._menu.GetTitle())
                if pos != wx.NOT_FOUND:
                    self._application.mainWindow.mainMenu.Remove (pos)

            # Удалим зарегистрированный action
            self._application.actionController.removeAction (HelloAction.stringId)

Плагин будет работать точно так же, как и предыдущая версия, но теперь в списке горячих клавиш появится пункт "Hello":

myfirstplugin_04.png: 800x528, 81k (02.10.2014 22:18)

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

Перечислим основные методы класса ActionController (в этом списке будут опущены методы, предназначенные в первую очередь для тестирования и внутренней работы OutWiker):

class ActionController (object):
    """
    Класс для управления Actions - добавление / удаление пунктов меню и кнопок на панели инструментов
    """

    def register (self, action, hotkey=None):
        """
        Добавить действие в словарь. При этом никаких элементов интерфейса не создается
        action - регистрируемое действие
        hotkey - горячая клавиша по умолчанию для этого действия (Экземпляр класса HotKey).
        Если в настройках задана другая горячая клавиша, то приоритет отдается клавише из настроек
        """

        ...


    def removeAction (self, strid):
        """
        Удалить действие из интерфейса.
        strid - строковый идентификатор удаляемого действия
        """

        ...

    def getAction (self, strid):
        ...


    def appendMenuItem (self, strid, menu):
        """
        Добавить действие в меню menu
        Действие должно быть уже зарегистрировано с помощью метода register
        """

        ...


    def appendMenuCheckItem (self, strid, menu):
        """
        Добавить действие в меню menu
        Действие должно быть уже зарегистрировано с помощью метода register
        """

        ...


    def removeToolbarButton (self, strid):
        """
        Убрать кнопку с панели инструментов
        Если кнопка не была добавлена, то метод ничего не делает
        """

        ...


    def removeMenuItem (self, strid):
        """
        Удалить пункт меню действия со строковым идентификатором strid
        """

        ...


    def appendToolbarButton (self, strid, toolbar, image, fullUpdate=False):
        """
        Добавить кнопку на панель инструментов.
        Действие уже должно быть зарегистрировано с помощью метода register
        strid - строковый идентификатор действия, для которого создается кнопка на панели инструментов
        toolbar - панель инструментов, куда будет добавлена кнопка (класс, производный от BaseToolBar)
        image - путь до картинки, которая будет помещена на кнопку
        fullUpdate - нужно ли полностью обновить панель после добавления кнопки
        """

        ...


    def appendToolbarCheckButton (self, strid, toolbar, image, fullUpdate=False):
        """
        Добавить кнопку на панель инструментов.
        Действие уже должно быть зарегистрировано с помощью метода register
        strid - строковый идентификатор действия, для которого создается кнопка на панели инструментов
        toolbar - панель инструментов, куда будет добавлена кнопка (класс, производный от BaseToolBar)
        image - путь до картинки, которая будет помещена на кнопку
        fullUpdate - нужно ли полностью обновить панель после добавления кнопки
        """

        ...


    def check (self, strid, checked):
        """
        Установить или снять флажок и нажать/отжать кнопку, соответствующие действию
        """

        ...


    def enableTools (self, strid, enabled=True):
        """
        Активировать или дезактивировать интерфейс, связанный с действием с идентификатором strid
        """

        ...

Пожалуй, что docstring в приведенном выше участке кода, а также предыдущий пример достаточно подробно описывают, как работать с классом ActionController.

Исходный код плагина на данной стадии можно скачать здесь

Добавим еще немного объектной ориентированности

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

Создадим новый файл controller.py со следующим содержимым:

# -*- coding: UTF-8 -*-

import wx

from actions import HelloAction


class Controller (object):
    def __init__ (self, application):
        self._application = application

        self.ID_HELLO = wx.NewId()
        self._menu = None


    def initialize (self):
        # Регистрируем HelloAction
        self._application.actionController.register (
            HelloAction (self._application),
            None)

        # Проверим, что главное окно создано
        if self._application.mainWindow is not None:
            # Создадим меню "MyFirstPlugin"
            self._menu = wx.Menu()
            self._application.mainWindow.mainMenu.Insert (0, self._menu, u"MyFirstPlugin")

            # Cоздаем пункт меню для HelloAction
            self._application.actionController.appendMenuItem (
                HelloAction.stringId,
                self._menu)


    def destroy (self):
        # Проверим, что главное окно создано
        if self._application.mainWindow is not None:
            # Удалим созданный пункт меню
            self._application.actionController.removeMenuItem (HelloAction.stringId)

            # Найдем позицию нашего меню и удалим его
            pos = self._application.mainWindow.mainMenu.FindMenu (self._menu.GetTitle())
            if pos != wx.NOT_FOUND:
                self._application.mainWindow.mainMenu.Remove (pos)

        # Удалим зарегистрированный action
        self._application.actionController.removeAction (HelloAction.stringId)

В этом коде нет ничего нового, но при этом код главного класса плагина стал очень компактным и изящным:

# -*- coding: UTF-8 -*-

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion
from outwiker.core.version import Version


__version__ = u"0.0.5"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    from controller import Controller

    class PluginMyFirst (Plugin):
        """
        Первый плагин для OutWiker
        """

        def __init__ (self, application):
            super (PluginMyFirst, self).__init__ (application)
            self._controller = Controller (application)


        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return u'''This is a first plugin'''


        @property
        def url (self):
            return u"http://example.com"


        @property
        def version (self):
            return __version__


        def initialize (self):
            self._controller.initialize()


        def destroy (self):
            self._controller.destroy()

Здесь можно обратить внимание на то, что импортирование класса контроллера находится внутри ветки else проверки номера версии OutWiker. Это сделано из-за того, что если плагин запускается в старой версии OutWiker, и в модуле Controller импортируются классы, которых в старой версии еще не было, то класс PluginLoader не сможет импортировать модуль с главным классом, и до проверки версии дело не дойдет. При этом не произойдет ничего страшного, плагин будет просто проигнорирован (как и в случае успешного импорта, но при неподходящей версии), но при это в лог или консоль не будет добавлена запись о том, что плагин ожидает увидеть более новую версию OutWiker.

Исходный код прагина на данной стадии можно скачать здесь.

Локализация плагина

И последнее, что мы сделаем в этой статье, это добавим поддержку нескольких языков в наш плагин. Именно поэтому все строки, которые использовались выше, были на английском языке. Далее мы их обернем в специальную функцию _(...), добавим несколько строк кода и создадим сборку для локализаций с помощью gettext.

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

Для облегчения загрузки строк перевода в базовом классе Plugin существуют метод _init_i18n, который принимает два параметра: имя домена для перевода и путь до папки с локализациями. Имя домена рекомендуется использовать такое же, как и название плагина, но в нижнем регистре букв. Папку с локализациями рекомендуется называть locale.

Поскольку из главного модуля плагина нам нужно будет передавать в другие модули функцию _, создадим простой модуль, который будет использоваться как "перевалочный пункт". Назовем его i18n.py - общепринятое сокращение для слова "internationalization". Этот файл есть во всех плагинах, поддерживающих локализацию. Вот его содержимое:

def set_(lang):
    global _
    _ = lang


def get_ ():
    return _

Теперь добавим в класс PluginMyFirst метод, загружающий локализации, и сохраняющий функцию ' с помощью set' из только что созданного модуля i18n:

# -*- coding: UTF-8 -*-

import os.path

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion
from outwiker.core.version import Version


__version__ = u"0.0.6"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    from outwiker.core.system import getOS
    from controller import Controller
    from .i18n import set_

    class PluginMyFirst (Plugin):
        """
        Первый плагин для OutWiker
        """

        def __init__ (self, application):
            super (PluginMyFirst, self).__init__ (application)
            self._controller = Controller (application)


        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return u'''This is a first plugin'''


        @property
        def url (self):
            return u"http://example.com"


        @property
        def version (self):
            return __version__


        def initialize (self):
            if self._application.mainWindow is not None:
                self._initlocale(u"myfirstplugin")

            self._controller.initialize()


        def destroy (self):
            self._controller.destroy()


        def _initlocale (self, domain):
            langdir = unicode (os.path.join (os.path.dirname (__file__), "locale"), getOS().filesEncoding)
            global _

            try:
                _ = self._init_i18n (domain, langdir)
            except BaseException, e:
                print e

            set_(_)

В приведенном выше коде появился метод _initlocale, который вызывается, если в программе существует главное окно (self._application.mainWindow is not None). Этот метод определяет абсолютный путь до папки locale (которой у нас пока нет) внутри папки плагина, а затем вызывает метод _init_i18n для инициализации поддержки локализаций.

Здесь нужно обратить внимание на использование функции getOS из модуля outwiker.core.system. Эта функция является фабрикой для создания классов, производных от System - Windows или Unix в зависимости от текущей операционной системы. Эти классы содержат свойства и методы, зависящие от операционной системы, в которой запущена программа. В частности, свойство filesEncoding возвращает кодировку, в которой хранятся имена файлов файловой системы. Под Windows это "1251", а в Unix - "utf-8". Честно говоря, под Unix не мешало бы эту кодировку аккуратно определять, но пока проблем с этим не было, и руки до этого исправления не дошли. Но вы можете прислать мне патч, определяющий кодировку файловой системы.

Теперь мы может воспользоваться функцией ' и обернуть ей все строки, которые нужно переводить. Эти строки содержатся в классах PluginMyFirst, HelloAction и Controller, главное не забыть получить функцию ' с помощью метода get_ при инициализации этих классов.

Исправленная версия модуля myfirstplugin.py теперь будет выглядеть таким образом:

# -*- coding: UTF-8 -*-

import os.path

from outwiker.core.pluginbase import Plugin
from outwiker.core.commands import getCurrentVersion
from outwiker.core.version import Version


__version__ = u"0.0.6"

if getCurrentVersion() < Version (1, 8, 0, 742):
    print ("MyFirstPlugin. OutWiker version requirement: 1.8.0.742")
else:
    from outwiker.core.system import getOS
    from controller import Controller
    from .i18n import set_

    class PluginMyFirst (Plugin):
        """
        Первый плагин для OutWiker
        """

        def __init__ (self, application):
            super (PluginMyFirst, self).__init__ (application)
            self._controller = Controller (application)


        @property
        def name (self):
            return u"MyFirstPlugin"


        @property
        def description (self):
            return _(u'''This is a first plugin''')


        @property
        def url (self):
            return _(u"http://example.com")


        @property
        def version (self):
            return __version__


        def initialize (self):
            if self._application.mainWindow is not None:
                self._initlocale(u"myfirstplugin")

            self._controller.initialize()


        def destroy (self):
            self._controller.destroy()


        def _initlocale (self, domain):
            langdir = unicode (os.path.join (os.path.dirname (__file__), "locale"), getOS().filesEncoding)
            global _

            try:
                _ = self._init_i18n (domain, langdir)
            except BaseException, e:
                print e

            set_(_)

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

Больше всего строк для перевода в модуле actions.py:

# -*- coding: UTF-8 -*-

import wx

from outwiker.gui.baseaction import BaseAction
from outwiker.core.commands import MessageBox

from i18n import get_


class HelloAction (BaseAction):
    """
    Hello world
    """

    stringId = u"MyFirstPlugin_Hello"

    def __init__ (self, application):
        self._application = application

        global _
        _ = get_()


    @property
    def title (self):
        return _(u"Hello...")


    @property
    def description (self):
        return _ (u"Hello world")


    def run (self, params):
        MessageBox (_ (u"Hello world"),
                    _ (u"Hello"),
                    wx.OK)

Здесь главное не увлечься и НЕ сделать переводимой строку stringId - она должна быть единой независимо от языка.

И, наконец, файл controller.py:

# -*- coding: UTF-8 -*-

import wx

from i18n import get_
from actions import HelloAction


class Controller (object):
    def __init__ (self, application):
        self._application = application

        self.ID_HELLO = wx.NewId()
        self._menu = None


    def initialize (self):
        global _
        _ = get_()

        # Регистрируем HelloAction
        self._application.actionController.register (
            HelloAction (self._application),
            None)

        # Проверим, что главное окно создано
        if self._application.mainWindow is not None:
            # Создадим меню "MyFirstPlugin"
            self._menu = wx.Menu()
            self._application.mainWindow.mainMenu.Insert (0, self._menu, _(u"MyFirstPlugin"))

            # Cоздаем пункт меню для HelloAction
            self._application.actionController.appendMenuItem (
                HelloAction.stringId,
                self._menu)


    def destroy (self):
        # Проверим, что главное окно создано
        if self._application.mainWindow is not None:
            # Удалим созданный пункт меню
            self._application.actionController.removeMenuItem (HelloAction.stringId)

            # Найдем позицию нашего меню и удалим его
            pos = self._application.mainWindow.mainMenu.FindMenu (self._menu.GetTitle())
            if pos != wx.NOT_FOUND:
                self._application.mainWindow.mainMenu.Remove (pos)

        # Удалим зарегистрированный action
        self._application.actionController.removeAction (HelloAction.stringId)

Здесь мы перевели только заголовок создаваемого меню.

Если мы теперь запустим OutWiker с исправленным плагином, то внешне в окне программы ничего не изменится, а в консоль (или в лог) будет выведена безобидная ошибка:

$ outwiker
[Errno 2] No translation file found for domain: u'myfirstplugin'

Это все из-за того, что мы не подготовили сами файлы переводов. Давайте исправим это недоразумение.

Сначала нужно подготовить структуру папок для загрузки локализаций, после этого создать файл *.pot с перечислением строк, которые нужно переводить, а потом по этому файлу сделать файлы *.po и *.mo с переводами на нужные языки.

Структура папок с локализациями будет выглядеть следующим образом (здесь приведена структура папок плагина Counter, который переведен на русский, украинский и итальянский языки):

counter
├── locale
│   ├── en
│   │   └── LC_MESSAGES
│   │       ├── counter.mo
│   │       └── counter.po
│   ├── it_IT
│   │   └── LC_MESSAGES
│   │       ├── counter.mo
│   │       └── counter.po
│   ├── ru_RU
│   │   └── LC_MESSAGES
│   │       ├── counter.mo
│   │       └── counter.po
│   ├── uk_UA
│   │   └── LC_MESSAGES
│   │       ├── counter.mo
│   │       └── counter.po
│   └── counter.pot

Для нашего плагина мы ограничимся английской и русской локализацией. Создадим нужные папки (списки обозначений локалей можно найти, например, здесь).

myfirstplugin
├── locale
│   ├── en
│   │   └── LC_MESSAGES
│   └── ru_RU
│       └── LC_MESSAGES

Теперь нам нужно "собрать" все строки, которые должны быть локализованы, в файл myfirstplugin.pot. Для этого есть разные способы, самый простой из них - воспользоваться консольной программой xgettext, которая этот файл создаст. Для этого заходим в папку с нашим плагином и выполняем такую команду:

xgettext -o locale/myfirstplugin.pot *.py

Программа xgettext просканирует все файлы *.py и найдет все строки, обернутые функцией _. Если у вас есть файлы *.py в папках с более глубокой вложенностью, нужно указать эти папки, например:

xgettext -o locale/myfirstplugin.pot *.py folder/*.py

Пользователи Linux могут воспользоваться следующей командой, которая ищет все файлы *.py во всех папках рекурсивно:

find . -iname "*.py" | xargs xgettext -o locale/myfirstplugin.pot

В результате у нас будет создан файл locale/myfirstplugin.pot со строками, которые нужно переводить.

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

Давайте для начала сделаем перевод на английский язык. У вас может возникнуть вопрос, зачем его делать, если все строки у нас написаны и так на английском языке, а если локализаций не будет найдена, то строковые идентификаторы, обернутые функцией _ будут использоваться в качестве строк. Это верно в 90% случаев, но как раз ради оставшихся 10% это все и делается. Иногда бывает необходимость в разных частях интерфейса переводить одно и то же английское слово по-разному. Теоретически для решения этой проблемы в gettext есть такое понятие как контекст, но, к сожалению, Python не поддерживает эту возможность, поэтому иногда для строк приходится указывать не английский вариант фразы, а некий идентификатор, которую после перевода будет заменен на нужную фразу. В данном случае у нас такой проблемы нет, поэтому все переводы на английский язык будут совпадать с идентификаторами, но мы все сделаем "идеологически верно".

Итак, теперь у нас есть файл locale/myfirstplugin.pot. Запускаем Poedit и выбираем пункт меню "Файл - Создать каталог из POT-файла", в открывшемся диалоге выбираем наш файл.

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

После нажатия кнопки "Ok" нам будет предложено сохранить файл, это надо сделать в папку locale/en/LC_MESSAGES, причем файл должен называться по имени плагина - myfirstplugin.po.

После сохранения будет открыто окно со списком строк для перевода.

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

После этого нужно сохраниться, и рядом с файлом myfirstplugin.po будет создан файл myfirstplugin.mo - скомпилированная версия перевода, именно из этого файла и читаются локализованные строки.

Теперь повторим то же самое для русского языка. Отличие будет состоять в том, что в качестве языка нужно указать ru_RU, и файл myfirstplugin.po сохранить в папку locale/ru_RU/LC_MESSAGES.

Окно с переводом может выглядеть следующим образом:

Собственно, все. Запускаем OutWiker, и видим, что все строки у нас переведены:

Окончательный текст плагина можно скачать здесь.

Заключение

В заключении давайте кратко подведем итоги и выделим основные моменты статьи:

  • Мы рассмотрели структуру папок плагинов и процесс их загрузки с помощью класса PluginsLoader.
  • Все плагины имеют главный класс, производный от outwiker.core.pluginbase.Plugin, и их имена начинаются с Plugin....
  • Мы научились проверять текущий номер версии OutWiker и решать, должен ли наш плагин работать с этой версией.
  • Узнали про глобальную переменную Application и рассмотрели некоторые ее члены и свойства.
  • Научились создавать меню устаревшим способом и с помощью Actions.
  • Научились работать с классом ActionController - регистрировать действия, создавать и удалять пункты меню.
  • Научились создавать локализации плагинов с помощью xgettext и Poedit.

Эта статья подошла к концу, надеюсь, что у вас уже появилось желание писать плагины, решающие ваши повседневные задачи.

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

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

К оглавлению документации

Немного рекламы

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 5.0/5. Всего 4 голос(а, ов)




Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 916