Shotwell Coding Notes
As with all Yorba projects, Shotwell source should conform to the Yorba coding conventions. The following notes regard Shotwel-specific issues developers should be aware of. Some of these are general to Vala.
Use assert () for sanity checking as well as parameter checking. Do not use assertions to check user input; this must be checked at run-time and either reported back to the user or silently corrected. Don’t go crazy with assertions. Vala’s generated C code does null checking automatically, for example.
Use Vala’s logging functions rather than stdout/stderr.printf (). Logged messages can be captured or suppressed later, if necessary. Note that calling the error () log function will terminate the application. Other logging, debugging, and error functions can be found at the bottom of the GLib page.
Avoid creating or using structs that will contain pointers, as this may cause the blocks pointed at to be either leaked or freed inappropriately. Use a class instead.
Another reason to prefer classes over structs is that access modifiers in structs don’t appear to be correctly handled as of Vala 0.14.
- The app’s database and caches and various other files are maintained in a hidden directory in the user’s home directory. We assume that these files will not be deleted or modified directly by the user at run-time, so no error recovery should be performed. However, basic sanity checking should be done at start time to ensure the application can execute.
- Although some configuration may be maintained outside the hidden data directory, external config changes won’t be recognized after program initialization.
- Data files external to the app (in particular, the user’s photo files) may be altered, moved, or deleted by the user at any time. Because they don’t “belong” to Shotwell, we must be account for the various possibilities.
- For SQLite, use assert () on prepare_v2 on the SQLite bind_*() calls, as all error conditions indicate programming errors as well.
- For SQLite’s step (), there are two non-error return code possibilities: Sqlite.DONE, which means no more rows can be found, and Sqlite.ROW, which means a row has been loaded and may be examined. There are two error return codes we must handle as well: Sqlite.CONSTRAINT, which means a value is out of bounds, most likely one that was passed in as an argument. An error code should be propagated for this. Sqlite.ERROR indicates a run-time error, which can be handled with an error () call.
- Use SQLite’s errmsg () in log functions.
- Where possible, name classes and parameters using the same words and phrases used in GNOME standards documents and API references.
- Whenever you’re writing a class and you see a group of methods that together provide some coherent feature (e.g., taken together, these methods mean that this thing can be tagged and untagged or taken together, these methods mean that this thing can be placed in the trash can and restored back out of the trash can) consider factoring them into an interface. Even if only one class implements the interface now, it’s good to introduce it, and make sure that, when possible, code that uses the class works through interface references as opposed to class references. This allows us to snap-in and snap-out different concrete classes without ever touching client code.
Whenever you’re writing a method or a series of methods that implement a mutating operation on a data object (e.g., “rotate this photo”) consider factoring those methods out of the data object itself and into a Command class (e.g., “RotateCommand”) that is parameterized in its constructor. This makes undo/redo much easier. It also makes automated testing easier, because test harnesses can assemble queues of command objects and then execute each one in sequence.
We all know that Shotwell is composed of logical subsystems (e.g., Printing, Image Transformation, Publishing, File System Monitoring, etc.). Since we’re going to start using interfaces more, the question of where to put interface definitions comes up. We decided that for each subsystem, all of the interfaces related to that subsystem should go in a file named SubsystemNameInterfaces.vala, (e.g., PrintingInterfaces.vala, etc.).