QtDockTile - cross-platform use of docks!

    Considering current trends in desktop development, it is difficult not to pay attention to the fact that the idea of ​​the dock is becoming more and more popular. There are at least three popular implementations of this principle: Makovsky Dock, taskbar from windiws 7 and launchers from unity. Icon tasks will also be added to this list in kde 4.8.
    In a word, the need is ripening to create a universal library to work with all this diversity.
    Meet qtdocktile


    Common to all docks



    First of all, you need to highlight a list of features that are common to all docks:
    1. Badges
    2. Progress indicator
    3. Menu
    4. Signaling

    All this functionality is basic and is somehow supported in the seven, in makoshi, and in ubunt. It is on the basis of it that the base qtdocktile API will be built, and all platform-dependent extensions will be added as the library develops and will not be required.

    Library architecture



    For maximum flexibility and extensibility, I decided that the implementations of each specific dock would be regular Qt plugins - this allows you to add support for new APIs without recompiling the entire library, and if it is impossible to use one or another implementation, the plugin simply will not start. Plugins are loaded by a special singleton manager. Each plug-in tells the manager whether it can work in this environment or not, as a result of which the manager can call the necessary methods only on those plug-ins that are workable in this environment.
    The user works with a simple QtDockTile class, which is a wrapper for the manager. As a result, you can safely create any number of QtDockTile instances - they will not interrupt the dock.
    For the dock menu, the usual Qtshnoe QMenu will be used. It is only necessary to remember the limitations that this or that platform sets.

    Sample library usage

    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)));
    


    As you can see, it is very simple! But writing a simple API is half the trouble, now you need to implement support for all platforms:

    Attention! Further there will be many technical details, if you are not interested in them, then you can immediately proceed to reading the conclusion.

    Plugin implementation for Unity



    Strangely enough, but for Unity the shortest and laconic implementation turned out. All api is based on sending quite simple dbus messages:

    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");
    }
    


    Where appUri is the unique name of the application, in this implementation it simply matches the name of the process, and appDesktopUri is an entry of the form application: //$appUri.desktop.
    In order to change the value on the badge, it is enough to send the following message:
    	QVariantMap map;
    	map.insert(QLatin1String("count"), count);
    	map.insert(QLatin1String("count-visible"), count > 0);
    	sendMessage(map);
    


    Similarly, for the progress indicator and signaling, the menu is a little more interesting: you need to use the DBusMenuExporter class when creating it by passing it appUri and a pointer to QMenu. That's all the APIs, now let's list the limitations:

    Unity Launcher API Limitations


    1. Badges are only digital and only greater than 0. Otherwise, 0 is displayed.
    2. The exported menu does not show submenus, therefore it is better to avoid them.
    3. If the menu is also exported to appmenu, then it will not appear in the dock
    4. There is a bug in the DBusMenuExporter implementation, as a result of which the checked state of the menu is inverted


    Well, the last: for the API to work, you must have an icon in / usr / share / applications .desktop for the application. By the way, the Unity API allows you to add permanent items to the menu that work when the application is not running, it looks something like this:
    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
    


    And for a snack a couple of screenshots:


    Unity:
    image
    KDE (Icon Tasks):
    image

    When writing the plugin, I used Torkve's developments for qutIM.

    Plugin implementation for Macos X



    There were also no particular difficulties, there is already a special method for exporting menus to Qt, a lot has been said about it here.
    The badge can be easily installed using Cocoa, you just need to convert QString to NSString, send a message to the dock and take care of clearing the memory.
        const char *utf8String = badge.toUtf8().constData();
        NSString *cocoaString = [[NSString alloc] initWithUTF8String:utf8String];
        [[NSApp dockTile] setBadgeLabel:cocoaString];
        [cocoaString release];
    

    Making the progress indicator turned out to be a little more difficult: the dock API has no built-in method, but there is a method for drawing your image in the dock icon. In order not to bother much, I just took the implementation of the indicator from QtCreator, since the LGPL license quietly allows such a feint with my ears.

    Screenshot

    image

    Implementation for Windows 7 Taskbar



    And finally, the most delicious! If in other systems the process of writing plugins went more or less smoothly, then for the most popular desktop OSes everything turned out to be not so cloudless, I had to remember to myself with various bad words Bill Gates, Steve Ballmer and nameless programmers who carefully laid out various rakes! In the course of writing, the phrase must die, wtf and the like arose many times right up to the good old windos.
    There are strange unreadable types like LPCSTR instead of wchar_t * and the Hungarian notation in all fields and the great and terrible COM, in a word, the code style is just awful. And there is also a problem in ABI as a result of which it is impossible to link the C ++ library compiled by the MS compiler to the code compiled by minGW. Well, the API itself is somewhat strange because of what I had to go to some crutches. In addition, examples of jump lists contain the use of the ATL library, which is available only in a paid studio and is not suitable for us for this reason.
    To solve problems with ABI, dtf and I decided to make a minimal C wrapper over the taskbar's COM API so that in the future it would be possible to link with it dynamically from any compiler.
    The API it turned out to be very simple, the wrapper itself is independent of Qt and can be used from anywhere, although it is written and completely not in the style of 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);
    ...
    


    I started with the simplest: I decided to make a progress indicator, the code for it was already written by comrade dtf , so there were no special difficulties with the transfer.

    //получаем указатель на таскбар
    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();
    }
    

    I implemented the badge through the setOverlayIcon method, and I drew the icon itself and turned it into HICON using 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;
    }
    

    As a result, only 2 characters can be inserted into the badge. The size of the icon is set through QStyle :: pixelMetrics, I found out that other implementations of overlayIcon just draw a 16x16 icon and do not care about dpi, so the icon on my monitor turns out to be blurry.
    And now the most interesting thing is the implementation of jump lists. That's where old Billy heard in absentia a lot of affectionate words addressed to him!

    Trial Number 1 - QAction serialization based on API restrictions


    Each action has a name, a command that is executed when you click on the action and, optionally, the path to the icon in ico format and a description. Moreover, all this must be transmitted in the form of wide wide char strings, and therefore independently monitor their life time. Well, of course, you need to somehow organize a callback, which is also not obvious, because you need to call the trigger method on QAction, which also does not look simple at first glance.
    We will transfer an array of structures of this content to our system wrapper:
    struct ActionInfo
    {
    	const char *id;
    	wchar_t *name;
    	wchar_t *description;
    	wchar_t *iconPath;
    	ActionType type;
    	void *data; //вот тут главная хитрость - через этот указатель мы будем реализовывать обратный вызов и заодно следить за временем жизни всех наших строк.
    };
    typedef void (*ActionInvoker)(void*); //указатель на функцию, аргументом её является тот самый void *data
    


    Now let's reveal the secret of void * data:
    typedef QVector ActionInfoList; //будем использовать возможности С++, чтобы избежать ручного слежения за временем жизни нашего массива
    typedef QVector 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 action;
    	TemporaryIcon icon;
    	QByteArray id;
    	WCharArray name;
    	WCharArray description;
    	WCharArray iconPath;
    };
    void invokeQAction(void *pointer)
    {
    	Data *data = reinterpret_cast(pointer);
    	if (data->action) {
    		qDebug() << data->action.data();
    		data->action.data()->trigger();
    	}
    }
    

    Here is such a serialization of actions. I tried to minimize the number of manual new and delete - everything happens automatically. It is this approach that ensures that your hair will be smooth and silky!

    Now let's recall the limitations of the platform and understand what actions we can serialize and which are better to ignore. So in jump lists there are no submenus, there are no disabled or checkable items here, the total number of items is limited to 20. But there are separators, it turns out something like this:
    		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;
    }
    

    In order for the icons to be displayed, we had to create our own implementation of temporary files, QTemporaryFile is not very suitable for us, because it exclusively owns the file. I will not consider it separately: everything is very simple and clear there.

    Ordeal number 2 - populating jumpLists


    To fill jump lists, you need to call the beginList method
    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();
    }
    

    Then fill this list
    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();
    }
    

    And call the commitList method
    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;
    }
    

    Verbose, don't you find? But alas, you have to bite the bullet and continue scribbling hundreds of lines of code, otherwise nothing will work, but are we real men and not afraid of difficulties? And if we are not afraid, then let's implement a callback!

    Ordeal Number 3 - Implementing Callbacks


    So what do we have? Activating an item in jumpList calls a command with some set of arguments. But how can we say through it that we want to find actionInfo with a specific id and make a callback? Dtf
    and I thought about this for a long time and he proposed to do everything through rundll, which is able to call a specific method from the library with the given arguments.
    As a result, a method was born that takes an action id, opens a socket on port 42042 and passes the received id into it, and the library listens to this socket and, having received id, calmly makes a callback and our sought-after QAction is called!
    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;
    }
    

    And the last method: implementing the function that rundll calls
    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(&sai), sizeof(sai)) == SOCKET_ERROR) {
    		WSACleanup();
    		return;
    	}
    	std::string cmd = cmdLine;
    	send(sk, cmd.c_str(), cmd.size(), 0);
    	closesocket(sk);
    	WSACleanup();
    }
    


    That's all, there’s enough code for today, you can breathe easy, let's summarize:

    Limitations in windows implementation


    1. Only two badges in the badge
    2. As a result of our manipulations, the last files from jump lists are lost.
    3. Actions are not supported - switches and inactive actions
    4. Submenus not supported


    Screenshot:

    image

    Conclusion



    The library is very easy to use and easy to expand. So far, it covers only the basic features that are on all platforms. In the future, we will think about how to add platform-specific extensions.
    To ensure that the menu is guaranteed to be exported without problems to the dock, it must satisfy the following points:
    • Do not have a submenu
    • Do not have switchable or disabled items
    • There should be few points
    • Should not be changed after it has been set via setMenu

    And a couple of comments:
    • Correct work of the dock is possible only in the case of a single application, use the dock in conjunction with Qt Single Application or other similar means
    • In badges it is better to use positive numbers less than 100

    In other cases, something will not be available on all platforms. In principle, this is not fatal, but you need to remember this!
    Thanks to Torkve for help in implementing the Unity plugin, dtf for great help in implementing the Windows plugin and QtCreator developers for help in implementing the Macos X version.
    Source code is available on github . Corrections and improvements are welcome.
    PS
    Are there any people who want to implement the Dockmanager API?

    Also popular now: