Qtでi3ウィンドウマネージャーのパネルを作成する

i3は私のお気に入りのタイルウィンドウマネージャーです。 しかし最近、デスクトップの別の塗り直しを行ったところ、私は1つの不愉快なことに出くわしました。私のネイティブパネルの機能は、すべての空想を実現するには不十分です。 特に、境界線のサイズを変更または変更する方法がわかりません。 そして、ソフトウェアがそれに適合せず、代替手段がない(そして何もない)場合、Linuxoidは何をしますか? そうです、既存のものにパッチを当てるか、自分で書いてください。 標準パネルが書かれているxcbを扱うつもりはまったくないので、2番目の方法に進みました。 C ++が言語として選択されました。 フレームワークについてK.O.に尋ねる



まえがき


一般的なパネルとは何ですか? このテーマに関するウィキペディアは次のように語っています。

「タスクバー(英語のタスクバー)-他のプログラムを起動したり、すでに実行中の管理に使用されるアプリケーションであり、ツールバーです。 特にアプリケーションウィンドウの管理に使用されます。

しかし、ウィンドウタイルマネージャーでは、物事はそれほど単純ではありません。 それらのウィンドウは互いにオーバーラップしません。 マネージャーは、各ウィンドウにスペース(タイル)を割り当てます。このウィンドウには、このウィンドウが引き伸ばされます。 例:



画像



ほら すべての窓が見えています。 そのため、このようなマネージャーでのタスクバーの必要性の問題は自然に消えます。 しかし、1つの画面では多くのアプリケーションに適合できません。 どうする そして、ここにいくつかのデスクトップの概念があります。 とても便利です! ウィンドウのフォーカス、サイズ、位置を毎分変更する必要はありません。 最初のデスクトップに切り替えました-ここに開発アプリケーションのセットがあり、2番目に切り替えました-ファイルマネージャーとプレーヤーを隣に置いてください。 などなど。 理論的には、無限に多くのデスクトップが存在する可能性がありますが、一度に10を超えるデスクトップを使用することは非常に問題になる可能性があります。

したがって、パネルはウィンドウではなくデスクトップを制御します。 したがって、「タスクバー」というフレーズではなく、「パネル」という言葉だけを使用することを好みます。



開発を開始


友情は笑顔から始まるので、ほとんどのQtアプリケーションはウィジェットから始まります。 したがって、私たちは伝統から離れることはありません。まず、将来のパネル用のウィジェットを作成します。 今のところ、それはQWidgetから継承された単なるクラスになります。 コンストラクタとデストラクタを使用します。

class Q3Panel : public QWidget { Q_OBJECT public: Q3Panel(QWidget *parent = 0); ~Q3Panel(); };
      
      





このウィジェットを表示すると、空いているウィンドウが#D4D0C8色で痛いほど見慣れた色で表示されます。 これをパネルにするには、ちょっとした魔法が必要で、フラグを1つ変更します。

 setAttribute(Qt::WA_X11NetWmWindowTypeDock, true);
      
      





これをウィジェットコンストラクタに追加するだけです。 したがって、ウィンドウ_NET_WM_WINDOW_TYPE X11のプロパティを値_NET_WM_WINDOW_TYPE_DOCKに設定します。 これは単なるウィンドウではなく、ドックです! 見てみましょう:



画像



これが私の夢のパネルです。 大きすぎる、本当に。 サイズと位置の制御は私たちを傷つけません。

 void Q3Panel::setup(int height) { QRect screen = QApplication::desktop()->screenGeometry(); resize(screen.width(), height); int x, y; x = screen.left(); y = position() == top ? screen.top() : screen.bottom(); move(x, y); }
      
      





このメソッドでは、変数screenで、いわゆるルートウィンドウのジオメトリを取得します 。 resize()メソッドを呼び出すと、パネルの幅が画面の幅に等しくなり、高さが渡されたパラメーターに等しくなります。 変数xには画面の左座標を、yには位置に応じて上部または下部の座標を書き込みます。 次に、座標(x、y)に沿ってパネルを移動します。 おそらく既に推測したとおり、position()は、隠しプロパティを操作するための2つの方法の1つです。

 public: Position position() { return _position; }; void setPosition(Position position) { _position = position; }; private: Position _position;
      
      





これで、パネルのサイズと位置を制御できます。 次のステップに移りましょう。



i3とのチャット


そこで、最も興味深い部分に到達しました。 ウィンドウマネージャーを管理するには、何らかの方法で彼と通信する必要があります。 幸いなことに、すぐに使えるi3はunix socketのようなIPCメソッドをサポートしています。 さらに幸いなことに、Qtには、QLocalSocketという非常に便利なクラスがあります。

しかし、最初に、プロトコルの簡単な説明。 メッセージは次のとおりです。



< > < > < > <>







さあ、まず最初に。 マジックストリングは「i3-ipc」です。 その唯一の目的は、プロトコルのバージョン管理です。 その後に、メッセージサイズが保存される32ビットの数値が続きます。 次に、まったく同じ32ビットタイプのメッセージ、そしてメッセージ自体。 メッセージタイプには次の値を指定できます。



