HACKING Dimbola for fun and very little profit ============================================== Introduction ------------ This file has some notes about how Dimbola should be developed. All the generally accepted good Python coding stuff applies: PEP8 formatting, using only spaces and no tabs for indentation, etc. We cover here only things specific to the Dimbola project. Overview of the code base ------------------------- * dimbola-gtk -- the main program; this is intended to be a one-liner * dimbola/*.py -- Python package with the core of the app * dimbola/plugins/*_plugin.py -- plugins that come with the core app; as much as possible is put into plugins * dimbola/ui.ui and dimbola/plugins/*.ui -- glade/GtkBuilder files; these are loaded automatically * dimbola/*_tests.py and dimbola/plugins/*_tests.py -- unit tests for the corresponding files Development priorities ---------------------- The main priority should always be to fix bugs. Each release should fix all bugs, if at all possible. It is obvious that this is not always possible, but it is the goal anyway. Better to attempt something really good and almost get it than to play it safe. Version numbering ----------------- The canonical location of the version number is in dimbola/__init__.py as the version variable. setup.py will extract it from there. ui.py will use dimbola.version to set the version number in the Help/About dialog box. Version number until 1.0 will be as follows: 0.0.x pre-alpha versions, not intended to be usable at all 0.x.0 alpha and beta versions; these are intended to be usable, though they are probably really buggy 1.0.0 first release intended to be usable by a non-hacker Eventually, a ROADMAP file will be written to specify what 1.0.0 should contain. Writing plugins --------------- To write a plugin, import the dimbola package, and subclass dimbola.Plugin. The initializer MUST have the following signature: def __init__(self, mwc): To use the plugin, put it in a file in dimbola/plugins/foo_plugin.py. Plugins may access the MainWindowController's attributes and methods. When a Plugin subclass is instantiated, it MUST NOT cause any side effects: it must not modify the user interface, connect to signals, or whatever. All of that must be done in the enable() method, and un-done in the disable() method. This is necessary so that the user may enable and disable any installed plugin. Plugins may add new items to menus via MWC.add_to_menu. The UI file needs to specify a name for the menu widget, and this name is used with add_to_menu. Additionally, the menu may have separators whose names begin with prepend_separator or append_separator, and add_to_menu will put the new menu item before or after such a separator. To remove the menu item (when the plugin is disabled), the MWC.remove_from_menu method may be used. Similarily for sidebar sections: see MWC.add_to_sidebar and MWC.remove_from_sidebar. Plugins may have their own user interface definition files. For a plugin named foo_plugin.py, the file foo.ui is loaded automatically, if it exists. The plugin may access the widgets via MainWindowController.widgets. Signals and hooks ----------------- We try use GObject signals for communicating between parts of the code. This decouples different modules, which is a good thing. For example, the thumbnail grid is implemented using model/view/controller. When the list of photos in the model changes, the view must redraw itself. Rather than having the model call a method on the view, the view connects to a signal in the model. Thus, the model need not know anything about the view. Indeed, other things than the view can make use of the same signal. In order to connect to a signal, you need to know the object the signal applies to. This is awkward when a plugin provides the object and the signal. We work around this by adding the signal to a well-known object, the MainWindowController, using the new_hook method. Thus, in Dimbola, a hook is a custom signal added to MWC. Unit tests and coverage ----------------------- The goal is to make Dimbola a very good program. Part of that is to make it very reliable, and the way to get there is to have an automated unit test suite with very good coverage. Currently, there is abysmal coverage. The current code was written in haste, to get a proof-of-concept prototype done very quickly. Writing tests would have slowed things down. The strategy to get to good coverage is as follows: all of the current GUI code exists in dimbola/db.py and dimbola/ui.py. db.py may get some unit tests later on, but ui.py is going to be test-less. However, as much code as possible will be broken out of it, into other modules, and those modules will have unit tests. The goal is to have those other modules have 100% statement coverage. Almost all the code will be called in response to GTK+ events and signals, from callback functions. The general design principle here, as far as testing is concerned, is that the actual callback function will be not be tested, but it will be as simple as possible and call functions elsewhere to do the actual work. Those functions will be unit tested. Using Glade ----------- The UI is defined using Glade and GtkBuilder. Everything that can reasonably be done in Glade shall be done in Glade. Widgets in the .glade file and their signal callbacks will be connected automatically based on a naming convention. For a widget named foo, and its signal named bar, a callback will be connected automatically if the code has a method called on_foo_bar in a relevant controller object. This means the .glade file should not have any signal handlers defined. Widgets that do not have callbacks can use whatever name Glade gives them by default (e.g., "textview123"). If a callback is needed, the widget should be renamed in a sensible way that indicates its use (e.g., "photo_tags_textview"). See dimbola/gtkapp.py for details on the magic. Background processing --------------------- Threads are evil, at least in the context of Python and GTK+. We do not use (explicit) threads in Dimbola. Instead, we use glib.idle_add and the Python standard library module multiprocessing. The latter is wrapped inside the BackgroundManager and BackgroundJob classes; see the dimbola/bgjobs.py module.