Рассматривая современные тенденции в развитии десктопов сложно не обратить внимание на то, что идея дока становится все более и более популярной. Существует как минимум три популярные реализации этого принципа: Маковский док, таскбар из windiws 7 и launcher'ы из unity. К этому списку в kde 4.8 добавится ещё и icon tasks.
Одним словом, назревает необходимость в создании универсальной библиотеки для работы со всем этим многообразием.
Встречаем qtdocktile
Прежде всего необходимо выделить список возможностей, которые являются общими для всех доков:
Весь этот функционал является базовым и так или иначе поддерживается и в семёрке, и в макоси, и в убунте. Именно на основе него будет строится базовое API qtdocktile, а все платформозависимые расширения будут добавляться по мере развития библиотеки и не будут являться обязательными.
Для максимальной гибкости и расширяемости я решил, что реализации каждого конкретного дока будут представлять из себя обычные Qt плагины — это позволяет добавлять поддержку новых API без перекомпиляции всей библиотеки, а в случае невозможности использовать ту или иную реализацию, плагин просто не запустится. Плагины загружает специальный синглтон-менеджер. Каждый плагин сообщает менеджеру, может ли он работать в данном окружении или нет, в результате чего менеджер может вызывать нужные методы лишь у тех плагинов, которые являются работоспособными в данной среде.
Пользователь же работает с простым классом QtDockTile, который является оберткой над менеджером. В результате чего можно безопасно создавать любое количество экземпляров QtDockTile — они не нарушат работу дока.
Для меню дока будет использоваться обычное Qtшное QMenu. Нужно лишь помнить об ограничениях, которые выставляет та или иная платформа.
Как можно заметить, оно очень простое! Но написать простое API это ещё полбеды, теперь нужно реализовать поддержку всех платформ:
Внимание! Дальше будет много технических подробностей, если вам они не интересны, то сразу можете переходить к чтению заключения.
Как ни странно, но для Unity получилась наиболее короткая и лаконичная реализация. Всё апи строится на отправки достаточно простых dbus сообщений:
Где appUri — это уникальное название приложения, в данной реализации просто совпадающее с именем процесса, а appDesktopUri — это запись вида application://$appUri.desktop.
Для того, чтобы изменить значение на бейдже достаточно отправить такое сообщение:
Аналогично для индикатора прогресса и сигнализации, с меню чуть поинтереснее: необходимо воспользоваться классом DBusMenuExporter при создании передав ему appUri и указатель на QMenu. Вот и всё API, теперь давайте перечислим ограничения:
Ну и последнее: для работы API обязательно необходимо наличие в /usr/share/applications .desktop значка для приложения. Кстати, Unity API позволяет добавлять в меню постоянные пункты, которые работают когда приложение не запущено, выглядит это примерно так:
Unity:
KDE (Icon Tasks):
При написании плагина я пользовался наработками Torkve для qutIM'а.
Тоже не возникло особенных сложностей, для экспорта меню в Qt уже есть специальный метод, о нём уже много было сказано здесь.
Бейдж же элементарно средствами Cocoa устанавливается, нужно лишь QString преобразовать в NSString, послать сообщение доку и позаботится об очисте памяти.
Чуть сложнее оказалось сделать индикатор прогресса: API дока не имеет встроенного метода, зато имеется метод для рисования своего изображения в иконке дока. Чтобы сильно не заморачиваться я просто взял реализацию индикатора из QtCreator'а, благо лицензия LGPL спокойно позволяет такой финт ушами.
И напоследок самое вкусное! Если в других системах процесс написания плагинов прошёл более-менее гладко, то для самой популярной настольной операционки все оказалось далеко не таким безоблачным, пришлось про себя поминать разными нехорошими словами Билл Гейтса, Стива Балмера и безымянных программистов, которые бережно разложили различные грабли! По ходу написания в голове не раз возникали фразы must die, wtf и тому подобное вплоть до старого доброго windos.
Тут есть и странные нечитаемые типы типа LPCSTR вместо wchar_t * и венгерская нотация во все поля и великий и ужасный COM, одним словом, стиль кода просто ужасен. А ещё есть здесь проблема в ABI в результате которой невозможно прилинковать С++ библиотеку, собранную MS компилятором к коду, собираемому minGW. Ну и само API несколько странное из за чего пришлось идти на некоторые костыли. Плюс ко всему примеры jump lists'ов содержат использование библиотеки ATL, которая есть лишь в платной студии и нам ну совершенно не подходит по этой причине.
Для решения проблем с ABI мы с dtf решили сделать минимальную Си обёртку над COM API таскбара чтобы в будущем была возможность линковаться с ней динамически из любого компилятора.
API у нее получилось весьма простым, сама обертка не зависит от Qt и её можно использовать из чего угодно, хотя она написана и совершенно не в стиле winAPI.
Начал я с самого простого: решил сделать индикатор прогресса, код для него уже был написан товарищем dtf, поэтому с переносом особенных сложностей не возникло.
Бейдж я реализовал через метод setOverlayIcon, а саму иконку я рисовал и превращал в HICON средствами Qt
В итоге в бейдж пока влазит лишь 2 символа. Размер иконки выставляется через QStyle::pixelMetrics, мной было выяснено, что другие реализации overlayIcon просто рисуют иконку 16х16 и не заботятся о dpi, поэтому у меня на мониторе иконка получается размазанной.
А теперь самое интересное — реализация jump lists'ов. Вот уж где старина Билли услышал заочно много ласковых слов в свой адрес!
У каждого действия есть название, команда, которая исполняется при нажатии на действие и опционально путь до иконки в формате ico и описание. Причем всё это нужно передавать в виде сишных wide char строк, а значит и самостоятельно следить за временем их жизни. Ну и конечно нужно как-то организовать обратный вызов, что тоже не очевидно, ибо нужно вызывать метод trigger у QAction'а, что тоже не выглядит простым на первый взгляд.
В нашу сишную обёртку мы будем передавать массив структур такого содержания:
Теперь давайте раскроем тайну void *data:
Вот такая вот сериализация действий получилась. Я постарался свести количество ручных new и delete к минимуму — всё происходит автоматически. Именно такой подход является залогом того, что ваши волосы будут гладкими и шелковистыми!
Теперь давайте вспомним об ограничениях платформы и поймем, какие действия мы можем сериализовывать, а какие лучше проигнорировать. Итак в jump lists'ах нет подменю, нет здесь и disabled и checkable пунктов, в общее число пунктов ограничено 20ью. Зато есть разделители, получается что-то вот такое:
Для того, чтобы отображались иконки, пришлось создать свою реализацию временных файлов, QTemporaryFile не очень нам подходит, ибо монопольно влалеет файлом. Её отдельно я рассматривать не буду: там всё очень просто и понятно.
Чтобы заполнить jump lists'ы нужно вызвать метод beginList
Потом этот список заполнить
И вызвать метод commitList
Многословно, не находите? Но увы, придется стиснуть зубы и продолжить строчить сотни строк кода, иначе работать ничего не будет, но мы же настоящие мужики и трудностей не боимся? А раз не боимся, то давайте реализуем обратный вызов!
Итак, что мы имеем? Активация пункта в jumpList'е вызывает команду с некоторым набором аргументов. Но как же нам через неё сказать, что мы хотим найти actionInfo с определенным id и совершить обратный вызов?
Мы с dtf долго думали над этим и он предложил сделать всё через rundll, который способен вызвать определённый метод из библиотеки с заданными аргументами.
В результате родился метод, который принимает id действия, открывает сокет на 42042 порту и передает в него полученное id, а библиотека слушает этот сокет и получив id спокойно делает обратный вызов и наш искомый QAction вызывается!
И последний метод: реализация функции, которую вызывает rundll
Всё, кода на сегодня хватит, можно вздохнуть спокойно, давайте подведем итоги:
Библиотека получилась очень простой в использовании и простой в расширении. Пока она покрывает лишь базовые возможности, которые есть на всех платформах. В дальнейшем мы будем думать как добавить платформозависимые расширения.
Чтобы меню гарантированно экспортировалось без проблем в док оно должно следующим пунктам удовлетворять:
И ещё пара замечаний:
В остальных случаях что-то будет доступно не во всех платформах. В принципе, это не смертельно, но нужно помнить об этом!
Спасибо Torkve за помощь в реализации Unity плагина, dtf за огромную помощь в реализации Windows плагина и разработчиков QtCreator'а за помощь в реализации Macos X версии.
Исходный код можно получить на github'е. Исправления и улучшения приветствуются.
ЗЫ
Есть ли желающие реализовать Dockmanager API?
Одним словом, назревает необходимость в создании универсальной библиотеки для работы со всем этим многообразием.
Встречаем qtdocktile
Общее для всех доков
Прежде всего необходимо выделить список возможностей, которые являются общими для всех доков:
- Бейджи
- Индикатор прогресса
- Меню
- Сигнализация
Весь этот функционал является базовым и так или иначе поддерживается и в семёрке, и в макоси, и в убунте. Именно на основе него будет строится базовое API qtdocktile, а все платформозависимые расширения будут добавляться по мере развития библиотеки и не будут являться обязательными.
Архитектура библиотеки
Для максимальной гибкости и расширяемости я решил, что реализации каждого конкретного дока будут представлять из себя обычные Qt плагины — это позволяет добавлять поддержку новых API без перекомпиляции всей библиотеки, а в случае невозможности использовать ту или иную реализацию, плагин просто не запустится. Плагины загружает специальный синглтон-менеджер. Каждый плагин сообщает менеджеру, может ли он работать в данном окружении или нет, в результате чего менеджер может вызывать нужные методы лишь у тех плагинов, которые являются работоспособными в данной среде.
Пользователь же работает с простым классом QtDockTile, который является оберткой над менеджером. В результате чего можно безопасно создавать любое количество экземпляров QtDockTile — они не нарушат работу дока.
Для меню дока будет использоваться обычное Qtшное QMenu. Нужно лишь помнить об ограничениях, которые выставляет та или иная платформа.
Примерное использование библиотеки
m_tile->setMenu(ui->menu);
connect(ui->pushButton, SIGNAL(clicked()), m_tile, SLOT(alert()));
connect(ui->lineEdit, SIGNAL(textChanged(QString)), m_tile, SLOT(setBadge(QString)));
connect(ui->horizontalSlider, SIGNAL(valueChanged(int)), m_tile, SLOT(setProgress(int)));
Как можно заметить, оно очень простое! Но написать простое API это ещё полбеды, теперь нужно реализовать поддержку всех платформ:
Внимание! Дальше будет много технических подробностей, если вам они не интересны, то сразу можете переходить к чтению заключения.
Реализация плагина для Unity
Как ни странно, но для Unity получилась наиболее короткая и лаконичная реализация. Всё апи строится на отправки достаточно простых dbus сообщений:
void UnityLauncher::sendMessage(const QVariantMap &map)
{
QDBusMessage message = QDBusMessage::createSignal(appUri(), "com.canonical.Unity.LauncherEntry", "Update");
QVariantList args;
args << appDesktopUri()
<< map;
message.setArguments(args);
if (!QDBusConnection::sessionBus().send(message))
qWarning("Unable to send message");
}
Где appUri — это уникальное название приложения, в данной реализации просто совпадающее с именем процесса, а appDesktopUri — это запись вида application://$appUri.desktop.
Для того, чтобы изменить значение на бейдже достаточно отправить такое сообщение:
QVariantMap map;
map.insert(QLatin1String("count"), count);
map.insert(QLatin1String("count-visible"), count > 0);
sendMessage(map);
Аналогично для индикатора прогресса и сигнализации, с меню чуть поинтереснее: необходимо воспользоваться классом DBusMenuExporter при создании передав ему appUri и указатель на QMenu. Вот и всё API, теперь давайте перечислим ограничения:
Ограничения Unity Launcher API
- Бейджи только цифровые и только больше 0. В противном случае выводится 0
- Экспортированное меню не показывает подменю, поэтому лучше их избегать.
- Если меню экспортируется еще и в appmenu, то оно не появится в доке
- В реализации DBusMenuExporter есть баг в результате чего состояние checked у меню инвертируется
Ну и последнее: для работы API обязательно необходимо наличие в /usr/share/applications .desktop значка для приложения. Кстати, Unity API позволяет добавлять в меню постоянные пункты, которые работают когда приложение не запущено, выглядит это примерно так:
X-Ayatana-Desktop-Shortcuts=NewWindow;
[NewWindow Shortcut Group]
Name=Open a New Window
Name[ast]=Abrir una ventana nueva
Name[bn]=Abrir una ventana nueva
Name[ca]=Obre una finestra nova
Name[da]=Åbn et nyt vindue
Name[de]=Ein neues Fenster öffnen
Name[es]=Abrir una ventana nueva
Name[fi]=Avaa uusi ikkuna
Name[fr]=Ouvrir une nouvelle fenêtre
Name[gl]=Abrir unha nova xanela
Name[he]=פתיחת חלון חדש
Name[hr]=Otvori novi prozor
Name[hu]=Új ablak nyitása
Name[it]=Apri una nuova finestra
Name[ja]=新しいウィンドウを開く
Name[ku]=Paceyeke nû veke
Name[lt]=Atverti naują langą
Name[nl]=Nieuw venster openen
Name[ro]=Deschide o fereastră nouă
Name[ru]=Открыть новое окно
Name[sv]=Öppna ett nytt fönster
Name[ug]=يېڭى كۆزنەك ئېچىش
Name[uk]=Відкрити нове вікно
Name[zh_CN]=新建窗口
Name[zh_TW]=開啟新視窗
Exec=firefox -new-window
TargetEnvironment=Unity
И на закуску пара скриншотов:
Unity:
KDE (Icon Tasks):
При написании плагина я пользовался наработками Torkve для qutIM'а.
Реализация плагина для Macos X
Тоже не возникло особенных сложностей, для экспорта меню в Qt уже есть специальный метод, о нём уже много было сказано здесь.
Бейдж же элементарно средствами Cocoa устанавливается, нужно лишь QString преобразовать в NSString, послать сообщение доку и позаботится об очисте памяти.
const char *utf8String = badge.toUtf8().constData();
NSString *cocoaString = [[NSString alloc] initWithUTF8String:utf8String];
[[NSApp dockTile] setBadgeLabel:cocoaString];
[cocoaString release];
Чуть сложнее оказалось сделать индикатор прогресса: API дока не имеет встроенного метода, зато имеется метод для рисования своего изображения в иконке дока. Чтобы сильно не заморачиваться я просто взял реализацию индикатора из QtCreator'а, благо лицензия LGPL спокойно позволяет такой финт ушами.
Скриншот
Реализация для Windows 7 Taskbar'а
И напоследок самое вкусное! Если в других системах процесс написания плагинов прошёл более-менее гладко, то для самой популярной настольной операционки все оказалось далеко не таким безоблачным, пришлось про себя поминать разными нехорошими словами Билл Гейтса, Стива Балмера и безымянных программистов, которые бережно разложили различные грабли! По ходу написания в голове не раз возникали фразы must die, wtf и тому подобное вплоть до старого доброго windos.
Тут есть и странные нечитаемые типы типа LPCSTR вместо wchar_t * и венгерская нотация во все поля и великий и ужасный COM, одним словом, стиль кода просто ужасен. А ещё есть здесь проблема в ABI в результате которой невозможно прилинковать С++ библиотеку, собранную MS компилятором к коду, собираемому minGW. Ну и само API несколько странное из за чего пришлось идти на некоторые костыли. Плюс ко всему примеры jump lists'ов содержат использование библиотеки ATL, которая есть лишь в платной студии и нам ну совершенно не подходит по этой причине.
Для решения проблем с ABI мы с dtf решили сделать минимальную Си обёртку над COM API таскбара чтобы в будущем была возможность линковаться с ней динамически из любого компилятора.
API у нее получилось весьма простым, сама обертка не зависит от Qt и её можно использовать из чего угодно, хотя она написана и совершенно не в стиле winAPI.
...
EXPORT void setApplicationId(const wchar_t *appId);
EXPORT void setOverlayIcon(HWND winId, HICON icon, wchar_t *description = 0);
EXPORT void clearOverlayIcon(HWND winId);
EXPORT void setProgressValue(HWND winId, int percents);
EXPORT void setProgressState(HWND winId, ProgressState state);
...
Начал я с самого простого: решил сделать индикатор прогресса, код для него уже был написан товарищем dtf, поэтому с переносом особенных сложностей не возникло.
//получаем указатель на таскбар
static ITaskbarList3 *windowsTaskBar()
{
ITaskbarList3 *taskbar;
if(S_OK != CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER, IID_ITaskbarList3, (void**)&taskbar))
return 0;
return taskbar;
}
...
// устанавливаем значение
void setProgressValue(HWND winId, int progress)
{
ITaskbarList3 *taskbar = windowsTaskBar();
if (!taskbar)
return;
taskbar->HrInit();
taskbar->SetProgressValue(winId, progress, 100);
taskbar->SetProgressState(winId, progress ? TBPF_NORMAL : TBPF_NOPROGRESS);
taskbar->Release();
}
// устанавливаем тип индикации
void setProgressState(HWND winId, ProgressState state)
{
TBPFLAG flags;
ITaskbarList3 *taskbar = windowsTaskBar();
if (!taskbar)
return;
taskbar->HrInit();
switch (state) {
default:
case ProgressStateNone : flags = TBPF_NOPROGRESS; break;
case ProgressStateNormal : flags = TBPF_NORMAL; break;
case ProgressStatePaused : flags = TBPF_PAUSED; break;
case ProgressStateError : flags = TBPF_ERROR; break;
case ProgressStateIndeterminate : flags = TBPF_INDETERMINATE; break;
}
taskbar->SetProgressState(winId, flags);
taskbar->Release();
}
Бейдж я реализовал через метод setOverlayIcon, а саму иконку я рисовал и превращал в HICON средствами Qt
QPixmap WindowsTaskBar::createBadge(const QString &badge) const
{
QPixmap pixmap(overlayIconSize());
QRect rect = pixmap.rect();
rect.adjust(1, 1, -1, -1);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
QPalette palette = window()->palette();
painter.setBrush(palette.toolTipBase());
QPen pen = painter.pen();
pen.setColor(palette.color(QPalette::ToolTipText));
painter.setPen(pen);
QString label = QFontMetrics(painter.font()).elidedText(badge, Qt::ElideMiddle, rect.width());
painter.drawRoundedRect(rect, 5, 5);
painter.drawText(rect,
Qt::AlignCenter | Qt::TextSingleLine,
label);
return pixmap;
}
В итоге в бейдж пока влазит лишь 2 символа. Размер иконки выставляется через QStyle::pixelMetrics, мной было выяснено, что другие реализации overlayIcon просто рисуют иконку 16х16 и не заботятся о dpi, поэтому у меня на мониторе иконка получается размазанной.
А теперь самое интересное — реализация jump lists'ов. Вот уж где старина Билли услышал заочно много ласковых слов в свой адрес!
Мытарство номер 1 — сериализация QAction'а с учетом ограничений API
У каждого действия есть название, команда, которая исполняется при нажатии на действие и опционально путь до иконки в формате ico и описание. Причем всё это нужно передавать в виде сишных wide char строк, а значит и самостоятельно следить за временем их жизни. Ну и конечно нужно как-то организовать обратный вызов, что тоже не очевидно, ибо нужно вызывать метод trigger у QAction'а, что тоже не выглядит простым на первый взгляд.
В нашу сишную обёртку мы будем передавать массив структур такого содержания:
struct ActionInfo
{
const char *id;
wchar_t *name;
wchar_t *description;
wchar_t *iconPath;
ActionType type;
void *data; //вот тут главная хитрость - через этот указатель мы будем реализовывать обратный вызов и заодно следить за временем жизни всех наших строк.
};
typedef void (*ActionInvoker)(void*); //указатель на функцию, аргументом её является тот самый void *data
Теперь давайте раскроем тайну void *data:
typedef QVector<ActionInfo> ActionInfoList; //будем использовать возможности С++, чтобы избежать ручного слежения за временем жизни нашего массива
typedef QVector<wchar_t> WCharArray; //аналогично для wchar_t *
static WCharArray toWCharArray(const QString &str)
{
WCharArray array(str.length() + 1);
str.toWCharArray(array.data());
return array;
}
struct Data
{
Data(QAction *action) : action(action), icon(action->icon()),
id(QUuid::createUuid().toByteArray()),
name(toWCharArray(action->text())),
description(toWCharArray(action->toolTip())),
iconPath(toWCharArray(icon.filePath()))
{
}
QWeakPointer<QAction> action;
TemporaryIcon icon;
QByteArray id;
WCharArray name;
WCharArray description;
WCharArray iconPath;
};
void invokeQAction(void *pointer)
{
Data *data = reinterpret_cast<Data*>(pointer);
if (data->action) {
qDebug() << data->action.data();
data->action.data()->trigger();
}
}
Вот такая вот сериализация действий получилась. Я постарался свести количество ручных new и delete к минимуму — всё происходит автоматически. Именно такой подход является залогом того, что ваши волосы будут гладкими и шелковистыми!
Теперь давайте вспомним об ограничениях платформы и поймем, какие действия мы можем сериализовывать, а какие лучше проигнорировать. Итак в jump lists'ах нет подменю, нет здесь и disabled и checkable пунктов, в общее число пунктов ограничено 20ью. Зато есть разделители, получается что-то вот такое:
if (!action->menu()
&& action->isVisible()
&& action->isEnabled()
&& !action->isCheckable())
list.append(serialize(action));
...
ActionInfo JumpListsMenuExporterPrivate::serialize(QAction *action)
{
Data *data = new Data(action);
ActionType type = action->isSeparator() ? ActionTypeSeparator
: ActionTypeNormal;
ActionInfo info = {
data->id.constData(),
data->name.data(),
data->description.data(),
data->iconPath.data(),
type,
data
};
return info;
}
Для того, чтобы отображались иконки, пришлось создать свою реализацию временных файлов, QTemporaryFile не очень нам подходит, ибо монопольно влалеет файлом. Её отдельно я рассматривать не буду: там всё очень просто и понятно.
Мытарство номер 2 — заполнение jumpLists'ов
Чтобы заполнить jump lists'ы нужно вызвать метод beginList
void JumpListsManager::beginList()
{
if (m_destList)
return;
ICustomDestinationList *list;
HRESULT res = CoCreateInstance(CLSID_DestinationList, 0, CLSCTX_INPROC_SERVER, IID_ICustomDestinationList, (void**)&list);
if (FAILED(res)) {
return;
}
UINT maxSlots;
m_destList = list;
m_destList->SetAppID(m_appId);
m_destList->BeginList(&maxSlots, IID_IObjectArray, (void**)&m_destListContent);
m_destListContent->Release();
IObjectArray *objArray;
CoCreateInstance(CLSID_EnumerableObjectCollection, 0, CLSCTX_INPROC_SERVER, IID_IObjectArray, (void**)&objArray);
objArray->QueryInterface(IID_IObjectCollection, (void**)&m_destListContent);
objArray->Release();
}
Потом этот список заполнить
void JumpListsManager::addTask(ActionInfo *info)
{
if (!m_destList)
return;
IShellLinkW *task;
HRESULT res = CoCreateInstance(CLSID_ShellLink, 0, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void**)&task);
if (FAILED(res))
return;
task->SetDescription(info->description);
task->SetPath(L"rundll32.exe");
task->SetArguments(makeArgs(info).c_str());
if (info->iconPath)
task->SetIconLocation(info->iconPath, 0);
IPropertyStore *title;
PROPVARIANT titlepv;
res = task->QueryInterface(IID_IPropertyStore, (void**)&title);
if (FAILED(res)) {
task->Release();
return;
}
InitPropVariantFromString(info->name, &titlepv);
title->SetValue(PKEY_Title, titlepv);
title->Commit();
PropVariantClear(&titlepv);
res = m_destListContent->AddObject(task);
title->Release();
task->Release();
m_actionInfoMap.insert(std::make_pair(info->id, info)); //обратите внимание на этот словарик: в нем хранится соответствие между id и указателем на действие.
}
...
void JumpListsManager::addSeparator()
{
IShellLinkW *separator;
IPropertyStore *propStore;
PROPVARIANT pv;
HRESULT res = CoCreateInstance(CLSID_ShellLink, 0, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void**)&separator);
if (FAILED(res))
return;
res = separator->QueryInterface(IID_IPropertyStore, (void**)&propStore);
if (FAILED(res)) {
separator->Release();
return;
}
InitPropVariantFromBoolean(TRUE, &pv);
propStore->SetValue(PKEY_AppUserModel_IsDestListSeparator, pv);
PropVariantClear(&pv);
propStore->Commit();
propStore->Release();
res = m_destListContent->AddObject(separator);
separator->Release();
}
И вызвать метод commitList
void JumpListsManager::commitList()
{
if (!m_destList)
return;
m_destList->AddUserTasks(m_destListContent);
m_destList->CommitList();
m_destList->Release();
m_destListContent->Release();
m_destList = 0;
m_destListContent = 0;
}
Многословно, не находите? Но увы, придется стиснуть зубы и продолжить строчить сотни строк кода, иначе работать ничего не будет, но мы же настоящие мужики и трудностей не боимся? А раз не боимся, то давайте реализуем обратный вызов!
Мытарство номер 3 — Реализация обратного вызова
Итак, что мы имеем? Активация пункта в jumpList'е вызывает команду с некоторым набором аргументов. Но как же нам через неё сказать, что мы хотим найти actionInfo с определенным id и совершить обратный вызов?
Мы с dtf долго думали над этим и он предложил сделать всё через rundll, который способен вызвать определённый метод из библиотеки с заданными аргументами.
В результате родился метод, который принимает id действия, открывает сокет на 42042 порту и передает в него полученное id, а библиотека слушает этот сокет и получив id спокойно делает обратный вызов и наш искомый QAction вызывается!
std::wstring JumpListsManager::makeArgs(ActionInfo *info)
{
std::wstring args = m_wrapperPath;
#ifdef _WIN64
args += L",_RundllCallback@28 "; // WARNING: TEST ME! // ptr×3 + int
#else
args += L",_RundllCallback@16 ";
#endif
// Convert to a wchar_t*
size_t origsize = strlen(info->id) + 1;
const size_t newsize = 64;
size_t convertedChars = 0;
wchar_t buffer[newsize];
mbstowcs_s(&convertedChars, buffer, origsize, info->id, _TRUNCATE);
args += buffer;
return args;
}
И последний метод: реализация функции, которую вызывает rundll
EXPORT void CALLBACK
RundllCallback(HWND hwnd, HINSTANCE hinst, LPSTR cmdLine, int cmdShow);
void CALLBACK RundllCallback(HWND, HINSTANCE, LPSTR cmdLine, int)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sk;
sk = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sk == INVALID_SOCKET) {
WSACleanup();
return;
}
sockaddr_in sai;
sai.sin_family = AF_INET;
sai.sin_addr.s_addr = inet_addr("127.0.0.1");
sai.sin_port = htons(Handler::port);
if (connect(sk, reinterpret_cast<SOCKADDR*>(&sai), sizeof(sai)) == SOCKET_ERROR) {
WSACleanup();
return;
}
std::string cmd = cmdLine;
send(sk, cmd.c_str(), cmd.size(), 0);
closesocket(sk);
WSACleanup();
}
Всё, кода на сегодня хватит, можно вздохнуть спокойно, давайте подведем итоги:
Ограничения в windows реализации
- Только два знака в бейдже
- В результате наших манипуляций пропадают последние файлы из jump lists'ов
- Не поддерживаются действия — переключатели и неактивные действия
- Не поддерживаются подменю
Скриншот:
Заключение
Библиотека получилась очень простой в использовании и простой в расширении. Пока она покрывает лишь базовые возможности, которые есть на всех платформах. В дальнейшем мы будем думать как добавить платформозависимые расширения.
Чтобы меню гарантированно экспортировалось без проблем в док оно должно следующим пунктам удовлетворять:
- Не иметь подменю
- Не иметь переключаемых или отключенных пунктов
- Пунктов должно быть немного
- Не должно изменяться после того, как было установлено через setMenu
И ещё пара замечаний:
- Корректная работа дока возможна лишь в случае single application, используйте док совместно с Qt Single Application или другими подобными средствами
- В бейджах лучше использовать положительные числа меньше 100
В остальных случаях что-то будет доступно не во всех платформах. В принципе, это не смертельно, но нужно помнить об этом!
Спасибо Torkve за помощь в реализации Unity плагина, dtf за огромную помощь в реализации Windows плагина и разработчиков QtCreator'а за помощь в реализации Macos X версии.
Исходный код можно получить на github'е. Исправления и улучшения приветствуются.
ЗЫ
Есть ли желающие реализовать Dockmanager API?