103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
"""Раскладка панелей RViz: строка «QMainWindow State» для конфига без Qt.
|
||
|
||
Где и какой ширины стоят панели, RViz хранит в конфиге сериализацией Qt —
|
||
шестнадцатеричной строкой `QMainWindow::saveState()`. Руками её не набрать, а
|
||
Qt с привязками для Python в образе нет (и из WSL нет выхода к apt). Формат
|
||
при этом простой и стабильный, он собран здесь по исходникам Qt 5.15
|
||
(`QDockAreaLayout::saveState`, `QDockAreaLayoutInfo::saveState`): маркер
|
||
версии, маркер доков, по каждой зоне — размер и список панелей с именем,
|
||
флагами, положением и размером. Панель находится по имени объекта, а у RViz
|
||
это имя панели или дисплея («Displays», «Мозг мухи»).
|
||
|
||
python tools/rviz_layout.py
|
||
|
||
печатает строки для обоих конфигов: `config/flyguard.rviz` (панель Displays
|
||
слева) и `config/flyguard_brain.rviz` (плюс схема мозга справа во всю высоту).
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import struct
|
||
|
||
QWIDGETSIZE_MAX = 16777215
|
||
LEFT, RIGHT = 0, 1 # QInternal::DockPosition
|
||
VERTICAL = 2 # Qt::Vertical — панели в зоне идут столбиком
|
||
CORNERS = (4, 4, 8, 8) # углы окна: верхняя, верхняя, нижняя, нижняя зона
|
||
|
||
|
||
def _i32(v: int) -> bytes:
|
||
return struct.pack(">i", v)
|
||
|
||
|
||
def _qstring(s: str) -> bytes:
|
||
b = s.encode("utf-16-be")
|
||
return struct.pack(">I", len(b)) + b
|
||
|
||
|
||
def state(areas, central: int, height: int) -> str:
|
||
"""areas — [(зона, ширина, [имя панели, ...])], central — ширина трёхмерного вида."""
|
||
out = _i32(0xff) + _i32(0) + b"\xfd" + _i32(len(areas))
|
||
for pos, width, docks in areas:
|
||
out += _i32(pos) + _i32(width) + _i32(height)
|
||
out += b"\xfc" + bytes([VERTICAL]) + _i32(len(docks))
|
||
step = height // len(docks)
|
||
for k, name in enumerate(docks):
|
||
out += b"\xfb" + _qstring(name) + b"\x01" # видима, не плавает
|
||
out += _i32(k * step) + _i32(step) + _i32(100) + _i32(QWIDGETSIZE_MAX)
|
||
out += _i32(central) + _i32(height)
|
||
for c in CORNERS:
|
||
out += _i32(c)
|
||
return out.hex()
|
||
|
||
|
||
def parse(hexs: str):
|
||
"""Обратный разбор — проверка, что строка собрана без хвостов."""
|
||
b = bytes.fromhex(hexs)
|
||
p = 0
|
||
|
||
def rd(fmt):
|
||
nonlocal p
|
||
v = struct.unpack_from(fmt, b, p)
|
||
p += struct.calcsize(fmt)
|
||
return v
|
||
|
||
assert rd(">ii") == (0xff, 0) and rd(">B") == (0xfd,)
|
||
n, = rd(">i")
|
||
areas = []
|
||
for _ in range(n):
|
||
pos, w, h = rd(">iii")
|
||
marker, _, cnt = rd(">BBi")
|
||
assert marker == 0xfc
|
||
docks = []
|
||
for _ in range(cnt):
|
||
assert rd(">B") == (0xfb,)
|
||
ln, = rd(">I")
|
||
docks.append(b[p:p + ln].decode("utf-16-be"))
|
||
p += ln
|
||
rd(">Biiii")
|
||
areas.append((pos, w, h, docks))
|
||
central = rd(">ii")
|
||
rd(">iiii")
|
||
assert p == len(b), "строка не разобрана до конца"
|
||
return areas, central
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser(description=__doc__,
|
||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
ap.add_argument("--width", type=int, default=1880, help="ширина окна, как Width в конфиге")
|
||
ap.add_argument("--height", type=int, default=900, help="высота зоны панелей")
|
||
ap.add_argument("--left", type=int, default=300, help="ширина панели Displays")
|
||
ap.add_argument("--brain", type=int, default=820, help="ширина схемы мозга")
|
||
a = ap.parse_args()
|
||
plain = state([(LEFT, a.left, ["Displays"])], a.width - a.left - 4, a.height)
|
||
left = a.left - 20
|
||
brain = state([(LEFT, left, ["Displays"]), (RIGHT, a.brain, ["Мозг мухи"])],
|
||
a.width - left - a.brain - 8, a.height)
|
||
for name, s in (("flyguard.rviz", plain), ("flyguard_brain.rviz", brain)):
|
||
parse(s)
|
||
print(f"{name}:\n QMainWindow State: {s}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|