Shotwell Architecture Overview: User Interface
Shotwell’s user interface is based on a concept of pages. Each page presents a particular view to the user of a collection of objects (one or more) which may be examined or manipulated. For example, one page may show all the photos available in the database (or library). Another page may show only the photos available in an event, which is a time-based grouping of photos. Another page may display thumbnails of all the photos available on a camera, while another shows a single photo scaled to fit the window.
The pages are contained (or owned) by an application window. It is possible for Shotwell to have multiple top-level windows (notably another window is spawned for fullscreen mode). However, the user sees Shotwell as a single-window application.
Shotwell can execute in two modes: Library and Direct-edit (also known as Photo Viewer). Library is the most common use case and is described first. Most but not all of the design of Library mode pertain to Direct-edit mode.
General User Interface Classes and Concepts
All pages descend from a common base class, Page (which is a Gtk.ScrolledWindow). Page is abstract; it is up to its children to determine exactly how they will display their view. Page does not even supply a common drawing widget (such as a Gtk.DrawingArea). The subclasses need to determine that.
Page has four primary responsibilities:
- provide a generic interface so all pages may be manipulated generically;
- provide its subclasses with helper methods to ease trapping GTK/GDK events;
- provide helper functions for its subclasses to generate GTK widgets;
and supply a ViewCollection where all common data objects are held by the Page.
An example of the first is Page.switched_to () / Page.switchingfrom and Page.onshift_pressed (). These simplify decoding GDK event structures. Page also generates method calls for events not directly provided by GDK. For example, Page uses a timer to determine when the user has finished (or paused) resizing a window.
The third point is vital to understanding Page’s role in the user interface. Each subclass of Page is responsible for providing its own Gtk.Toolbar and Gtk.MenuBar. This allows the child class to tailor the UI elements to the operations available on that view. However, the Page is only responsible for the layout of its main viewing window and the toolbar; it is up to the controlling window to place the menu bar and lay out the Page with the other widgets (for example, the sidebar). To assist with building these UI elements, Page provides several helper functions with ui in their name.
All Pages have a ViewCollection which can be queried and manipulated by the controller (or anyone else). ViewCollections are explained in Data Structures.
PageWindow and AppWindow
Any top-level window which holds one or more Pages is a descendant of PageWindow (which is a Gtk.!Window). PageWindow is a thin class; its primary concerns are to enforce the notion of a current page (the one currently displayed to the user, and therefore the only Page that should be doing any heavy-duty drawing) and to hook up some window-wide GDK events to the Page itself.
AppWindow (which is a PageWindow) is a heavier base class. There can be only one AppWindow in a process. AppWindow provides several common Gtk.ActionEntries that either all Pages or most Pages need (such as Help -> About and File -> Quit). AppWindow also understands how to create FullscreenWindows (which are themselves PageWindows). It also provides helper functions for displaying Yes / No and Error message dialogs.
Library mode is Shotwell’s primary face. Here users can import photos from cameras, organize photos in various ways, and perform non-destructive edits. The user interface is characterized as a sidebar on the left side (with a basic information widget below it) and a display area on the right with a toolbar beneath it.
The user selects the page to display from the sidebar, a Gtk.TreeView which presents the pages as a hierarchy. Note that in code the pages have no notion of hierarchy (their parent or children); their concern is interacting with the user as the current view. It’s the LibraryWindow that understands this hierarchy. (This may change in the future if a feature requires it. Currently, there is no need.)
The application window in library mode is LibraryWindow (an AppWindow). In addition to maintaining the Sidebar, LibraryWindow also maintains references to all Pages in the systems. It uses a Gtk.!Notebook to switch between Pages (although this is entirely opaque to external callers and may change in the future). Thus, LibraryWindow acts as the UI glue between the Sidebar and the Pages. (Note that Pages can have LibraryWindow switch to other Pages, which must be reflected in the Sidebar).
Like AppWindow, LibraryWindow also provides Gtk.ActionEntries common to most pages, including sort order options and a toggle to turn on and off the Basic Information pane (which is also held by LibraryWindow).
LibraryWindow is a drag destination. The user can drag files from another application onto any part of the top-level window and LibraryWindow will initiate a photo import. Note that certain Pages within the system are drag sources, but this is on a page-by-page basis.
LibraryWindow monitors the CameraTable object to be notified when cameras are attached to the system. It dynamically creates ImportPages for attached cameras and destroys those pages when the camera is detached.
LibraryWindow uses a Page stub scheme. On large collections, creating Pages for each event and event directory (and, in the future, albums and other views) is expensive, both in terms of memory and startup time. LibraryWindow uses PageStub (and subclasses) which instantiate the appropriate Page on-demand (typically when the user clicks on the page in the sidebar). Currently the instantiated Pages are held in memory until the application exits. This may change in the future.
The Sidebar object is a Gtk.TreeView with an internally-managed Gtk.TreeModel. It is "dumb", that is, although it displays all the Pages in the system in a heirarchical format, it does not understand much about those Pages, and doesn’t even switch to them. As explained above, that’s the job of LibraryWindow. It does this by monitoring Sidebar for cursor changes and switching to that page via the Gtk.!Notebook.
In fact, Sidebar doesn’t deal with Pages, it deals with SidebarPages, which is an interface. Page and PageStub are both SidebarPages. This allows for both of them to be attached to the Sidebar (not all Pages are stubbed at this time, for example, the main Photos page, i.e. LibraryPage). SidebarPages may be attached to the Sidebar in any number of ways. They can be added as parents (they reside on the top-level of the sidebar), or as children, in which case there are a number of methods to insert children alongside their siblings in auto-sorted or manually-sorted order. Also, a grouping row may be added to the Sidebar; this is a top-level item in the sidebar that is not clickable, but rather exists to organize its children. (Cameras is one example of a grouping row.)
The Sidebar has a number of limitations, and currently most ordering in the sidebar is done manually in the code. A better solution would be for the Sidebar to allow for multiple models (i.e. sorting objects): One for the top-level items (it may be as simple as assigning each an ordinal to sort by), and a model for each branch (which may be as simple as order-added or alphabetical to more sophisticated models for a full tree sort, necessary for Events). This is work that needs to be done soon, as managing the Sidebar is becoming painful.
New Page Types
To create a new Page (or page type, i.e. one that can show any Tag), you need to (a) write a subclass of Page (preferably CollectionPage if LibraryPhotos attached to LibraryPhotoSourceCollection, CheckerboardPage for other items), (b) write a PageStub that’s generated by the Page in a factory method, add it to the Sidebar via one of its attachment methods, and (d) hook it up in the LibraryWindow.onsidebarcursor_changed () method, to switch to that page when selected. If the page should come and go depending on the availability of the objects its displaying (such as OfflinePage, which only appears when files are missing), then LibraryWindow should monitor the appropriate objects (or their collection and enable / disable the page appropriately. (OfflinePage is a good model for this.)
This is definitely one area that could use some automation, especially as more Pages are added to Shotwell. Since each page’s requirements are different, it’s difficult to genericize this a single way; some flexibility would need to be built into this automation, to allow for the pages to come and go.
CheckerboardPage, CheckerboardLayout, and CollectionPage
CheckerboardPage is a subclass of Page that, with CheckerboardLayout, displays thumbnails of photos (either on disk or stored on a camera) in a grid pattern across the page. CheckerboardPage owns its CheckerboardLayout, which is a Gtk.DrawingArea subclasses added to the Page container.
CheckerboardLayout in turn works with CheckerboardItems, which are a generic base class that understands how to display a thumbnail with text (and, in the future, other UI elements) below it. CheckerboardLayout is handed a reference to CheckerboardPage’s ViewCollection of LayoutItems. The two classes (as well as others) can then work together with the ViewCollection and observe its changes without special-case down- and up-calls between themselves.
It is these two classes that handle most of the UI work of getting the CheckerboardItems on the screen and the various methods of selecting them (either by keyboard, accelerator, or mouse). Selection state is maintained by ViewCollection, again, as a central repository for all interested parties. (Again, see Data Structures for a fuller explanation of ViewCollection.)
Although CheckerboardPage can be used as-is, there’s an additional subclass for more specialized operations: CollectionPage. Almost all checkerboard pages in Shotwell are CollectionPages (with notable exceptions). An important distinction here is that CheckeboardPage, CheckerboardLayout, and CheckerboardItem have no knowledge of the database or other library mode features. Thus, they can be generically used in direct-edit mode (although they’re not currently) and with ImportPage, where the backing photos are obviously not in the library (yet).
CollectionPage, on the other hand, knows all about library mode and offers almost all the functionality any library-mode checkerboard page would need, including UI elements, drag-and-drop source handling, variable thumbnail sizes (available by using Thumbnail, a child of LayoutItem), and various editing and export functions. CollectionPage does not understand specifics of certain views or containers (such as Events or, later, Albums), which need to be handled by subclasses. Any checkerboard functionality specific to generic library manipulation belongs in CollectionPage.
SinglePhotoPage & Its Children
The other major child of Page is SinglePhotoPage. SinglePhotoPage is the conceptual opposite of CheckerboardPage; its purpose is to display a single photo scaled to best fit the available window space while maintaining its aspect ratio. It does maintain a ZoomState that allows for the page to display a zoomed image that may be panned around by the user.
SinglePhotoPage also offers a handful of overrideable virtual methods to allow subclasses to draw on or otherwise manipulate the display. SinglePhotoPage, like CheckerboardPage, has no library-mode concepts; it deals primarily with GDK objects to get the image on the display and keep it scaled as the window is resized. A simple subclass and user of SinglePhotoPage is SlideshowPage. Another less-simple subclass is ImportQueuePage, which displays photos as they are imported into the library.
EditingHostPage is a more sophisticated example of a SinglePhotoPage. It is explained in Photo Editor.
Shotwell is also capable of opening a photo file and editing it directly, without importing it into the library. In this mode, there is no sidebar and there is only one Page, DirectPhotoPage, which is a descendant of EditingHostPage. Instead of an on-disk database, SQLite’s in-memory database feature is used, meaning the same photo pipeline and non-destructive editing system can be used in direct-edit mode. Multiple direct-editing sessions can be open at the same time (each in its own process space).
The AppWindow that controls a direct-editing session is a DirectWindow, which hosts the DirectPhotoPage. When the user moves through a directory of photos (using DirectPhotoPage’s back and forward buttons), the DirectPhotoPage merely swaps the new photo in and drops the old one (prompting the user to save changes if applicable). Unlike LibraryWindow, DirectWindow has few responsibilities manipulating Pages.