Skip to content

How a frontend shows a session on screen

A frontend is what shows your session to a person: a desktop window today, and perhaps a web page in the future. redsun ships one frontend, for Qt.

The core of redsun knows nothing about windows. A frontend adds a session class to subclass, such as QtSession, which starts the toolkit and shows the views, and a Frontend listing the placements it can show. QtSession lives in redsun.qt because importing it imports the toolkit, which a session without a window doesn't install.

Placements

A view says where it wants to be shown, and the frontend decides whether it can. The core defines only the Placement base class; the Qt frontend defines docks and menus. Point at a placement to read what it needs:

Where Qt puts a view
main windowMenuItem("File")an entry in the File menuHolds a QAction. The menu is made if the window has none of that name. In a session file: {menu: File}.ToolBarItem("Main")an entry in the Main toolbarHolds a QAction. The toolbar is made if the window has none of that name. In a session file: {toolbar: Main}.Dock("top")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Dock("bottom")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Dock("left")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Central()the main areaHolds a QWidget. When several views ask for it, each gets a tab. In a session file: central.Dock("right")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Holds a QAction. The menu is made if the window has none of that name. In a session file: {menu: File}. Holds a QAction. The toolbar is made if the window has none of that name. In a session file: {toolbar: Main}. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}. Holds a QWidget. When several views ask for it, each gets a tab. In a session file: central. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.
main windowMenuItem("File")an entry in the File menuHolds a QAction. The menu is made if the window has none of that name. In a session file: {menu: File}.ToolBarItem("Main")an entry in the Main toolbarHolds a QAction. The toolbar is made if the window has none of that name. In a session file: {toolbar: Main}.Dock("top")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Dock("bottom")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Dock("left")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Central()the main areaHolds a QWidget. When several views ask for it, each gets a tab. In a session file: central.Dock("right")Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.Holds a QAction. The menu is made if the window has none of that name. In a session file: {menu: File}. Holds a QAction. The toolbar is made if the window has none of that name. In a session file: {toolbar: Main}. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}. Holds a QWidget. When several views ask for it, each gets a tab. In a session file: central. Holds a QWidget, in a dock against that edge. Docks given the same group on one edge are tabbed together. In a session file: left, right, top, bottom, or {dock: left, group: name}.

In Frontend.requires, a frontend lists what each placement must hold: Qt asks for a QWidget in a dock or in the centre, and a QAction in a menu or toolbar.

Checks before a build

The session checks every view before it builds anything; point at a check to read what it asks:

What the session checks of a view
which placement?The placement given in the declaration, else the class's. A view class that names no placement is refused, since a component that is shown nowhere is a presenter.does the frontendshow it?The placement must be one the frontend lists in Frontend.requires. Qt lists Central, Dock, MenuItem and ToolBarItem.is the view thetype it needs?Qt needs a QWidget in a dock or in the centre, and a QAction in a menu or a toolbar.build the viewdoes the constructorstart right?Frontend.check_view runs here. Qt asks for a constructor that starts with (name: str, parent: QWidget). The placement given in the declaration, else the class's. A view class that names no placement is refused, since a component that is shown nowhere is a presenter. The placement must be one the frontend lists in Frontend.requires. Qt lists Central, Dock, MenuItem and ToolBarItem. Qt needs a QWidget in a dock or in the centre, and a QAction in a menu or a toolbar. Frontend.check_view runs here. Qt asks for a constructor that starts with (name: str, parent: QWidget).
which placement?The placement given in the declaration, else the class's. A view class that names no placement is refused, since a component that is shown nowhere is a presenter.does the frontendshow it?The placement must be one the frontend lists in Frontend.requires. Qt lists Central, Dock, MenuItem and ToolBarItem.is the view thetype it needs?Qt needs a QWidget in a dock or in the centre, and a QAction in a menu or a toolbar.build the viewdoes the constructorstart right?Frontend.check_view runs here. Qt asks for a constructor that starts with (name: str, parent: QWidget). The placement given in the declaration, else the class's. A view class that names no placement is refused, since a component that is shown nowhere is a presenter. The placement must be one the frontend lists in Frontend.requires. Qt lists Central, Dock, MenuItem and ToolBarItem. Qt needs a QWidget in a dock or in the centre, and a QAction in a menu or a toolbar. Frontend.check_view runs here. Qt asks for a constructor that starts with (name: str, parent: QWidget).

A view that fails a check is left out before anything is built, with a message naming what the frontend shows instead:

Failed to build view 'stray': MyApp.stray asks to be attached as 'Route', which
Qt does not attach. It attaches: Central, Dock, MenuItem, ToolBarItem.

A placement set from a property can only be checked after the view is built; that form is deprecated and goes in 0.16, so set it as a class attribute or in the declaration. A session file's placement words, such as left or central, are read by the frontend's read_placement; the core knows none of them.

The Qt frontend

A Qt view's constructor starts with exactly (name: str, parent: QWidget), and QtSession passes its main window as the parent (How to place a view). QtSession also:

  • runs a widget's slots on the main thread unless a slot names another
  • builds the main window from an app-model Application holding the menus and commands
  • restores the docks where the user left them
  • asks before closing when a component has unsaved changes
  • logs an exception no slot caught and keeps the window open, where the Qt binding would end the process silently
  • closes and deletes every view at shutdown, after delivering waiting signals, so a third-party widget can clean up in its closeEvent

