📜 ⬆️ ⬇️

QtDockTile - cross-platform use of docks!

Considering the current trends in the development of desktops, 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 Doc, taskbar from windiws 7 and launcher from unity. Icon tasks will be added to this list in kde 4.8.
In short, there is a need to create a universal library for working 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 both in the seven, and in makosi, and in ubunt. It is based on it that the qtdocktile base API will be built, and all platform-specific 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 ordinary Qt plugins - this allows adding support for new APIs without recompiling the entire library, and if it is impossible to use this or that implementation, the plugin simply won't start. Plugins are loaded by a special singleton manager. Each plug-in tells the manager whether it can work in this environment or not, with the result that 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 break the work of the dock.
For the dock menu, the usual QMenu Qt will be used. It is only necessary to remember the limitations that this or that platform puts up.

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 to write a simple API is still half the problem, now you need to implement support for all platforms:

Attention! There will be a lot of technical details, if you are not interested in them, you can immediately proceed to the reading of the conclusion.

Unity plugin implementation



Strangely enough, but for Unity the shortest and laconic implementation turned out. All api is built on sending fairly 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 just matches the process name, and appDesktopUri is a record of the application: //$appUri.desktop type.
In order to change the value on the badge just send a message:
  QVariantMap map; map.insert(QLatin1String("count"), count); map.insert(QLatin1String("count-visible"), count > 0); sendMessage(map); 


Similarly, for the progress indicator and alarm, the menu is slightly more interesting: you need to use the DBusMenuExporter class when creating an appUri and a pointer to QMenu when creating it. That's all the API, now let's list the restrictions:

Unity Launcher API Restrictions


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


And finally: for the API to work, it is necessary to have an icon for the application in / usr / share / applications .desktop. 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 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 a couple of screenshots for a snack:


Unity:
image
KDE (Icon Tasks):
image

When writing a plugin, I used the Torkve tkimt 's tv experience.

Plugin implementation for Macos X



Also, there were no particular difficulties, for exporting the menu to Qt there is already a special method, a lot has been said about it here.
The badge is elementary using Cocoa tools; you just need to convert the 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]; 

It turned out to be a little harder to make the progress indicator: the dock API does not have a built-in method, but there is a method for drawing its 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 plug-ins went more or less smoothly, then for the most popular desktop OS everything was far from being so cloudless, I had to remember for myself the different bad words of Bill Gates, Steve Balmer and nameless programmers who carefully laid out various rakes! In the course of writing, the phrase must die, wtf, and so on, up to the good old windos, appeared more than once in my head.
There are strange and unreadable types of LPCSTR instead of wchar_t * and Hungarian notation in all fields and great and terrible COM, in a word, the style of the code is simply terrible. And there is also a problem here 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 had to go on some crutches. In addition, examples of jump lists contain the use of the ATL library, which is only available in a paid studio and is not suitable for us for this reason.
To solve problems with ABI, we with dtf decided to make a minimal C wrapper over the taskbar's COM API so that in the future we could link to it dynamically from any compiler.
The API turned out to be quite simple, the wrapper itself does not depend on Qt and can be used from anything, although it is written and not at all 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 a friend of dtf , so there was no particular difficulty 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 symbols are in the badge. The size of the icon is set through QStyle :: pixelMetrics, I found out that other overlayIcon implementations simply draw the 16x16 icon and do not care about the dpi, so the icon on my monitor turns out to be blurred.
And now the most interesting is the implementation of jump lists. This is where old Billy heard many kind words addressed to him in absentia!

Cruelty number 1 - serialization QAction'a given the limitations of the API


Each action has a name, a command that is executed when you click on the action and optionally the path to the icon in the ico format and a description. And all this needs to be transferred in the form of sishnyh wide char strings, and therefore independently follow the time of their life. And 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.
In our wrapper we will pass an array of structures of this content:
 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 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(); } } 

Here such action serialization turned out. I tried to keep the number of manual new and delete to a minimum - everything happens automatically. This approach is a guarantee that your hair will be smooth and silky!

Now let's remember the limitations of the platform and understand which actions we can serialize and which ones should be ignored. So there is no submenu in jump lists, there are no both disabled and 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 to display the icons, we had to create our own implementation of temporary files, the QTemporaryFile doesn’t really suit us, because it owns the file exclusively. I will not consider it separately: everything is very simple and clear there.

Ordeal number 2 - filling in jumpLists


To fill jump lists, 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, is not it? But alas, you have to bite the bullet and continue scribbling hundreds of lines of code, otherwise nothing will work, but we are not real men and difficulties? And since we are not afraid, let's implement the callback!

Prosecution number 3 - Implementation of the callback


So what do we have? Activating a clause in jumpList invokes a command with some set of arguments. But how do we say through it that we want to find actionInfo with a specific id and make a callback?
Dtf and I have been thinking 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 given arguments.
As a result, a method was born that accepts an action id, opens a socket on port 42042 and sends the received id to it, and the library listens to this socket and after receiving id it calmly makes a callback and our desired 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: the implementation of 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<SOCKADDR*>(&sai), sizeof(sai)) == SOCKET_ERROR) { WSACleanup(); return; } std::string cmd = cmdLine; send(sk, cmd.c_str(), cmd.size(), 0); closesocket(sk); WSACleanup(); } 


Everything, the code for today is enough, you can breathe easy, let's summarize:

Restrictions in windows implementation


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


Screenshot:

image

Conclusion



The library was very easy to use and easy to expand. While it covers only the basic features that are on all platforms. In the future, we will think about how to add platform-specific extensions.
In order for the menu to be guaranteed to be exported without problems to the dock, it must satisfy the following items:

And a couple of comments:

In other cases, something will not be available in all platforms. In principle, this is not fatal, but you need to remember this!
Thanks to Torkve for helping to implement the Unity plugin, dtf for the tremendous help in implementing the Windows plugin and the QtCreator developers for helping to implement the Macos X version.
Source code is available on github 'e. Corrections and improvements are welcome.
Shl
Anyone wanting to implement the Dockmanager API?

Source: https://habr.com/ru/post/135443/


All Articles