Center a QWidget/QLayout ignoring other widgets

I am making a header in my app with this format:

||Icon| Page QueTueDue v0.6-b3 |

  1. Icon (self.header_menu)
  2. Page name (self.header_page_label)
  3. App name (self.header_label)
  4. App version (self.header_sub_label)

My problem is that the app name and version are offset from the center of the window (not the header layout). I could solve this with a QSpacerItem/QWidget with a certain width on one end, however the page name varies, and so does its width. How can I center the app name and version without the other widgets in the layout affecting it?

Code snippets:
Header definition and code in MainWindow:

        self.header_layout = QHBoxLayout()
        self.header_layout.setSpacing(8)
        self.header_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.header_menu_layout = QHBoxLayout()
        self.header_title_layout = QHBoxLayout()

        self.header_menu = QMenu()
        self.header_menu_file = QAction("File")
        self.header_menu_file.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_file)
        self.header_menu_add = QAction("Add")
        self.header_menu_add.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_add)
        self.header_menu_remove = QAction("Remove")
        self.header_menu_remove.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_remove)
        self.header_menu_mark_off = QAction("Mark Off")
        self.header_menu_mark_off.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_mark_off)
        self.header_menu_edit = QAction("Edit")
        self.header_menu_edit.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_edit)

        self.header_menu_button = QPushButton()
        self.header_menu_button.setIcon(QIcon(os.path.join(ICON_PATH, f"header_menu_icon_{THEME}.png")))
        self.header_menu_button.setIconSize(QSize(24, 24))
        self.header_menu_button.setFixedSize(48, 32)
        self.header_menu_button.setFlat(True)
        self.header_menu_button.setMenu(self.header_menu)

        self.header_page_label = QLabel("Tasks")
        self.header_page_label.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
        self.header_page_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
        self.header_page_label.setFont(QFont(self.families[4][0]))

        self.header_menu_layout.addWidget(self.header_menu_button)
        self.header_menu_layout.addWidget(self.header_page_label)
        self.header_menu_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

        self.header_label = QLabel("QueTueDue")
        self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.header_label.setContentsMargins(0, 4, 0, 4)
        self.header_label.setFont(QFont(self.families[4], 12))

        self.header_sub_label = QLabel(f"{__version__}")
        self.header_sub_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.header_sub_label_palette = self.header_sub_label.palette()
        self.header_sub_label_palette.setColor(QPalette.ColorRole.WindowText, QColor(50, 50, 50))
        self.header_sub_label.setPalette(self.header_sub_label_palette)
        self.header_sub_label.setFont(QFont(self.families[4], 10))

        self.header_title_spacer = QSpacerItem(
            self.header_menu_button.width() + self.header_page_label.width() + 4,
            10,
            QSizePolicy.Policy.Minimum,
            QSizePolicy.Policy.Minimum,
        )
        self.header_title_spacer.setAlignment(Qt.AlignmentFlag.AlignRight)

        self.header_title_layout.addWidget(self.header_label)
        self.header_title_layout.addWidget(self.header_sub_label)
        self.header_title_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)

        self.header_layout.addLayout(self.header_menu_layout)
        self.header_layout.addLayout(self.header_title_layout)
        self.header_layout.addItem(self.header_title_spacer)

        self.header_separator = separator("h")
        self.window_layout.addLayout(self.header_layout)
        self.window_layout.addWidget(self.header_separator)

The function update_header_spacer to recalculate the width of the spacer:

    def update_header_spacer(self):
        """Calculate and resize the width on the header spacer
        (header_title_spacer)
        """
        print(self.header_menu_button.width())
        print(self.header_page_label.width())
        print(self.header_menu_button.width() + self.header_page_label.width() + 4)
        width = self.header_menu_button.width() + self.header_page_label.width() + 4
        self.header_title_spacer.changeSize(
            width,
            1,
            QSizePolicy.Policy.Fixed,
            QSizePolicy.Policy.Minimum,
        )
        self.header_layout.invalidate()

And the change_page function to change the current widget on the main stack layout which makes up the main window layout (minus the header and toolbar):

    def change_page(self, page, header_label, item=False):
        """Changes the current widget on the stack layout
        (self.stack_layout).
        """
        if self.stack_layout.currentWidget() == page:
            self.stack_layout.setCurrentWidget(self.main_page)
            self.header_page_label.setText("Tasks")
            return

        self.stack_layout.setCurrentWidget(page)
        self.header_page_label.setText(header_label)
        self.update_header_spacer()