The Qt binding is chosen with QT_API, read by qtpy, never by a session file.

Hook points

A hook acts at a fixed moment of a Qt session's life without changing what the session builds. The five hook points run in this order; point at one to read what it receives:

The hook points of a Qt session
create_applicationmake the QApplicationyourselfCalled with the command-line arguments, when no QApplication exists yet. It returns the QApplication.configure_applicationset a style or a fontCalled with the QApplication, before any view is made.during_buildshow progress whilethe build runsCalled with the QApplication. It wraps the build steps, from services to report.confirm_closeanswer whether thewindow may closeCalled with nothing, when the window is asked to close. It answers in place of the question about unsaved changes.the window is shownand the event loop runsconfigure_main_viewchange the windowbefore it is shownCalled with the main window, in the presentation step, once the views are in place. Called with the command-line arguments, when no QApplication exists yet. It returns the QApplication. Called with the QApplication, before any view is made. Called with the QApplication. It wraps the build steps, from services to report. Called with nothing, when the window is asked to close. It answers in place of the question about unsaved changes. Called with the main window, in the presentation step, once the views are in place.
create_applicationmake the QApplicationyourselfCalled with the command-line arguments, when no QApplication exists yet. It returns the QApplication.configure_applicationset a style or a fontCalled with the QApplication, before any view is made.during_buildshow progress whilethe build runsCalled with the QApplication. It wraps the build steps, from services to report.confirm_closeanswer whether thewindow may closeCalled with nothing, when the window is asked to close. It answers in place of the question about unsaved changes.the window is shownand the event loop runsconfigure_main_viewchange the windowbefore it is shownCalled with the main window, in the presentation step, once the views are in place. Called with the command-line arguments, when no QApplication exists yet. It returns the QApplication. Called with the QApplication, before any view is made. Called with the QApplication. It wraps the build steps, from services to report. Called with nothing, when the window is asked to close. It answers in place of the question about unsaved changes. Called with the main window, in the presentation step, once the views are in place.

Install hooks shows how. A session with no frontend calls no hook points, so it refuses a hook declared on it.

Choosing the frontend from a file

A session file names its frontend by the name the frontend is registered under:

frontend: qt

Session.from_config builds on the class registered under that name, or, without a frontend key, on the class you called it on. Frontends are registered as entry points, the packaging feature through which an installed package announces what it offers, in the redsun.frontends group:

[project.entry-points."redsun.frontends"]
qt = "redsun.qt:QtSession"

Another package registers its session class the same way, with no change to redsun. A component asking for SessionConfig finds the registered name in its frontend field, or None.

Writing a frontend

A new frontend defines its placements, its Frontend and a session class that shows the views (How to write a frontend). It may override Frontend.check_view, to refuse a view class it can't build, and Session.view_arguments, to add arguments to every view's constructor.

What a frontend provides

what where the Qt frontend
the placements it shows Frontend.requires Central, Dock, MenuItem, ToolBarItem
the thread its views' slots run on Frontend.thread_of the main thread, for a QWidget
the delivery of the calls held for that thread the session's run psygnal.qt.start_emitting_from_queue

The thread and the delivery matter because presenters on other threads call a view's slots, while most toolkits allow one thread only. Step through such a call:

A presenter calls a view from another thread

A presenter working on a worker thread emits a signal connected to a view's slot.

presenter emitson a worker thread
presenter emitson a worker thread

The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget. The call waits in a queue.

presenter emitson a worker threadthe call waitsin a queueThe slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.
presenter emitson a worker threadthe call waitsin a queueThe slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.

The session calls psygnal.emit_queued from the toolkit's event loop, as often as the views should follow the presenters.

presenter emitson a worker threadthe call waitsin a queueThe slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.the event loop callspsygnal.emit_queuedThe session calls it from the toolkit's event loop, as often as the views should follow the presenters.The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget. The session calls it from the toolkit's event loop, as often as the views should follow the presenters.
presenter emitson a worker threadthe call waitsin a queueThe slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.the event loop callspsygnal.emit_queuedThe session calls it from the toolkit's event loop, as often as the views should follow the presenters.The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget. The session calls it from the toolkit's event loop, as often as the views should follow the presenters.

The queued call runs, and the view's slot runs on the main thread, where Qt allows it.

presenter emitson a worker threadthe call waitsin a queueThe slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.the view's slot runson the main threadthe event loop callspsygnal.emit_queuedThe session calls it from the toolkit's event loop, as often as the views should follow the presenters.The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget. The session calls it from the toolkit's event loop, as often as the views should follow the presenters.
presenter emitson a worker threadthe call waitsin a queueThe slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget.the view's slot runson the main threadthe event loop callspsygnal.emit_queuedThe session calls it from the toolkit's event loop, as often as the views should follow the presenters.The slot names no thread and neither does its class, so the session asks Frontend.thread_of, which answers the main thread for a QWidget. The session calls it from the toolkit's event loop, as often as the views should follow the presenters.

Coroutine slots need nothing from the frontend, because every session sets the backend that runs them when it starts its runtime. That's why a frontend's start_runtime must call the start_runtime it overrides.