このすべての機能を使用して、多くの機能を備えたパネルを作成できますが、この記事の一部として、デスクトップの表示と切り替えに限定します。 このためには、COMMAND、GET_WORKSPACES、SUBSCRIBEの3種類のメッセージが必要です。 次に、それぞれについて詳しく説明します。 そうそう、私はほとんど忘れていました。 i3が送受信するデータの形式はjsonです。



コマンド :すべての可能なコマンドを説明するわけではありません-それらが多すぎます。 デスクトップを切り替えるには、「ワークスペース番号X」のみが必要です。ここで、Xはデスクトップの番号です。 コマンドへの応答は、「成功」という1つのプロパティのみを含む連想配列です。これはtrueまたはfalseです。 回答例:

 { "success": true }
      
      





GET_WORKSPACES :メッセージ本文は空です。答えはデスクトップのリストで、各デスクトップには次のプロパティが含まれています。



回答例:

 [ { "num": 0, "name": "1", "visible": true, "focused": true, "urgent": false, "rect": { "x": 0, "y": 0, "width": 1280, "height": 800 }, "output": "LVDS1" }, { "num": 1, "name": "2", "visible": false, "focused": false, "urgent": false, "rect": { "x": 0, "y": 0, "width": 1280, "height": 800 }, "output": "LVDS1" } ]
      
      





SUBSCRIBE :i3では、イベントをサブスクライブできます。 合計で2種類のイベントがあります。



イベントメッセージは標準メッセージと完全に同一ですが、唯一の違いはメッセージタイプの最上位ビットが1に設定されていることです。したがって、2つのタイプのイベントのうち、最初のイベントのみに関心があります。 その本体は、1つの文字列プロパティが「変更」された連想配列で、値は「focus」、「init」、「empty」、および「urgent」を取ることができます。 回答例:

 { "change": "focus" }
      
      





プロトコルの知識があれば、クライアントの実装を開始できます。 2つのソケットを使用します。1つはイベントにサブスクライブし、もう1つはコマンドを送信してデスクトップのリストを受信します。 一般的なアルゴリズムは次のとおりです。

  1. 接続中
  2. デスクトップのリストを更新する
  3. ワークスペースイベントを購読する
  4. 待っています
  5. イベントの到着時に、デスクトップのリストを更新します
  6. 後藤4


UNIXソケットの識別子はファイルシステム内のファイルであることを思い出させてください。 名前を取得するには、「I3_SOCKET_PATH」 ルートウィンドウプロパティを読み取るか、i3 --get-socketpathを呼び出します。 私は最も抵抗の少ない道を取りました。

 QString I3Ipc::getSocketPath() { QProcess i3process; i3process.start("i3 --get-socketpath", QIODevice::ReadOnly); if (!i3process.waitForFinished()) { qDebug() << i3process.errorString(); exit(EXIT_FAILURE); } return QString(i3process.readAllStandardOutput()).remove(QChar('\n')); }
      
      





ソケットファイルへのパスがわかったら、サーバーに参加できます。

 void I3Ipc::reconnect() { mainSocket->abort(); eventSocket->abort(); QString socketPath = getSocketPath(); mainSocket->connectToServer(socketPath); eventSocket->connectToServer(socketPath); if (!mainSocket->waitForConnected() || !eventSocket->waitForConnected()) { qDebug() << "Connection timeout!"; exit(EXIT_FAILURE); } subscribe(); }
      
      





データを送信します。

 QByteArray I3Ipc::pack(int type, QString payload) { QByteArray b; QDataStream s(&b, QIODevice::WriteOnly); s.setByteOrder(QDataStream::LittleEndian); s.writeRawData(I3_IPC_MAGIC, qstrlen(I3_IPC_MAGIC)); s << (quint32) payload.size(); s << (quint32) type; s.writeRawData(payload.toAscii().data(), payload.size()); return b; } void I3Ipc::send(int type, QString payload) { send(type, payload, mainSocket); } void I3Ipc::send(int type, QString payload, QLocalSocket* socket) { socket->write(pack(type, payload)); }
      
      





プロトコル部分を注意深く読んだ場合、このコードは明確になります。 データをパックし、ソケットに書き込みます。 I3_IPC_MAGIC-プロトコルを説明するヘッダーファイル<i3 / ipc.h>からの定数。

setByteOrder()に関して:BigEndianはQDataStreamバイトオーダーの標準であり、i3はネイティブでデータを待機します。 データを送信する方法を学びました。今度は、回答を受け入れる方法を学びます。

 void I3Ipc::read() { QLocalSocket *socket = (QLocalSocket*)sender(); if (socket->bytesAvailable() < (int) (qstrlen(I3_IPC_MAGIC) + sizeof(quint32) * 2)) return; QDataStream s(socket); s.setByteOrder(QDataStream::LittleEndian); quint32 msgType, payloadSize; s.skipRawData(qstrlen(I3_IPC_MAGIC)); s >> payloadSize; s >> msgType; while (socket->bytesAvailable() < payloadSize) { if (!socket->waitForReadyRead()) { qDebug() << "Reading timeout!"; exit(EXIT_FAILURE); } } char buf[payloadSize]; s.readRawData(buf, payloadSize); QByteArray jsonPayload(buf, payloadSize); if (msgType >> 31) { if (msgType == I3_IPC_EVENT_WORKSPACE) { emit workspaceEvent(); } } else { if (msgType == I3_IPC_REPLY_TYPE_WORKSPACES) { emit workspaceReply(jsonPayload); } } }
      
      