Finally, MainWindow:

class MainWindow(QMainWindow):
    """The main app window containing all the categories, tasks toolbars
    and more.
    """

    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle("QueTueDue")
        self.setWindowIcon(QIcon(os.path.join(ICON_PATH, "logo.svg")))
        self.setMinimumWidth(800)

        # Define fonts
        self.fonts = [
            "AdwaitaMono-Regular.ttf",
            "AdwaitaMono-Bold.ttf",
            "AdwaitaMono-Italic.ttf",
            "AdwaitaMono-BoldItalic.ttf",
            "AdwaitaSans-Regular.ttf",
            "AdwaitaSans-Italic.ttf",
        ]
        self.font_dialogs = []
        self.families = []
        for self.font in self.fonts:
            self.fontfile = os.path.join(FONT_PATH, self.font)
            if not os.path.exists(self.fontfile):
                process.startDetached("python", [os.path.join(ROOT_PATH, "file_checker.py")])
                sys.exit()
            else:
                id = QFontDatabase.addApplicationFont(self.fontfile)
                self.families.append(QFontDatabase.applicationFontFamilies(id))

        # System tray icon
        self.icon = QIcon(os.path.join(ICON_PATH, "logo.svg"))

        self.tray = QSystemTrayIcon()
        self.tray.setIcon(self.icon)
        self.tray.setVisible(True)
        self.tray.show()

        self.tray_menu = QMenu()

        self.title_action = QAction("𝗤𝘂𝗲𝗧𝘂𝗲𝗗𝘂𝗲")
        self.title_action.setIcon(QIcon(os.path.join(ICON_PATH, "logo.svg")))

        self.open_app_tray_action = QAction("𝗢𝗽𝗲𝗻 𝗳𝘂𝗹𝗹 𝗮𝗽𝗽")
        self.open_app_tray_action.triggered.connect(self.open_app)
        self.open_app_tray_action.setIcon(QIcon(os.path.join(ICON_PATH, f"open_app_icon_{THEME}.png")))

        self.quit_app_tray_action = QAction("𝗤𝘂𝗶𝘁 𝗤𝘂𝗲𝗧𝘂𝗲𝗗𝘂𝗲")
        self.quit_app_tray_action.triggered.connect(self.quit_app)
        self.quit_app_tray_action.setIcon(QIcon(os.path.join(ICON_PATH, f"quit_app_icon_{THEME}.png")))

        self.tray.setContextMenu(self.tray_menu)

        # Toolbar
        self.toolbar = QToolBar("Utilities")
        self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)

        self.add_action = QAction(QIcon(os.path.join(ICON_PATH, f"add_task_icon_{THEME}.png")), "Add", self)
        self.add_action.setStatusTip("Add a new task")
        self.add_action.triggered.connect(lambda: self.change_page(self.add_page, "Add a Task", self.add_action))

        self.del_action = QAction(QIcon(os.path.join(ICON_PATH, f"del_task_icon_{THEME}.png")), "Remove", self)
        self.del_action.setStatusTip("Remove a task")
        self.del_action.triggered.connect(lambda: self.change_page(self.del_page, "Remove a Task", self.del_action))

        self.mark_all_as_done_action = QAction(
            QIcon(os.path.join(ICON_PATH, f"mark_all_as_done_icon_{THEME}.png")), "Mark all as Done", self
        )
        self.mark_all_as_done_action.setStatusTip("Mark all items off as Done")
        self.mark_all_as_done_action.triggered.connect(
            lambda: self.change_page(self.mark_off_page, "Mark all as Done", self.mark_all_as_done_action)
        )

        self.del_done_action = QAction(
            QIcon(os.path.join(ICON_PATH, f"del_done_icon_{THEME}.png")), "Remove Done", self
        )
        self.del_done_action.setStatusTip("Remove all tasks marked as done")
        self.del_done_action.triggered.connect(
            lambda: self.change_page(self.del_done_page, "Remove Done Tasks", self.del_done_action)
        )

        self.del_all_action = QAction(
            QIcon(os.path.join(ICON_PATH, f"del_all_icon_{THEME}.png")), "Remove ALL Items", self
        )
        self.del_all_action.setStatusTip("Remove ALL ITEMS PERMANENTLY")
        self.del_all_action.triggered.connect(
            lambda: self.change_page(self.del_all_page, "Remove ALL Tasks", self.del_all_action)
        )

        self.toolbar.addAction(self.add_action)
        self.toolbar.addAction(self.del_action)
        self.toolbar.addAction(self.mark_all_as_done_action)
        self.toolbar.addAction(self.del_done_action)
        self.toolbar.addAction(self.del_all_action)

        # Main layouts
        self.window_layout = QVBoxLayout()
        self.stack_layout = QStackedLayout()
        self.main_layout = QVBoxLayout()
        self.tasks_layout = QHBoxLayout()
        self.todo_layout = QVBoxLayout()
        self.todo_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.in_prog_layout = QVBoxLayout()
        self.in_prog_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.done_layout = QVBoxLayout()
        self.done_layout.setAlignment(Qt.AlignmentFlag.AlignTop)

        # Widgets
        self.header_layout = QHBoxLayout()
        self.header_layout.setSpacing(8)
        self.header_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.header_menu_layout = QHBoxLayout()
        self.header_title_layout = QHBoxLayout()

        self.header_menu = QMenu()
        self.header_menu_file = QAction("File")
        self.header_menu_file.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_file)
        self.header_menu_add = QAction("Add")
        self.header_menu_add.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_add)
        self.header_menu_remove = QAction("Remove")
        self.header_menu_remove.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_remove)
        self.header_menu_mark_off = QAction("Mark Off")
        self.header_menu_mark_off.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_mark_off)
        self.header_menu_edit = QAction("Edit")
        self.header_menu_edit.setFont(QFont(self.families[4][0]))
        self.header_menu.addAction(self.header_menu_edit)

        self.header_menu_button = QPushButton()
        self.header_menu_button.setIcon(QIcon(os.path.join(ICON_PATH, f"header_menu_icon_{THEME}.png")))
        self.header_menu_button.setIconSize(QSize(24, 24))
        self.header_menu_button.setFixedSize(48, 32)
        self.header_menu_button.setFlat(True)
        self.header_menu_button.setMenu(self.header_menu)

        self.header_page_label = QLabel("Tasks")
        self.header_page_label.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
        self.header_page_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
        self.header_page_label.setFont(QFont(self.families[4][0]))

        self.header_menu_layout.addWidget(self.header_menu_button)
        self.header_menu_layout.addWidget(self.header_page_label)
        self.header_menu_layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

        self.header_label = QLabel("QueTueDue")
        self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.header_label.setContentsMargins(0, 4, 0, 4)
        self.header_label.setFont(QFont(self.families[4], 12))

        self.header_sub_label = QLabel(f"{__version__}")
        self.header_sub_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.header_sub_label_palette = self.header_sub_label.palette()
        self.header_sub_label_palette.setColor(QPalette.ColorRole.WindowText, QColor(50, 50, 50))
        self.header_sub_label.setPalette(self.header_sub_label_palette)
        self.header_sub_label.setFont(QFont(self.families[4], 10))

        self.header_title_spacer = QSpacerItem(
            self.header_menu_button.width() + self.header_page_label.width() + 4,
            10,
            QSizePolicy.Policy.Minimum,
            QSizePolicy.Policy.Minimum,
        )
        self.header_title_spacer.setAlignment(Qt.AlignmentFlag.AlignRight)

        self.header_title_layout.addWidget(self.header_label)
        self.header_title_layout.addWidget(self.header_sub_label)
        self.header_title_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)

        self.header_layout.addLayout(self.header_menu_layout)
        self.header_layout.addLayout(self.header_title_layout)
        self.header_layout.addItem(self.header_title_spacer)

        self.header_separator = separator("h")
        self.window_layout.addLayout(self.header_layout)
        self.window_layout.addWidget(self.header_separator)

        self.tasks_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.tasks_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.tasks_layout.addLayout(self.todo_layout)
        self.tasks_layout.addWidget(separator("v"))
        self.tasks_layout.addLayout(self.in_prog_layout)
        self.tasks_layout.addWidget(separator("v"))
        self.tasks_layout.addLayout(self.done_layout)

        self.tasks_layout.setAlignment(Qt.AlignmentFlag.AlignTop)

        self.in_prog_layout.setAlignment(Qt.AlignmentFlag.AlignTop)

        self.done_layout.setAlignment(Qt.AlignmentFlag.AlignTop)

        self.main_layout.addLayout(self.tasks_layout)

        self.header_spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
        self.main_layout.addItem(self.header_spacer)

        self.todo_header = QLabel("To-Do")
        try:
            self.todo_header.setFont(QFont(self.families[0], 32))
        except IndexError:
            self.todo_header.setFont(QFont("", 32))
        self.todo_layout.addWidget(self.todo_header)
        self.todo_header.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)

        self.in_prog_header = QLabel("In Prog.")
        try:
            self.in_prog_header.setFont(QFont(self.families[0], 32))
        except IndexError:
            self.in_prog_header.setFont(QFont("", 32))
        self.in_prog_layout.addWidget(self.in_prog_header)
        self.in_prog_header.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)

        self.done_header = QLabel("Done :)")
        try:
            self.done_header.setFont(QFont(self.families[0], 32))
        except IndexError:
            self.done_header.setFont(QFont("", 32))
        self.done_layout.addWidget(self.done_header)
        self.done_header.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)

        self.load_checkboxes()
        container = QWidget()
        container.setLayout(self.main_layout)

        self.setCentralWidget(container)

        # Pages
        self.main_page = QWidget()
        self.main_page.setLayout(self.main_layout)
        self.stack_layout.addWidget(self.main_page)

        self.add_page = AddWindow(self)
        self.stack_layout.addWidget(self.add_page)

        self.del_page = DelWindow(self)
        self.stack_layout.addWidget(self.del_page)

        self.mark_off_page = MarkAllAsDoneWindow(self)
        self.stack_layout.addWidget(self.mark_off_page)

        self.del_done_page = DelDoneWindow(self)
        self.stack_layout.addWidget(self.del_done_page)

        self.del_all_page = DelAllWindow(self)
        self.stack_layout.addWidget(self.del_all_page)

        self.change_page(self.main_page, "Tasks", False)
        self.window_layout.addLayout(self.stack_layout)

        self.window_wrapper = QWidget()
        self.window_wrapper.setLayout(self.window_layout)
        self.setCentralWidget(self.window_wrapper)

    def clear_layout(self, layout, start):
        """Removes all widgets in a given layout."""
        for i in reversed(range(start, layout.count())):
            task = layout.takeAt(i)
            widget = task.widget()
            if widget:
                widget.deleteLater()

    def load_checkboxes(self):
        """Clears layouts and system tray list and re-adds checkboxes
        from to-do.txt.
        """

        # Clear checkboxes
        self.clear_layout(self.todo_layout, 1)
        self.clear_layout(self.in_prog_layout, 1)
        self.clear_layout(self.done_layout, 1)

        # Clear and re-add static tray tasks (except open full app).
        self.tray_menu.clear()

        self.tray_actions = []
        self.tray_menu.addAction(self.title_action)
        self.tray_menu.addSeparator()

        with open(TODO_PATH, "r") as f:
            checkbox = ""
            for line in f.readlines():
                if not line.strip():
                    continue

                self.blockSignals(True)

                task_text = line[1:].strip()
                checkbox = QCheckBox(task_text)

                max_width = max(50, int((self.width() / 3) - self.toolbar.width()))
                checkbox.setMaximumWidth(max_width)
                checkbox.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

                try:
                    checkbox.setFont(QFont(self.families[4]))
                except IndexError:
                    continue

                # Add checkboxes and tray tasks depending on category
                if line.startswith("t") or line.startswith("i"):
                    checkbox.setCheckState(Qt.CheckState.Unchecked)
                    self.todo_layout.addWidget(checkbox)
                    checkbox_tray_action = QAction(task_text)
                    self.tray_actions.append(checkbox_tray_action)
                    self.tray_menu.addAction(checkbox_tray_action)
                elif line.startswith("i"):
                    checkbox.setCheckState(Qt.CheckState.PartiallyChecked)
                    self.in_prog_layout.addWidget(checkbox)
                elif line.startswith("d"):
                    checkbox.setCheckState(Qt.CheckState.Checked)
                    self.done_layout.addWidget(checkbox)
                else:
                    continue

                self.blockSignals(False)

                checkbox.setProperty("task", task_text)
                checkbox.setTristate(True)
                checkbox.stateChanged.connect(lambda state, cb=checkbox: self.moveCheckbox(cb, state))

        # Add final static tray tasks and refresh the tray context menu
        self.tray_menu.addSeparator()
        self.tray_menu.addAction(self.open_app_tray_action)
        self.tray_menu.addAction(self.quit_app_tray_action)
        self.tray.setContextMenu(self.tray_menu)

    def closeEvent(self, e):
        """Override the close signal and hide the window instead of
        closing it, meaning that the process is not killed and the
        system tray icon remains.
        """
        if HIDE_WHEN_CLOSED == "True":
            e.ignore()
            self.hide()

    def open_popup_window(self, WindowInstance, checked=False):
        """Open the specified popup window (WindowInstance). This is used for the
        toolbar and button actions."""
        self.w = WindowInstance(self)
        self.w.show()

    def open_app(self):
        """Open the main app when the Open full app system tray context
        menu option is triggered.
        """
        self.show()

    def quit_app(self):
        """Quit the whole process when the Quit QueTueDue system tray
        context menu option is triggered.
        """
        sys.exit()

    def moveCheckbox(self, cb, state):
        """Deletes specified task (cb) from to-do.txt and re-adds it
        with a new prefix based on the state (state) of the checkbox.
        """
        text = cb.text()
        prefixes = (f"t{text}", f"i{text}", f"d{text}")
        if state == 0:
            self.todo_layout.addWidget(cb)

            with open(TODO_PATH, "r") as f:
                lines = f.readlines()

            lines = [line for line in lines if not any(line.startswith(p) for p in prefixes)]
            lines = [line for line in lines if line.strip()]
            with open(TODO_PATH, "w", encoding="utf-8") as f:
                f.writelines(lines)
            with open(TODO_PATH, "a", encoding="utf-8") as f:
                f.write(f"\nt{text}")

        elif state == 1:
            max_width = max(50, int((self.width() / 3) - self.toolbar.width()))
            cb.setMaximumWidth(max_width)
            cb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
            self.in_prog_layout.addWidget(cb)

            with open(TODO_PATH, "r") as f:
                lines = f.readlines()

            lines = [line for line in lines if not any(line.startswith(p) for p in prefixes)]
            lines = [line for line in lines if line.strip()]

            with open(TODO_PATH, "w", encoding="utf-8") as f:
                f.writelines(lines)
            with open(TODO_PATH, "a", encoding="utf-8") as f:
                f.write(f"\ni{text}")

        elif state == 2:
            max_width = max(50, int((self.width() / 3) - self.toolbar.width()))
            cb.setMaximumWidth(max_width)
            cb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
            self.done_layout.addWidget(cb)

            with open(TODO_PATH, "r") as f:
                lines = f.readlines()

            lines = [line for line in lines if not any(line.startswith(p) for p in prefixes)]

            with open(TODO_PATH, "w", encoding="utf-8") as f:
                f.writelines(lines)
            with open(TODO_PATH, "a", encoding="utf-8") as f:
                f.write(f"\nd{text}")

    def resizeEvent(self, event):
        """Override the default resizeEvent to calculate and adjust
        checkbox label widths accordingly.
        """
        self.load_checkboxes()
        self.update_header_spacer()
        super().resizeEvent(event)

    def update_header_spacer(self):
        """Calculate and resize the width on the header spacer
        (header_title_spacer)
        """
        print(self.header_menu_button.width())
        print(self.header_page_label.width())
        print(self.header_menu_button.width() + self.header_page_label.width() + 4)
        width = self.header_menu_button.width() + self.header_page_label.width() + 4
        self.header_title_spacer.changeSize(
            width,
            1,
            QSizePolicy.Policy.Fixed,
            QSizePolicy.Policy.Minimum,
        )
        self.header_layout.invalidate()

    def change_page(self, page, header_label, item=False):
        """Changes the current widget on the stack layout
        (self.stack_layout).
        """
        if self.stack_layout.currentWidget() == page:
            self.stack_layout.setCurrentWidget(self.main_page)
            self.header_page_label.setText("Tasks")
            return

        self.stack_layout.setCurrentWidget(page)
        self.header_page_label.setText(header_label)
        self.update_header_spacer()