ここでも、すべてが非常に簡単です。14バイトが蓄積されるまで待機し(6はマジックストリングと2つの4バイト数)、6バイトをスキップしてメッセージサイズを読み取り、変数に入力します。 メッセージ自体を待機し、タイプに応じて適切な信号を送信するだけです。

各イベントの後、json形式のデスクトップの最新リストを受け取ります。 それを「通常の」状態にするために、QJsonライブラリーを使用します。 多くのディストリビューションでは、リポジトリにあり、そうでない場合、誰も自分でそれを組み立てることを気にしません。 したがって、接続します。

 LIBS += -lqjson
      
      





.proファイルおよび

 #include <qjson/parser.h>
      
      





ヘッダーファイル。 QJsonは非常に使いやすいです。

 void Q3Panel::workspaceReplySlot(const QByteArray jsonPayload) { bool ok; QList<QVariant> workspacesList = jsonParser->parse(jsonPayload, &ok).toList(); if (!ok) { qDebug() << "Parser error: " << jsonParser->errorString(); return; } workspaces->clear(); for (int i = 0; i < workspacesList.size(); ++i) { QMap<QString, QVariant> w = workspacesList.at(i).toMap(); workspaces->insert(w.value("num").toUInt(), workspaceInfo(w.value("name").toString(), w.value("focused").toBool(), w.value("urgent").toBool())); } emit updateWorkspacesWidget(workspaces); }
      
      





ワークスペースは、デスクトップに関する情報を格納するハッシュテーブルです。 そのキーはquint16デスクトップ番号であり、値は次の構造です。

 struct workspaceInfo { QString name; bool focused; bool urgent; workspaceInfo(QString _n, bool _f = 0, bool _u = 0) { name = _n; focused = _f; urgent = _u; } };
      
      





ここではすべてがシンプルで明確です。 デスクトップに関する情報を取得し、保存する方法も知っています。 些細なことがあります:特定のデスクトップをクリックして表示するには、ウィンドウマネージャーにコマンドを送信します。 さまざまな方法で移動できます。QHBoxLayoutに基づいてウィジェットを作成することにしました。 複雑なことはありません。キーがデスクトップ番号で、値がWorkspaceButtonボタンへのリンクであるハッシュテーブルを保存します。 WorkspaceButtonはQToolButtonから継承され、サイズ変更とスタイルのポリシーを除き、新しいものを表しません。 デスクトップのリストを更新するたびに、ウィジェットを更新する必要があります。 すべてのボタンを削除して新しいボタンを作成するだけでも可能ですが、少し異なる方法で行います。

 void WorkspacesWidget::updateWorkspacesWidgetSlot(const QHash<qint16, workspaceInfo> *workspaces) { clearLayout(); QHash<qint16, workspaceInfo>::const_iterator wi = workspaces->constBegin(); while (wi != workspaces->constEnd()) { if (buttons->contains(wi.key())) { buttons->value(wi.key())->setFocused(wi.value().focused); } else { addButton(wi.key(), wi.value().focused, wi.value().name); } ++wi; } QHash<qint16, WorkspaceButton*>::const_iterator bi = buttons->constBegin(); QList<qint16> toDelete; while (bi != buttons->constEnd()) { if (!workspaces->contains(bi.key())) { delete bi.value(); toDelete << bi.key(); } else { mainLayout->addWidget(bi.value()); } ++bi; } for (int i = 0; i < toDelete.size(); ++i) { buttons->remove(toDelete.at(i)); } } void WorkspacesWidget::clearLayout() { while (mainLayout->takeAt(0)); } void WorkspacesWidget::addButton(quint16 num, bool focused, QString name) { WorkspaceButton* newButton = new WorkspaceButton(name, focused); connect(newButton, SIGNAL(clicked()), this, SLOT(buttonClickedSlot())); buttons->insert(num, newButton); }
      
      





まず、QHBoxLayoutをクリアしてワークスペースを調べ、現在のデスクトップ用のボタンが既にある場合は、フォーカスされたプロパティを更新します。 ボタンがない場合は追加します。 次に、すべてのボタン要素をチェックして、デスクトップがまだ存在するかどうかを確認します。 また、存在する場合は、QHBoxLayoutを追加します。 そうでない場合は、削除します。 この方法がどれほど最適かはわかりませんが、毎回すべてのボタンを削除して再作成するよりもずっと良かったようです。

それだけです! パネルは次のとおりです。

画像

適切に動作し、デスクトップ、スイッチを表示します。 ただし、これは基本的な機能に過ぎません。機能の点で標準パネルに追いつくために、設定、トレイ、メニュー、時計を追加する必要があります。 もちろん、トピックが興味深いものでない限り、これについては次の記事で説明します。



ソースコードリポジトリ



All Articles