← Back to team overview

spv-dev team mailing list archive

[Merge] lp:~claude-bolduc/slidepresenterview/choose-projector-screens into lp:slidepresenterview

 

C. Bolduc has proposed merging lp:~claude-bolduc/slidepresenterview/choose-projector-screens into lp:slidepresenterview.

Requested reviews:
  C. Bolduc (claude-bolduc)

For more details, see:
https://code.launchpad.net/~claude-bolduc/slidepresenterview/choose-projector-screens/+merge/45326

Implement the feature presenter-choose-projector-screens.
-- 
https://code.launchpad.net/~claude-bolduc/slidepresenterview/choose-projector-screens/+merge/45326
Your team SlidePresenterView Development Team is subscribed to branch lp:slidepresenterview.
=== modified file '.bzrignore'
--- .bzrignore	2010-05-03 02:19:38 +0000
+++ .bzrignore	2011-01-06 06:25:00 +0000
@@ -6,3 +6,6 @@
 slidepresenterview/ui/qt_forms/*.py
 run_spv.bat
 slidepresenterview.egg-info
+*.pro.user
+doc/_build
+doc/developers/_api_doc

=== modified file 'HACKING'
--- HACKING	2010-07-11 17:27:55 +0000
+++ HACKING	2011-01-06 06:25:00 +0000
@@ -14,13 +14,7 @@
       * Since 2010-01, we use a patched version of Mock. That version
         in already included in thirdparty/mock.py.
   - Freshen (for functionnal tests)
-      * Since 2010-05, we use a pre-release of Freshen 0.2 (currently
-        the Master branch) available for download at:
-          http://github.com/rlisagor/freshen
-        To install it:
-          python setup.py install 
-  - PyYaml (needed by Freshen)
-      * easy_install pyyaml
+      * easy_install freshen 
       
       
 How to compile/generate UI files

=== added directory 'doc'
=== added directory 'doc/_static'
=== added directory 'doc/_templates'
=== added file 'doc/conf.py'
--- doc/conf.py	1970-01-01 00:00:00 +0000
+++ doc/conf.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+#
+# SlidePresenterView documentation build configuration file, created by
+# sphinx-quickstart on Sun Jan 02 17:50:49 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 
+              'sphinx.ext.autosummary', 
+              'sphinx.ext.pngmath', 
+              'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'SlidePresenterView'
+copyright = u'2011, SlidePresenterView Development Team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# -- Specific autodoc configuration --------------------------------------------
+
+autoclass_content = "both"
+
+autodoc_default_flags = ['members', 'undoc-members']
+
+# -- Specific autosummary configuration ----------------------------------------
+
+autosummary_generate = True
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'SlidePresenterViewdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'SlidePresenterView.tex', u'SlidePresenterView Documentation',
+   u'SlidePresenterView Development Team', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'slidepresenterview', u'SlidePresenterView Documentation',
+     [u'SlidePresenterView Development Team'], 1)
+]

=== added directory 'doc/developers'
=== added file 'doc/developers/api_reference.rst'
--- doc/developers/api_reference.rst	1970-01-01 00:00:00 +0000
+++ doc/developers/api_reference.rst	2011-01-06 06:25:00 +0000
@@ -0,0 +1,39 @@
+SlidePresenterView's modules
+============================
+
+GUI modules
+-----------
+
+.. autosummary::
+   :toctree: _api_doc/
+   
+   slidepresenterview
+   slidepresenterview.ui
+   slidepresenterview.ui.console_window
+   slidepresenterview.ui.presentation
+   
+   slidepresenterview.ui.widgets
+   slidepresenterview.ui.widgets.screen
+   slidepresenterview.ui.widgets.preferred_projectors
+
+
+Configuration modules
+---------------------
+
+.. autosummary::
+   :toctree: _api_doc/
+   
+   slidepresenterview.config
+   slidepresenterview.config.qt_configuration
+   slidepresenterview.config.user_configuration
+
+
+Utility modules
+---------------
+
+.. autosummary::
+   :toctree: _api_doc/
+   
+   slidepresenterview.utils
+   slidepresenterview.utils.observation
+   slidepresenterview.utils.pyqt
\ No newline at end of file

=== added directory 'doc/en'
=== added file 'doc/en/user_manual.rst'
--- doc/en/user_manual.rst	1970-01-01 00:00:00 +0000
+++ doc/en/user_manual.rst	2011-01-06 06:25:00 +0000
@@ -0,0 +1,4 @@
+User manual for SlidePresenterView
+==================================
+
+TODO
\ No newline at end of file

=== added file 'doc/index.rst'
--- doc/index.rst	1970-01-01 00:00:00 +0000
+++ doc/index.rst	2011-01-06 06:25:00 +0000
@@ -0,0 +1,25 @@
+Welcome to SlidePresenterView's documentation!
+==============================================
+
+Console to allow presenters to control their presentations on multiple screens. 
+
+Designed for seminars, talks and courses using mostly PDF slides.
+
+See the :doc:`user manual <en/user_manual>` to get started with SPV.
+
+Contents
+--------
+
+.. toctree::
+   :maxdepth: 1
+
+   en/user_manual
+   developers/api_reference
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+

=== added file 'resources/spv_qtproject.pro'
--- resources/spv_qtproject.pro	1970-01-01 00:00:00 +0000
+++ resources/spv_qtproject.pro	2011-01-06 06:25:00 +0000
@@ -0,0 +1,6 @@
+QT += svg \
+    xml
+TARGET = slidepresenterview_form
+TEMPLATE = app
+FORMS += ui/console.ui \
+    ui/preferred_projectors.ui

=== removed file 'resources/tests/unicode_éè.pdf'
Binary files resources/tests/unicode_éè.pdf	2009-08-28 22:53:03 +0000 and resources/tests/unicode_éè.pdf	1970-01-01 00:00:00 +0000 differ
=== modified file 'resources/ui/console.ui'
--- resources/ui/console.ui	2009-07-04 22:34:33 +0000
+++ resources/ui/console.ui	2011-01-06 06:25:00 +0000
@@ -109,7 +109,7 @@
      <x>0</x>
      <y>0</y>
      <width>800</width>
-     <height>24</height>
+     <height>21</height>
     </rect>
    </property>
    <widget class="QMenu" name="menuFile">
@@ -124,8 +124,10 @@
     <property name="title">
      <string>Presentation</string>
     </property>
-    <addaction name="action_start"/>
-    <addaction name="action_stop"/>
+    <addaction name="action_show_presentation"/>
+    <addaction name="action_end_presentation"/>
+    <addaction name="separator"/>
+    <addaction name="action_configure_preferred_projectors"/>
    </widget>
    <addaction name="menuFile"/>
    <addaction name="menuPresentation"/>
@@ -135,38 +137,31 @@
    <property name="text">
     <string>Open...</string>
    </property>
-   <property name="shortcut">
-    <string>Ctrl+O</string>
-   </property>
   </action>
   <action name="action_exit">
    <property name="text">
     <string>Exit</string>
    </property>
-   <property name="shortcut">
-    <string>Ctrl+Q</string>
-   </property>
-  </action>
-  <action name="action_start">
-   <property name="enabled">
-    <bool>false</bool>
-   </property>
-   <property name="text">
-    <string>Start</string>
-   </property>
-   <property name="shortcut">
-    <string>Ctrl+L</string>
-   </property>
-  </action>
-  <action name="action_stop">
-   <property name="enabled">
-    <bool>false</bool>
-   </property>
-   <property name="text">
-    <string>Stop</string>
-   </property>
-   <property name="shortcut">
-    <string>Ctrl+E</string>
+  </action>
+  <action name="action_show_presentation">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Show</string>
+   </property>
+  </action>
+  <action name="action_end_presentation">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>End</string>
+   </property>
+  </action>
+  <action name="action_configure_preferred_projectors">
+   <property name="text">
+    <string>Configure preferred projector(s)</string>
    </property>
   </action>
  </widget>

=== added file 'resources/ui/preferred_projectors.ui'
--- resources/ui/preferred_projectors.ui	1970-01-01 00:00:00 +0000
+++ resources/ui/preferred_projectors.ui	2011-01-06 06:25:00 +0000
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>preferred_projectors</class>
+ <widget class="QDialog" name="preferred_projectors">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>300</width>
+    <height>350</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Choose preferred projector(s)</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Show presentation on</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="console_screen_button">
+     <property name="text">
+      <string>console sreen</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="other_screens_button">
+     <property name="text">
+      <string>other screen(s)</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="selected_screens_button">
+     <property name="text">
+      <string>selected screen(s):</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="screen_list_widget" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <property name="spacing">
+       <number>0</number>
+      </property>
+      <property name="sizeConstraint">
+       <enum>QLayout::SetDefaultConstraint</enum>
+      </property>
+      <property name="margin">
+       <number>0</number>
+      </property>
+      <item>
+       <widget class="QGroupBox" name="selected_screens_group">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="title">
+         <string/>
+        </property>
+        <layout class="QVBoxLayout" name="selected_screens_items_layout"/>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="refresh_button">
+        <property name="text">
+         <string> Refresh screen list </string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="choose_projectors_dialog_buttons">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>choose_projectors_dialog_buttons</sender>
+   <signal>accepted()</signal>
+   <receiver>preferred_projectors</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>257</x>
+     <y>340</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>choose_projectors_dialog_buttons</sender>
+   <signal>rejected()</signal>
+   <receiver>preferred_projectors</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>290</x>
+     <y>340</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>selected_screens_button</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>selected_screens_group</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>50</x>
+     <y>83</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>145</x>
+     <y>193</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

=== added directory 'resources/ui/qss'
=== added file 'resources/ui/qss/common.qss'
--- resources/ui/qss/common.qss	1970-01-01 00:00:00 +0000
+++ resources/ui/qss/common.qss	2011-01-06 06:25:00 +0000
@@ -0,0 +1,21 @@
+/* SlidePresenterView - Console for presenters 
+ * https://launchpad.net/slidepresenterview
+ *
+ * Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Common Qt style sheet for the whole SPV application. */
+

=== added file 'resources/ui/qss/windows.qss'
--- resources/ui/qss/windows.qss	1970-01-01 00:00:00 +0000
+++ resources/ui/qss/windows.qss	2011-01-06 06:25:00 +0000
@@ -0,0 +1,45 @@
+/* SlidePresenterView - Console for presenters 
+ * https://launchpad.net/slidepresenterview
+ *
+ * Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Qt style sheet for the whole SPV application 
+   specific to the Windows platform. */
+
+QMenuBar {
+  background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
+                                    stop:0 lightgray, stop:1 darkgray);
+}
+
+QMenuBar::item {
+  background: transparent;
+}
+
+QMenuBar::item:selected {
+  background: #a8a8a8;
+}
+
+QMenuBar::item:pressed {
+  background: #888888;
+}
+
+QToolTip {
+  border: 2px solid #ffffcc;
+  padding: 5px;
+  border-radius: 3px;
+  opacity: 200;
+}

=== modified file 'setup.cfg'
--- setup.cfg	2010-07-11 17:27:55 +0000
+++ setup.cfg	2011-01-06 06:25:00 +0000
@@ -3,6 +3,7 @@
 with-doctest=1
 #detailed-errors=1
 with-freshen=1
+tags=~pending
 testmatch=(?:^|[\b_\.%s-])([Tt]est|should)
 where=slidepresenterview/
 exclude=slidepresenterview/tests/manual

=== modified file 'setup.py'
--- setup.py	2010-05-02 23:05:14 +0000
+++ setup.py	2011-01-06 06:25:00 +0000
@@ -32,7 +32,7 @@
 #####
 import os
 import sys
-
+import shutil
 
 from distutils.command.build import build as build_cmd
 from distutils.core import Command
@@ -56,10 +56,30 @@
     sys.path.insert(0, path)
 
 
+# Needed for default documentation location.
+_DIR_TO_BUILD_DOC = 'doc/_build'
+_DIR_TO_BUILD_API_DOC = 'doc/developers/_api_doc'
+_base_path = os.path.dirname(__file__)
+_builded_doc_path = os.path.join(_base_path, _DIR_TO_BUILD_DOC)
+_builded_api_doc_path = os.path.join(_base_path, _DIR_TO_BUILD_API_DOC)
+
+
 ##########
 # Custom commands
 #####
 
+def _get_gen_ui_cmd():
+    """
+    :return: a list of strings representing the invocation of the gen_ui.py script.
+    """
+    cur_dir = os.path.dirname(__file__)
+    cmd = [sys.executable] # First, the python command.
+    tool_dir = os.path.join(cur_dir, 'tools')
+    cmd.append(os.path.join(tool_dir, 'gen_ui.py')) # Second, the script
+                                                    # to run.
+    return cmd
+
+
 class SPVGenUICommand(Command):
 
     description = "Generate .py from .ui files"
@@ -72,11 +92,7 @@
         pass
 
     def run(self):
-        cur_dir = os.path.dirname(__file__)
-        cmd = [sys.executable] # First, the python command.
-        tool_dir = os.path.join(cur_dir, 'tools')
-        cmd.append(os.path.join(tool_dir, 'gen_ui.py')) # Second, the script
-                                                        # to run.
+        cmd = _get_gen_ui_cmd()
         spawn(cmd)
  
  
@@ -94,11 +110,12 @@
     def run(self):
         self._remove_pyc()
         self._remove_ui()
+        self._remove_doc()
     
     def _remove_pyc(self):
         log.info("-- Removing pyc files...")
         ## See: http://bugs.python.org/file14146
-        for root, dirs, files in os.walk(os.getcwd(), topdown=False):
+        for root, _dirs, files in os.walk(os.getcwd(), topdown=False):
             for name in files:
                 if (name.endswith('.pyc') and 
                         os.path.isfile(os.path.join(root, name))):
@@ -106,16 +123,24 @@
                     os.remove(os.path.join(root, name))
                     
     def _remove_ui(self):
-        log.info("-- Removing Qt UI generated files...")
-        cur_dir = os.path.dirname(__file__)
-        cmd = [sys.executable] # First, the python command.
-        tool_dir = os.path.join(cur_dir, 'tools')
-        cmd.append(os.path.join(tool_dir, 'gen_ui.py')) # Second, the script
-                                                        # to run.
+        log.info("-- Removing Qt generated files...")
+        cmd = _get_gen_ui_cmd()
         cmd.append("--clean")
+        
         spawn(cmd)
-
-
+        
+        
+    def _remove_doc(self):
+        log.info("-- Removing Sphinx documentation generated files...")
+        self._remove_dir(_builded_doc_path)
+        self._remove_dir(_builded_api_doc_path)
+        
+    def _remove_dir(self, path):
+        if os.path.isdir(path):
+            log.info('removing: %s' % path)
+            shutil.rmtree(path)
+
+        
 class SPVBuildCommand(build_cmd):
     
     def has_ui(self):
@@ -143,6 +168,30 @@
             self.run_command(cmd_name)
 
 
+try:
+    from sphinx.setup_command import BuildDoc
+    
+    _sphinx_is_installed = True
+    
+    class SPVBuildDocCommand(BuildDoc):
+        
+        def finalize_options(self):
+            if self.build_dir is None:
+                # We build at a different default location than Sphinx.
+                self.build_dir = _builded_doc_path
+                self.mkpath(self.build_dir)
+            self.ensure_dirname('build_dir')
+            BuildDoc.finalize_options(self)
+            
+            
+        def run(self):
+            self.run_command('gen_ui') # Must be done before building the documentation.
+            BuildDoc.run(self)
+
+except ImportError:
+    _sphinx_is_installed = False
+
+
 ##########
 # META INFORMATION
 # http://docs.python.org/dist/meta-data.html
@@ -185,9 +234,12 @@
     'gen_ui'      : SPVGenUICommand,
     'build'       : SPVBuildCommand,
     'clean_gen'   : SPVCleanGeneratedCommand,
-    'clean_all'    : SPVCleanAllCommand,
+    'clean_all'   : SPVCleanAllCommand,
      }
 
+if _sphinx_is_installed:
+    _cmdclass['build_sphinx'] = SPVBuildDocCommand
+
 _extras_requires = {
      }
 
@@ -197,6 +249,8 @@
 
 _tests_requires = [
     'nose',
+    'unittest2',
+    'freshen'
     ]
 
 _entry_points = {

=== modified file 'slidepresenterview/__init__.py'
--- slidepresenterview/__init__.py	2011-01-06 03:40:30 +0000
+++ slidepresenterview/__init__.py	2011-01-06 06:25:00 +0000
@@ -25,15 +25,27 @@
 ##########
 # Packages
 #####
-import sys
-from itertools import imap
+import os, sys
+import itertools
+
+
+##########
+# Paths
+#####
+
+BASE_DIR = '../'
+RESOURCES_DIR = 'resources' # Relative to BASE_DIR
+
+
+_file_path = os.path.dirname(__file__)
+_base_path = os.path.abspath(os.path.join(_file_path, BASE_DIR))
+_resources_path = os.path.join(_base_path, RESOURCES_DIR)
 
 
 ##########
 # Version
 #####
 
-
 # Version tuple (major, minor, fix, type, type_ver)
 #   type can be: release, candidate, beta, alpha, dev
 #   type_ver is a sub-release number (Alpha1 or Alpha2)
@@ -70,8 +82,8 @@
     ...
     ValueError: Unknown version type (Alpha).
     
-    @raise ValueError: If the information from version_info is not valid. 
-    @return: String with the version
+    :raise ValueError: If the information from version_info is not valid. 
+    :return: String with the version
     """
     base = '%d.%d.%d' % version[:3]
     
@@ -121,8 +133,8 @@
     ...
     ValueError: Unknown version type (Alpha).
     
-    @raise ValueError: Version info is not valid. 
-    @return: The PyPi Development Status string classifier.
+    :raise ValueError: Version info is not valid. 
+    :return: The PyPi Development Status string classifier.
     """
     version_type = version[3]
     if version_type == 'release':
@@ -149,12 +161,12 @@
 ##
 def check_env():
     """
-    Checks if all dependencies are installed and if SPV car run wth 
+    Checks if all dependencies are installed and if SPV can run with 
     the current setup (environment).
     
-    @return: True if SPV can be run, False if not.  
+    :return: True if SPV can be run, False if not.  
     
-    @note: It prints errors to the stderr.
+    :note: It prints errors to the stderr.
     """
     return (
         _check_python() and
@@ -164,20 +176,20 @@
 
 
 def _check_python():
-    if sys.version_info < (2, 5):
-        print >> sys.stderr, "[ERROR] Python >=2.5."
+    if sys.version_info < (2, 6):
+        print >> sys.stderr, "[ERROR] Python >=2.6."
         return False
     
     return True
     
 def _check_qt():
     try:
+        from PyQt4 import QtGui # pylint: disable-msg=W0612
         from PyQt4 import QtCore
-        from PyQt4 import QtGui # pylint: disable-msg=W0612
         from PyQt4 import QtXml # pylint: disable-msg=W0612
         
         qt_version_str = QtCore.QT_VERSION_STR
-        qt_version = tuple(imap(int, qt_version_str.split('.')))
+        qt_version = tuple(itertools.imap(int, qt_version_str.split('.')))
         
     except ImportError:
         print >> sys.stderr, "[ERROR] PyQt4 not found."
@@ -207,20 +219,53 @@
         return False
         
     return True
-    
+
+
+def _load_stylesheet():
+    common_stylesheet = _load_stylesheet_for_platform('all')
+    specific_stylesheet = _load_stylesheet_for_platform(sys.platform)
+    return common_stylesheet + specific_stylesheet
+
+
+def _load_stylesheet_for_platform(platform):
+    from slidepresenterview.config import qt_configuration
+    qss_path = os.path.join(_resources_path, qt_configuration.QSS_DIR)
+
+    try:
+        
+        qss = ""
+        for qss_filename in qt_configuration.QSS_LIST_FOR_PLATFORM[platform]:
+            qss_file = os.path.join(qss_path, qss_filename)
+            with open(qss_file, "r") as file_:
+                qss = qss + file_.read() 
+        return qss
+    except: # pylint: disable-msg=W0702
+        return "" # There is no style sheet.
+
+
 
 def main_gui():
+    """
+    Main function of the SPV program.
+    """
     if not check_env():
         sys.exit(1)
-    
-    from PyQt4 import QtGui
-    
+
+    from slidepresenterview.utils import pyqt    
     from slidepresenterview.ui import console_window
     from slidepresenterview.ui import presentation
+    from slidepresenterview.ui.widgets.screen import ScreenProviderQt
+    from slidepresenterview.config import user_configuration
     
-    app = QtGui.QApplication(sys.argv)
+    app = pyqt.get_application()
+    stylesheet = _load_stylesheet()
+    app.setStyleSheet(stylesheet)
+    user_config = user_configuration.UserConfiguration() 
     presentation_mediator = presentation.Presentation()
-    window = console_window.ConsoleWindow(presentation_mediator)
+    screen_provider = ScreenProviderQt(app.desktop())
+    window = console_window.ConsoleWindow(user_config, presentation_mediator, 
+                                          screen_provider)
     window.show()
+    if sys.platform == "darwin":
+        window.raise_()
     sys.exit(app.exec_())
-    

=== added directory 'slidepresenterview/config'
=== added file 'slidepresenterview/config/__init__.py'
--- slidepresenterview/config/__init__.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/config/__init__.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+Configurations used by SlidePresenterView.
+"""

=== added file 'slidepresenterview/config/qt_configuration.py'
--- slidepresenterview/config/qt_configuration.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/config/qt_configuration.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+Configurations used by SlidePresenterView for the Qt environment.
+
+It is loaded at the start of the application. 
+"""
+
+QSS_DIR = 'ui/qss' # Relative to RESOURCES_DIR
+
+QSS_LIST_FOR_PLATFORM = {
+  'all': ['common.qss'],
+  'win32': ['windows.qss'],
+  'cygwin': [],
+  'darwin': [],
+  'linux2': []
+}

=== added file 'slidepresenterview/config/user_configuration.py'
--- slidepresenterview/config/user_configuration.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/config/user_configuration.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+User-defined configurations for SlidePresenterView.
+
+:note: Currently, this contains only configurations for the preferred projectors.
+"""
+
+class UserConfiguration(object):
+    
+    OPTION_CONSOLE_SCREEN = 1
+    OPTION_OTHER_SCREENS = 2
+    OPTION_SELECTED_SCREENS = 3
+
+    def __init__(self):
+        self.set_preferred_projectors_configuration(UserConfiguration.OPTION_OTHER_SCREENS, [])
+        
+        
+    def set_preferred_projectors_configuration(self, projector_type_option, 
+                                               preferred_selected_screens):
+        """
+        :param projector_type_option: Option chose for the type of preferred projectors.
+                                      Can be either OPTION_CONSOLE_SCREEN, OPTION_OTHER_SCREENS
+                                      or OPTION_SELECTED_SCREENS.
+                                      
+        :param preferred_selected_screens: List of screen numbers (provided by the screen provider).
+        """
+        # pylint: disable-msg=W0201
+        self._projector_type_option = projector_type_option
+        self._preferred_selected_screens_as_projector = preferred_selected_screens
+
+
+    def is_preferring_presentation_showed_on_other_screens(self):
+        return self._projector_type_option == UserConfiguration.OPTION_OTHER_SCREENS
+
+
+    def is_preferring_presentation_showed_on_console_screen(self):
+        return self._projector_type_option == UserConfiguration.OPTION_CONSOLE_SCREEN
+    
+    
+    def is_preferring_presentation_showed_on_selected_screens(self):
+        return self._projector_type_option == UserConfiguration.OPTION_SELECTED_SCREENS
+    
+
+    def get_preferred_selected_screens_as_projectors(self):
+        return self._preferred_selected_screens_as_projector 
+
+
+    def is_screen_selected_as_preferred_projector(self, screen_number):
+        """
+        :param screen_number: The screen numbers is provided by the screen provider of the 
+                              application.
+        """
+        return screen_number in self._preferred_selected_screens_as_projector

=== modified file 'slidepresenterview/docformats/format_pdf.py'
--- slidepresenterview/docformats/format_pdf.py	2010-05-03 03:13:08 +0000
+++ slidepresenterview/docformats/format_pdf.py	2011-01-06 06:25:00 +0000
@@ -23,10 +23,6 @@
 """
 import os
 
-from PyQt4 import QtGui # pylint: disable-msg=W0611,E0611
-     # This import is not necessary, but it removes a bug
-     # in the order of the doctests.
-
 from QtPoppler import Poppler
 
 from slidepresenterview.docformats import errors

=== modified file 'slidepresenterview/tests/__init__.py'
--- slidepresenterview/tests/__init__.py	2010-07-11 23:14:59 +0000
+++ slidepresenterview/tests/__init__.py	2011-01-06 06:25:00 +0000
@@ -27,6 +27,8 @@
 import doctest
 import unittest2
 
+from shutil import copyfile
+
 
 
 ##########
@@ -92,9 +94,16 @@
     
     @param resource_file_name: The file name
     
-    @raise ValueError: If the file doen't exist 
-    @return: The full path the resource file 
+    @raise ValueError: If the file does not exist 
+    @return: The full path of the resource file 
     """
+    file_path = _get_absolute_path_of_resource(resource_file_name)
+    if not os.path.isfile(file_path):
+        raise ValueError("Test resource file not found at %s." % file_path)
+    return file_path
+
+
+def _get_absolute_path_of_resource(resource_file_name):
     res_dir = TEST_RESOURCES_DIR
     cur_path = os.path.dirname(__file__)
     base_dir = os.path.normpath(BASE_DIR)
@@ -103,7 +112,30 @@
     res_path_rel = os.path.join(base_dir, res_dir)
     res_path_full = os.path.normpath(os.path.abspath(res_path_rel))
     file_path = os.path.join(res_path_full, resource_file_name)
-    
-    if not os.path.isfile(file_path):
-        raise ValueError("Test resource file not found at %s." % file_path)
     return file_path
+
+
+def copy_resource(source_file_path, target_resource_file_name):
+    """
+    Copy a source file in a new target resource file.
+    
+    @param source_file_path: The source file absolute and normalized path
+    @param target_resource_file_name: The target file name
+
+    @return: The full path of the target resource file 
+    """
+    target_file_path = _get_absolute_path_of_resource(target_resource_file_name)
+    copyfile(source_file_path, target_file_path)
+    return target_file_path
+
+
+def delete_resource(resource_file_name):
+    """
+    Delete a resource file.
+    
+    @param resource_file_name: The file name
+    
+    @raise ValueError: If the file does not exist 
+    """
+    file_path = get_resources_path(resource_file_name)
+    os.remove(file_path)

=== added file 'slidepresenterview/tests/functional/choose_projector_screens.feature'
--- slidepresenterview/tests/functional/choose_projector_screens.feature	1970-01-01 00:00:00 +0000
+++ slidepresenterview/tests/functional/choose_projector_screens.feature	2011-01-06 06:25:00 +0000
@@ -0,0 +1,77 @@
+Using step definitions from: 'steps/qt_environment_steps', 'steps/preferred_projectors_steps'
+
+Feature: Choose the projector screens to show the presentation
+
+    In order to give my talk
+    As a presenter
+    I want to be able to choose the projector screens to show the presentation.
+    
+    
+    Background:
+        Given I have 2 screens
+            And I can select my preferred projectors
+
+
+    Scenario: Hot plugging a screen
+        Given I hot plug a screen
+        When I refresh the available screens
+        Then 3 screens are listed
+
+        
+    Scenario: Hot unplugging a screen
+        Given I hot unplug a screen
+        When I refresh the available screens
+        Then 1 screen is listed
+
+        
+    Scenario: Hot unplugging all screens
+        Given I hot unplug all screens
+        When I refresh the available screens
+        Then 0 screen is listed
+
+
+    Scenario: Selecting other screens as projectors
+        When I select other screens as projectors
+            And I apply the changes
+        Then the other screens are selected as projectors
+
+
+    Scenario: Selecting the console screen as projector
+        When I select the console screen as projector
+            And I apply the changes
+        Then the console screen is selected as projector
+
+
+    Scenario Outline: Selecting screens as projector
+        When I select screens <screens> as projectors
+            And I apply the changes
+        Then screens <screens> are selected as projectors
+        	And the other screens are not selected as projectors
+    Examples:
+            | screens     |
+            | 1           |
+            | 2           |
+            | 1, 2        |
+
+
+    Scenario: Applying the changes stores current view
+	    When I select screen 2 as projectors
+            And I select other screens as projectors
+            And I apply the changes
+        Then the other screens are selected as projectors
+            And screen 2 is selected as projector
+
+
+    Scenario: Discarding the changes does not modify the initial configuration
+    	Given my old preferences was to select the console screen as projector
+	    When I select other screens as projectors
+            And I discard the changes
+        Then my old preferences are kept
+
+
+    Scenario: Bumping old preferences that do not exist in the current configuration
+	    Given my old preferences was to select screens 1 and 3 as projectors
+        When I select other screens as projectors
+            And I apply the changes
+        Then the other screens are selected as projectors
+            And screen 1 is selected as projector

=== modified file 'slidepresenterview/tests/functional/open_pdf.feature'
--- slidepresenterview/tests/functional/open_pdf.feature	2010-05-03 00:07:27 +0000
+++ slidepresenterview/tests/functional/open_pdf.feature	2011-01-06 06:25:00 +0000
@@ -57,7 +57,8 @@
         When I open a file named with spaces
         Then the requested document is opened
          
-         
+    
+    @need_unicode_file     
     Scenario: Open a file with Unicode characters
         Given no already opened document
         When I open a file named with Unicode characters

=== modified file 'slidepresenterview/tests/functional/steps/console_steps.py'
--- slidepresenterview/tests/functional/steps/console_steps.py	2010-05-03 00:18:13 +0000
+++ slidepresenterview/tests/functional/steps/console_steps.py	2011-01-06 06:25:00 +0000
@@ -24,11 +24,9 @@
 """
 __all__ = []
 
-from PyQt4 import QtCore, QtGui
 from mock import Mock
 
-from freshen import *
-from nose.tools import assert_true
+from freshen import Given, assert_true
 
 from slidepresenterview.tests.functional.steps.slide_view_steps import *
 

=== modified file 'slidepresenterview/tests/functional/steps/document_steps.py'
--- slidepresenterview/tests/functional/steps/document_steps.py	2010-05-03 00:34:47 +0000
+++ slidepresenterview/tests/functional/steps/document_steps.py	2011-01-06 06:25:00 +0000
@@ -24,15 +24,12 @@
 """
 __all__ = []
 
-from PyQt4 import QtCore, QtGui
-
-from freshen import *
-from nose.tools import assert_not_equals, assert_true
-
-from slidepresenterview.utils import pyqt as pyqtutils
+from freshen import (Before, After, Given, When, Then, scc, run_steps, 
+                     assert_true, assert_equals, assert_not_equals)
 
 from slidepresenterview.tests.helpers.open_documents import (PDF_3PAGES, 
-    PDF_WITH_SPACE, PDF_UNICODE, PDF_1PAGE_100X100, PDF_INVALID,
+    PDF_WITH_SPACE, PDF_1PAGE_100X100, PDF_INVALID,
+    create_unicode_file, 
     simulate_open_file_dialog, simulate_cancel_file_dialog,
     WarningRaised)
 
@@ -42,10 +39,19 @@
     '3PAGES': PDF_3PAGES,
     'INVALID': PDF_INVALID,
     'SPACE': PDF_WITH_SPACE,
-    'UNICODE': PDF_UNICODE,
     'NOTFOUND': 'not_found.pdf',
     }
-        
+
+@Before("@need_unicode_file")
+def create_pdf_unicode_file(scenario):
+    file_path = create_unicode_file()
+    DOCS['UNICODE'] = file_path
+
+@After("@need_unicode_file")
+def delete_pdf_unicode_file(scenario):
+    DOCS['UNICODE'] = None
+    #delete_unicode_file() #FIXME not working
+       
 
 @Given("a document opened at page (\d+)[.]?$")
 def set_already_some_doc_at_page(page):
@@ -63,8 +69,10 @@
     
 @Given("the document ([A-Z0-9_]+) opened at page (\d+|None)[.]?$")
 def set_already_doc_at_page(doc, page):
-    run_steps('Given I previously opened the document %s' % doc)
-    run_steps('Given is currently at page %s' % page)
+    run_steps("""
+              Given I previously opened the document %s
+              And is currently at page %s
+              """ % (doc, page))
     
 @Given("is currently at page (\d+|None).*")
 def set_already_at_page(page):
@@ -141,8 +149,10 @@
 
 @Then("the document ([A-Z0-9_]*|None) is(?: still)? opened at page (\d+|None)")
 def check_opened_at_page(doc, page):
-    run_steps('Then the document %s is still opened' % doc)
-    run_steps('Then is at page %s' % page)
+    run_steps("""
+              Then the document %s is still opened
+              And is at page %s
+              """ % (doc, page))
 
 @Then("the document ([A-Z0-9_]*|None) is(?: still)? opened[.]?$")
 def check_doc_opened(doc):
@@ -156,8 +166,10 @@
     
 @Then("the requested document is opened at page (\d+|None)$")
 def check_requested_doc_at_page(page):
-    run_steps('Then the requested document is opened')
-    run_steps('Then is at page %s' % page)
+    run_steps("""
+              Then the requested document is opened
+              And is at page %s
+              """ % page)
     
 @Then("the requested document is opened$")
 def check_requested_doc():
@@ -204,8 +216,10 @@
     
 @Then("both buttons are (enabled|disabled)[.]?$")
 def check_both_buttons(status):
-    run_steps('Then the next button is %s' % status)
-    run_steps('Then the previous button is %s' % status)
+    run_steps("""
+              Then the next button is %s
+              And the previous button is %s
+              """ % (status, status))
 
 @Then("no warning was shown")
 def check_no_warning():

=== added file 'slidepresenterview/tests/functional/steps/preferred_projectors_steps.py'
--- slidepresenterview/tests/functional/steps/preferred_projectors_steps.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/tests/functional/steps/preferred_projectors_steps.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+"""
+Step definitions when configuring the preferred projectors.
+"""
+__all__ = []
+
+from PyQt4 import QtGui, QtCore
+
+from mock import Mock
+
+from freshen import (Before, Given, When, Then, Transform, scc, 
+                     assert_true, assert_false, assert_equal)
+
+
+from slidepresenterview.ui.widgets.screen import convert_to_spv_screen_number
+from slidepresenterview.ui.widgets.preferred_projectors import PreferredProjectors
+
+
+class _FakeScreenInformationProvider(object):
+    FAKE_SCREEN_SIZE = QtCore.QRect(0,0,1000,1000)
+    
+    def __init__(self, screen_info_provider):
+        self.mock = Mock(spec=QtGui.QDesktopWidget)
+        self.mock.screenGeometry.return_value = _FakeScreenInformationProvider.FAKE_SCREEN_SIZE
+        self.mock.screenNumber.return_value = 0
+        
+        self.screen_info_provider = screen_info_provider
+        self.screen_info_provider._screen_information_provider = self.mock
+        
+    
+    def set_fake_screens(self, number_of_screens):
+        self.mock.numScreens.return_value = number_of_screens
+        
+        
+    def add_fake_screens(self, number_of_screens_added):
+        old_number_of_screens = self.mock.numScreens()
+        self.set_fake_screens(old_number_of_screens + number_of_screens_added)
+
+
+@Before
+def replace_screen_info_provider(scenario):
+    scc.mock_screen_info_provider = _FakeScreenInformationProvider(scc.screen_info_provider)
+
+
+@Transform(r"^(\d+) screen$")
+def transform_number_of_screens(number_of_screens):
+    return int(number_of_screens)
+
+@Transform(r"^screen[s]? (.+)$")
+def transform_screens(screens):
+    screens_string = screens.split(',')
+    screens_int = [int(screen_string.strip()) for screen_string in screens_string]
+    return screens_int
+
+
+@Given("^I can select my preferred projectors$")
+def can_select_projectors():
+    scc.pref_projectors = PreferredProjectors(scc.user_config, 
+                                              scc.mock_screen_info_provider.screen_info_provider, 
+                                              scc.win, 
+                                              scc.win)
+
+
+@Given("^I have (\d+ screen)[s]?$")
+def have_number_of_screens(number_of_screens):
+    scc.mock_screen_info_provider.set_fake_screens(number_of_screens)
+    
+@Given("^I hot plug a screen$")
+def hot_plugging_a_screen():
+    scc.mock_screen_info_provider.add_fake_screens(1)
+
+@Given("^I hot unplug a screen$")
+def hot_unplugging_a_screen():
+    scc.mock_screen_info_provider.add_fake_screens(-1)
+
+@Given("^I hot unplug all screens$")
+def hot_unplugging_all_screens():
+    scc.mock_screen_info_provider.set_fake_screens(0)
+    
+@Given("^my old preferences was to select screens 1 and 3 as projectors$")
+def set_old_preference():
+    scc.user_config.set_preferred_projectors_configuration(
+      scc.user_config.OPTION_SELECTED_SCREENS, [1, 3])
+    scc.pref_projectors.on_refresh_button_clicked(True)
+
+@Given("^my old preferences was to select the console screen as projector$")
+def set_old_preference_console_screen():
+    scc.user_config.set_preferred_projectors_configuration(
+      scc.user_config.OPTION_CONSOLE_SCREEN, [])
+    scc.pref_projectors.on_refresh_button_clicked(True)
+
+
+
+@When("^I select other screens as projectors$")
+def select_other_screens_as_projectors():
+    scc.pref_projectors.other_screens_button.setChecked(True)
+
+@When("^I select the console screen as projector$")
+def select_console_screen_as_projector():
+    scc.pref_projectors.console_screen_button.setChecked(True)
+    
+@When("^I select (screen[s]? .+) as projector[s]?$")
+def select_specific_screens_as_projectors(screen_numbers):
+    scc.pref_projectors.selected_screens_button.setChecked(True)
+    _set_check_state_for_selected_screens(screen_numbers)
+
+def _set_check_state_for_selected_screens(screen_numbers):
+    screen_checkboxes = scc.pref_projectors._screen_items
+    num_available_screens = len(screen_checkboxes)
+    if num_available_screens < max(screen_numbers):
+        _fail("Cannot select a screen as projector that have a number bigger than" 
+             + " the number of available screens.")
+    
+    for row, screen_checkbox in enumerate(screen_checkboxes):
+        _set_check_state_for_item_at_according_to(screen_checkbox, row, screen_numbers)
+
+def _fail(message):
+    assert_true(False, message)
+    
+def _set_check_state_for_item_at_according_to(screen_checkbox, row, screen_numbers):
+    spv_screen_number = convert_to_spv_screen_number(row)
+    is_checked = spv_screen_number in screen_numbers
+    screen_checkbox.setChecked(is_checked)
+
+@When("^I refresh the available screens$")
+def refresh_screens():
+    scc.pref_projectors.on_refresh_button_clicked(True)
+
+@When("^I apply the changes$")
+def apply_changes():
+    scc.pref_projectors.accept()
+
+@When("^I discard the changes$")
+def discard_changes():
+    scc.pref_projectors.reject()
+
+
+@Then("^the other screens are selected as projectors$")
+def check_other_screens_selected():
+    assert_true(scc.user_config.is_preferring_presentation_showed_on_other_screens())
+
+@Then("^the console screen is selected as projector$")
+def check_console_screen_selected():
+    assert_true(scc.user_config.is_preferring_presentation_showed_on_console_screen())
+
+@Then("^my old preferences are kept$")
+def check_old_preferences_are_kept():
+    assert_true(scc.user_config.is_preferring_presentation_showed_on_console_screen())
+
+
+@Then("^(\d+ screen)[s]? (?:are|is) listed$")
+def check_number_of_screens(number_of_screens):
+    num_listed_screens = len(scc.pref_projectors._screen_items)
+    assert_equal(num_listed_screens, number_of_screens)
+    
+@Then("^(screen[s]? .+) (?:are|is) selected as projector[s]?$")
+def check_screens_selected(screen_numbers):
+    assert_equal(scc.user_config.get_preferred_selected_screens_as_projectors(), screen_numbers)
+    
+@Then("^the other screens are not selected as projectors$")
+def check_other_screens_not_selected():
+    assert_false(scc.user_config.is_preferring_presentation_showed_on_other_screens())
\ No newline at end of file

=== modified file 'slidepresenterview/tests/functional/steps/qt_environment_steps.py'
--- slidepresenterview/tests/functional/steps/qt_environment_steps.py	2010-05-03 02:50:56 +0000
+++ slidepresenterview/tests/functional/steps/qt_environment_steps.py	2011-01-06 06:25:00 +0000
@@ -24,22 +24,25 @@
 """
 __all__ = []
 
-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtCore
 from freshen import Before, After, scc, glc
 
 from slidepresenterview.utils import pyqt as pyqtutils
+from slidepresenterview.config import user_configuration
 from slidepresenterview.ui import presentation
 from slidepresenterview.ui.console_window import ConsoleWindow
+from slidepresenterview.ui.widgets.screen import ScreenProviderQt 
 
 
 @Before
 def before(sc):
-    if glc.app  is None: # QApplication must be started ONLY ONCE!
-        glc.app = QtGui.QApplication([])
+    glc.app = pyqtutils.get_application()
     QtCore.qInstallMsgHandler(pyqtutils.msg_handler_no_message)
 
+    scc.user_config = user_configuration.UserConfiguration()
     scc.presentation_mediator = presentation.Presentation()
-    scc.win = ConsoleWindow(scc.presentation_mediator)
+    scc.screen_info_provider = ScreenProviderQt(glc.app.desktop())
+    scc.win = ConsoleWindow(scc.user_config, scc.presentation_mediator, scc.screen_info_provider)
     scc.slide_view = scc.win.current_slide_view
 
 @After

=== modified file 'slidepresenterview/tests/functional/steps/slide_view_steps.py'
--- slidepresenterview/tests/functional/steps/slide_view_steps.py	2010-05-03 00:18:13 +0000
+++ slidepresenterview/tests/functional/steps/slide_view_steps.py	2011-01-06 06:25:00 +0000
@@ -22,10 +22,7 @@
 """
 Step definitions when using a slide view.
 """
-from PyQt4 import QtCore, QtGui
-
-from freshen import *
-from nose.tools import assert_true
+from freshen import When, Then, scc, assert_equal
 
 
 class _NextAction: pass
@@ -66,4 +63,4 @@
     elif action is _PREVIOUS:
         button = scc.win.current_slide_previous_button
         
-    assert_equals(should_be_enabled, button.isEnabled())
\ No newline at end of file
+    assert_equal(should_be_enabled, button.isEnabled())
\ No newline at end of file

=== modified file 'slidepresenterview/tests/helpers/open_documents.py'
--- slidepresenterview/tests/helpers/open_documents.py	2010-05-03 00:34:47 +0000
+++ slidepresenterview/tests/helpers/open_documents.py	2011-01-06 06:25:00 +0000
@@ -29,7 +29,6 @@
     'PDF_INVALID',
     'PDF_1PAGE_100X100',
     'PDF_1PAGE_100X100',
-    'PDF_UNICODE',
     'PDF_WITH_SPACE',
     ]
 
@@ -38,16 +37,25 @@
 from PyQt4 import QtCore
 import mock
 
-from slidepresenterview.tests import get_resources_path
+from slidepresenterview.tests import get_resources_path, copy_resource, delete_resource
 
 
 PDF_3PAGES = get_resources_path(u'slides_3p.pdf')
 PDF_2PAGES = get_resources_path(u'slides_2p.pdf')
 PDF_INVALID = get_resources_path(u'invalid.pdf')
 PDF_1PAGE_100X100 = get_resources_path(u'100x100.pdf')
-PDF_UNICODE = get_resources_path(u'unicode_éè.pdf')
 PDF_WITH_SPACE = get_resources_path(u'with space.pdf')
 
+PDF_UNICODE_FILE_NAME = u'unicode_éè.pdf'
+def create_unicode_file():
+    """
+    @return: the absolute path of the created unicode file
+    """
+    return copy_resource(PDF_1PAGE_100X100, PDF_UNICODE_FILE_NAME)
+
+def delete_unicode_file():
+    delete_resource(PDF_UNICODE_FILE_NAME)
+
 
 _QT4_OPEN_METHOD = 'PyQt4.QtGui.QFileDialog.getOpenFileName'
 _QT4_WARNING_DIALOG = 'PyQt4.QtGui.QMessageBox.warning'

=== modified file 'slidepresenterview/tests/unit/docformats/tests_pdf.py'
--- slidepresenterview/tests/unit/docformats/tests_pdf.py	2010-07-11 17:27:55 +0000
+++ slidepresenterview/tests/unit/docformats/tests_pdf.py	2011-01-06 06:25:00 +0000
@@ -25,7 +25,7 @@
 import os
 import unittest2
 from nose.tools import assert_equals, assert_true
-from mock import Mock, sentinel, patch_new, patch
+from mock import Mock, patch
 
 from PyQt4 import QtCore
 

=== added file 'slidepresenterview/tests/unit/ui/widgets/tests_screen.py'
--- slidepresenterview/tests/unit/ui/widgets/tests_screen.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/tests/unit/ui/widgets/tests_screen.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+"""
+Unit tests for slidepresenterview.ui.widgets.screen.
+"""
+import unittest2
+
+from mock import Mock, patch
+
+from PyQt4 import QtCore
+
+from slidepresenterview.ui.widgets.screen import ScreenDescription, Resolution, ScreenProviderQt
+
+
+class TestResolution(unittest2.TestCase):
+    """
+    Tests for the class Resolution.
+    """
+    
+    def setUp(self):
+        self.resolution_100_x_200 = Resolution(100, 200)
+        
+        
+    def should_have_correct_string_representation(self):
+        self.assertEqual(str(self.resolution_100_x_200), "100x200")
+        
+        
+class TestScreenDescription(unittest2.TestCase):
+    """
+    Tests for the class ScreenDescription.
+    """
+    
+    def setUp(self):
+        self.screen = ScreenDescription(2, Resolution(100, 200))
+        
+        
+    def should_have_correct_string_representation(self):
+        self.assertEqual(str(self.screen), "Screen #2 (100x200)")
+        
+
+class TestScreenProviderQt(unittest2.TestCase):
+    """
+    Tests for the class ScreenProviderQt.
+    """
+    
+    def setUp(self):
+        self.mock_info_provider = Mock(spec=['numScreens', 'screenGeometry', 'screenNumber'])
+        self.mock_info_provider.numScreens.return_value = 2
+        self.mock_info_provider.screenGeometry.return_value = QtCore.QRect(0,0,100,200)
+        self.mock_info_provider.screenNumber.return_value = 0
+        with patch('PyQt4.QtCore.QObject.connect'):
+            self.screen_provider = ScreenProviderQt(self.mock_info_provider)
+        
+        
+    def should_give_two_screen_descriptions(self):
+        screen_descriptions = self.screen_provider.get_screen_descriptions()
+        
+        self.assertEqual(2, len(screen_descriptions))
+        self.assertEqual(ScreenDescription(1, Resolution(100, 200)), screen_descriptions[0])
+        self.assertEqual(ScreenDescription(2, Resolution(100, 200)), screen_descriptions[1])
+
+    def should_give_correct_first_screen_description(self):
+        screen_description = self.screen_provider.get_screen_description(1)
+        self.assertEqual(ScreenDescription(1, Resolution(100, 200)), screen_description)
+        
+        
+    def should_give_correct_second_screen_description(self):
+        screen_description = self.screen_provider.get_screen_description(2)
+        self.assertEqual(ScreenDescription(2, Resolution(100, 200)), screen_description)
+
+
+    def should_give_correct_screen_description_when_getting_screen_containing_a_widget(self):
+        screen_description = self.screen_provider.get_screen_description_containing(Mock())
+        self.assertEqual(ScreenDescription(1, Resolution(100, 200)), screen_description)
+
+
+if __name__ == "__main__":
+    from slidepresenterview.tests import MultiplePrefixesTestLoader
+    unittest2.main(testLoader=MultiplePrefixesTestLoader())

=== modified file 'slidepresenterview/tests/unit/ui/widgets/tests_slideview.py'
--- slidepresenterview/tests/unit/ui/widgets/tests_slideview.py	2010-07-11 17:27:55 +0000
+++ slidepresenterview/tests/unit/ui/widgets/tests_slideview.py	2011-01-06 06:25:00 +0000
@@ -31,8 +31,6 @@
 
 from slidepresenterview.utils import pyqt as pyqtutils
 
-from slidepresenterview import tests
-from slidepresenterview.docformats import errors
 from slidepresenterview.docformats.format_pdf import PDFDocumentQt
 from slidepresenterview.ui.presentation import Presentation
 from slidepresenterview.ui.widgets.slideview import SlideViewStretchable
@@ -54,14 +52,13 @@
     """
     
     def setUp(self):
+        self.app = pyqtutils.get_application()
         QtCore.qInstallMsgHandler(pyqtutils.msg_handler_no_message)
-        self.app = QtGui.QApplication([])
         
         self.view = SlideViewStretchable()
         
         
     def tearDown(self):
-        self.app.quit()
         QtCore.qInstallMsgHandler(None)
         
         
@@ -117,8 +114,8 @@
            * Create a mock presentation associated to this slide view.
            * The mock presentation contains a mock document.
         """
+        self.app = pyqtutils.get_application()
         QtCore.qInstallMsgHandler(pyqtutils.msg_handler_no_message)
-        self.app = QtGui.QApplication([])
         
         self.view = SlideViewStretchable()
         self.mock_presentation = Mock(spec=Presentation)
@@ -128,7 +125,6 @@
         
         
     def tearDown(self):
-        self.app.quit()
         QtCore.qInstallMsgHandler(None)
         
         

=== added directory 'slidepresenterview/tests/unit/utils'
=== added file 'slidepresenterview/tests/unit/utils/tests_observation.py'
--- slidepresenterview/tests/unit/utils/tests_observation.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/tests/unit/utils/tests_observation.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 F.-A. Bourbonnais <bouf10pub _AT@. rubico.info>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+Tests for `observation` module.
+"""
+
+__all__ = ['ObservableTestCase']
+
+
+import unittest2
+
+from mock import Mock
+
+from slidepresenterview.utils.observation import Observable
+
+
+EVENT = 'on_some_event'
+
+
+
+class ObservableTestCase(unittest2.TestCase):
+
+
+    def assert_received_notification(self, observer, event):
+        attr = getattr(observer, event, Mock())
+        self.assertTrue(attr.called)
+
+    def assert_not_received_notification(self, observer, event):
+        attr = getattr(observer, event, Mock())
+        self.assertFalse(attr.called)
+
+
+    def assert_is_register_in(self, observer, observable):
+        self.assertIn(observer, observable._observers,
+                      "'%s' should be an observer." % observer)
+
+    def assert_is_not_register_in(self, observer, observable):
+        self.assertNotIn(observer, observable._observers,
+                         "'%s' should not be an observer." % observer)
+
+
+
+class TestObserverGivenNoObserver(ObservableTestCase):
+
+    def setUp(self):
+        self.observer = Mock(spec=[EVENT])
+        self.observable = Observable()
+
+
+    def test_unregister_object_should_do_nothing(self):
+        obj = object()
+        self.observable.unregister_observer(obj)
+
+
+    def test_register_object_should_add_observer(self):
+        self.observable.register_observer(self.observer)
+        self.assert_is_register_in(self.observer, self.observable)
+
+
+    def test_notify_should_do_nothing(self):
+        self.observable._notify_observers(EVENT) #pylint: disable-msg=W0212
+        self.assert_not_received_notification(self.observer, EVENT)
+
+
+class TestObserverGivenSomeObservers(ObservableTestCase):
+
+    def setUp(self):
+        self.observer = Mock(spec=[EVENT])
+        self.other_observer = Mock(spec=[EVENT])
+        self.observable = Observable()
+        self.observable.register_observer(self.observer)
+        self.observable.register_observer(self.other_observer)
+
+
+    def test_register_twice_should_do_nothing(self):
+        self.observable.register_observer(self.observer) #Already registered
+
+        #pylint: disable-msg=W0212
+        self.assertEquals(2, len(self.observable._observers))
+
+
+    def test_unregister_observer_should_remove_it(self):
+        self.observable.unregister_observer(self.observer)
+        self.assert_is_not_register_in(self.observer, self.observable)
+
+
+    def test_unregister_observer_should_remove_only_it(self):
+        self.observable.unregister_observer(self.observer)
+        self.assert_is_register_in(self.other_observer, self.observable)
+
+
+    def should_notify_all_observers(self):
+        self.observable._notify_observers(EVENT) #pylint: disable-msg=W0212
+        self.assert_received_notification(self.observer, EVENT)
+        self.assert_received_notification(self.other_observer, EVENT)
+
+
+    def should_be_able_to_unregister_and_register_again(self):
+        self.observable.unregister_observer(self.observer)
+        self.observable.register_observer(self.observer)
+
+        self.assert_is_register_in(self.observer, self.observable)
+
+
+    def should_ignore_observer_that_doesnt_handle_the_event(self):
+        obj = Mock(spec=[])
+        self.observable.register_observer(obj)
+        self.observable._notify_observers(EVENT) #pylint: disable-msg=W0212
+
+        self.assert_not_received_notification(obj, EVENT)
+
+
+# pylint: disable-msg=R0904,C0103
+class TestObserverGivenUnregisteringAllObservers(ObservableTestCase):
+
+    def setUp(self):
+        self.observer = Mock(spec=[EVENT])
+        self.observable = Observable()
+        self.observable.register_observer(self.observer)
+        self.observable.unregister_observer(self.observer)
+
+
+    def should_be_able_to_register_one(self):
+        obj = object()
+        self.observable.register_observer(obj)
+
+        self.assert_is_register_in(obj, self.observable)
+
+
+    def test_notify_should_do_nothing(self):
+        self.observable._notify_observers(EVENT) #pylint: disable-msg=W0212
+        self.assert_not_received_notification(self.observer, EVENT)
+
+
+
+if __name__ == "__main__":
+    from slidepresenterview.tests import MultiplePrefixesTestLoader
+    unittest2.main(testLoader=MultiplePrefixesTestLoader())

=== modified file 'slidepresenterview/ui/console_window.py'
--- slidepresenterview/ui/console_window.py	2010-05-02 23:36:17 +0000
+++ slidepresenterview/ui/console_window.py	2011-01-06 06:25:00 +0000
@@ -26,69 +26,88 @@
 ##########
 # Imports
 #####
-from PyQt4 import QtCore # pylint: disable-msg=E0611
 from PyQt4 import QtGui
+from PyQt4 import QtCore
 
 from slidepresenterview.ui.qt_forms.console import Ui_console_window
+from slidepresenterview.ui.widgets.preferred_projectors import PreferredProjectors
 from slidepresenterview.utils import pyqt as pyqtutils
 
-class ConsoleWindow(QtGui.QMainWindow, # pylint: disable-msg=E1101
+class ConsoleWindow(QtGui.QMainWindow, 
                     Ui_console_window): 
     """
     The main console window for SlidePresenterView. 
     It is the presenter's console that allows to control projectors.
     
-    @ivar presentation_mediator: a presentation mediator. 
+    :ivar presentation_mediator: a presentation mediator. 
     """
 
-    def __init__(self, presentation_mediator):
-        """
-        Constructor. Set up the UI and manage the presentation mediator.
-        
-        @param presentation_mediator: the presentation mediator.
-        """
-        QtGui.QMainWindow.__init__(self) # pylint: disable-msg=E1101
+    def __init__(self, user_config, presentation_mediator, screen_provider):
+        QtGui.QMainWindow.__init__(self)
         Ui_console_window.__init__(self)
         
         self.setupUi(self)
+        self.user_config = user_config
+        self.screen_provider = screen_provider
+        
         self.set_presentation_mediator(presentation_mediator)
         presentation_mediator.register_console_window(self)
-        self.current_slide_view.set_presentation_mediator(\
-                                                    presentation_mediator)
-        presentation_mediator.register_current_slide_view(\
-                                                    self.current_slide_view)
+        self.current_slide_view.set_presentation_mediator(presentation_mediator)
+        presentation_mediator.register_current_slide_view(self.current_slide_view)
         
         # Disables buttons by default (mainly because we must be sure that
         # the workaround for some versions of Qt is applied).
         pyqtutils.set_button_enabled(self.current_slide_next_button, False)
         pyqtutils.set_button_enabled(self.current_slide_previous_button, False)
         
-    
+        self._set_shortcuts_for_actions()
+        
+        
+    def _set_shortcuts_for_actions(self):
+        """
+        .. note:: 
+           
+           We do not use QtCreator or designer to do this because
+           it does not seem to handle standard shortcuts and 
+           multiple shortcuts.
+        """
+        self.action_open.setShortcut(QtGui.QKeySequence.Open)
+        self.action_exit.setShortcut(QtGui.QKeySequence.Close)
+        self.action_show_presentation.setShortcuts([QtCore.Qt.Key_F5, 
+                                                    QtCore.Qt.CTRL + QtCore.Qt.Key_L])
+        self.action_end_presentation.setShortcuts([QtCore.Qt.Key_Escape,
+                                                   QtCore.Qt.CTRL + QtCore.Qt.Key_E])
+        
+        
     def set_presentation_mediator(self, presentation):
         """
         Set the presentation mediator.
-        @param presentation: the presentation mediator.
+        
+        :param presentation: the presentation mediator.
         """
         self.presentation_mediator = presentation # pylint: disable-msg=W0201
         
         
     def on_action_exit_triggered(self, checked=None):
         """
-        Actions to be taken when the exit button is triggered.
+        Actions to be taken when the :guilabel:`exit` button is triggered.
         """
-        if checked is None: return
-        self.close() # pylint: disable-msg=E1101
+        if checked is None: 
+            return
+        self.close() 
         
         
     def on_action_open_triggered(self, checked=None):
         """
-        Actions to be taken when the open button is triggered.
+        Actions to be taken when the :guilabel:`open` button is triggered.
         """
-        if checked is None: return
+        if checked is None: 
+            return
+        
         filename_qstr = \
-                QtGui.QFileDialog.getOpenFileName( # pylint: disable-msg=E1101
+                QtGui.QFileDialog.getOpenFileName(
                                  self, "Open file",  
-                                 QtCore.QString(), # pylint: disable-msg=E1101
+                                 QtCore.QString(),
                                  "PDF file (*.pdf);;")
         filename = unicode(filename_qstr)
         if filename is None or len(filename) < 1:
@@ -104,13 +123,13 @@
         self._enable_next_previous_button()
         
             
-    def on_current_slide_previous_button_clicked( # pylint: disable-msg=C0103
-                                                 self, 
+    def on_current_slide_previous_button_clicked(self, 
                                                  checked=None):
         """
-        Actions to be taken when the previous button is triggered.
+        Actions to be taken when the :guilabel:`previous` button is triggered.
         """
-        if checked is None: return
+        if checked is None: 
+            return
         
         page = self.presentation_mediator.current_page_no
         doc = self.presentation_mediator.document
@@ -126,13 +145,13 @@
         self.presentation_mediator.go_to_slide(page - 1)
         
         
-    def on_current_slide_next_button_clicked( # pylint: disable-msg=C0103
-                                             self, 
+    def on_current_slide_next_button_clicked(self, 
                                              checked=None):
         """
-        Actions to be taken when the next button is triggered.
+        Actions to be taken when the :guilabel:`next` button is triggered.
         """
-        if checked is None: return
+        if checked is None: 
+            return
         
         page = self.presentation_mediator.current_page_no
         doc = self.presentation_mediator.document
@@ -156,30 +175,36 @@
     
     def _enable_next_previous_button(self):
         """
-        Decide to enable/disable the next and previous buttons.
+        Decide to enable/disable the :guilabel:`next` and :guilabel:`previous` buttons.
         """
         page = self.presentation_mediator.current_page_no
         doc = self.presentation_mediator.document
         
         if doc is None or not self.presentation_mediator.is_file_opened():
-            pyqtutils.set_button_enabled(self.current_slide_next_button, 
-                                         False)
-            pyqtutils.set_button_enabled(self.current_slide_previous_button, 
-                                         False)
+            pyqtutils.set_button_enabled(self.current_slide_next_button, False)
+            pyqtutils.set_button_enabled(self.current_slide_previous_button, False)
             return
         
         doc_page_count = doc.get_page_count()
             
         if page < doc_page_count and page > 0:
-            pyqtutils.set_button_enabled(self.current_slide_next_button, 
-                                         True)
+            pyqtutils.set_button_enabled(self.current_slide_next_button, True)
         else:
-            pyqtutils.set_button_enabled(self.current_slide_next_button, 
-                                         False)
+            pyqtutils.set_button_enabled(self.current_slide_next_button, False)
                 
         if page > 1 and page <= doc_page_count:
-            pyqtutils.set_button_enabled(self.current_slide_previous_button, 
-                                         True)
+            pyqtutils.set_button_enabled(self.current_slide_previous_button, True)
         else:
-            pyqtutils.set_button_enabled(self.current_slide_previous_button, 
-                                         False)
+            pyqtutils.set_button_enabled(self.current_slide_previous_button, False)
+
+
+    def on_action_configure_preferred_projectors_triggered(self, checked=None):
+        """
+        Actions to be taken when the :guilabel:`configure preferred projector(s)` button is 
+        triggered.
+        """
+        if checked is None: 
+            return
+        
+        pref_projectors = PreferredProjectors(self.user_config, self.screen_provider, self, self)
+        pref_projectors.exec_()

=== modified file 'slidepresenterview/ui/presentation.py'
--- slidepresenterview/ui/presentation.py	2010-01-27 06:34:41 +0000
+++ slidepresenterview/ui/presentation.py	2011-01-06 06:25:00 +0000
@@ -31,8 +31,6 @@
 
 from slidepresenterview.docformats import errors
 from slidepresenterview.docformats.format_pdf import PDFDocumentQt
-#from slidepresenterview.ui.console_window import ConsoleWindow
-#from slidepresenterview.ui.widgets.slideview import SlideViewStretchable
 
 class Presentation(object):
     """
@@ -40,22 +38,18 @@
     It allows to notify the presenter view, the projectors and other objects
     of any change in the current presentation.
     
-    @ivar document: Opened document OR None
-    
-    @ivar current_page_no: Current page number OR None if no document opened
-    
-    @ivar console_window: Main console window of the application. 
-    @type console_window: ConsoleWindow object (or None if not set).
-    
-    @ivar current_slide_view: The current slide view for the presentation. 
-    @type current_slide_view: SlideViewStretchable object 
-                              (or None if not set).
+    :ivar document: Opened document OR None
+    
+    :ivar current_page_no: Current page number OR None if no document opened
+    
+    :ivar console_window: Main console window of the application. 
+    :type console_window: ConsoleWindow object (or None if not set).
+    
+    :ivar current_slide_view: The current slide view for the presentation. 
+    :type current_slide_view: SlideViewStretchable object (or None if not set).
     """
 
     def __init__(self):
-        """
-        Initialization of a L{PresentationMediator}. 
-        """
         self.document = None
         self.current_page_no = None
         self.console_window = None
@@ -64,9 +58,7 @@
         
     def number_of_colleagues(self):
         """
-        Returns the number of colleagues currently registered.
-                
-        @return: the number of colleagues currently registered.
+        :return: The number of colleagues currently registered.
         """
         number_of_colleagues = 0
         if self.console_window is not None:
@@ -81,7 +73,7 @@
         Test if the received potential colleague is a colleague of the 
         presentation.
                 
-        @return: A boolean stating if the presentation has the potential 
+        :return: A boolean stating if the presentation has the potential 
                  colleague as a registered colleague.
         """
         return (self.console_window == potential_colleague 
@@ -92,8 +84,8 @@
         """
         Register the console window for the mediator.
         
-        @param console_window: the console window to register.
-        @type console_window: ConsoleWindow object.
+        :param console_window: the console window to register.
+        :type console_window: ConsoleWindow object.
         """
         self.console_window = console_window
 
@@ -102,24 +94,25 @@
         """
         Register the current slide view for the mediator.
         
-        @param current_slide_view: the slide view to register.
-        @type current_slide_view: SlideViewStretchable object.
+        :param current_slide_view: the slide view to register.
+        :type current_slide_view: SlideViewStretchable object.
         """
         self.current_slide_view = current_slide_view
     
 
     def open_presentation(self, file_name, page=1):# pylint: disable-msg=R0911
         """
-        Open the file "file_name" at page "page".
-        
-        @note: The document attribute is changed only if the file
-               can be opened.
-        
-        @param file_name: The file to be opened
-        @type file_name: A unicode string (Python) 
-        @param page: The page number (starting at 1).
-        
-        @return: True on success, False if not (error or document already open)
+        Open a file at a given page.
+        
+        .. note:: 
+        
+           The document attribute is changed only if the file can be opened.
+        
+        :param file_name: The file to be opened
+        :type file_name: A unicode string (Python) 
+        :param page: The page number (starting at 1).
+        
+        :return: True on success, False if not (error or document already open)
         """
         #TODO: open_presentation should not handle exceptions, but must only
         #      propagate it. Also, if the exceptions is just propagated, the
@@ -166,15 +159,15 @@
     
     def go_to_slide(self, page=1):
         """
-        Go to the page "page" of the currently opened document.
-        
-        @note: The current_page_no attribute is modified only if the page
-               can be reached.
-        
-        @param page: The page number (starting at 1).
-        
-        @return: True on success, False if not (invalid page or document 
-                                                not opened).
+        Go to the given page of the currently opened document.
+        
+        .. note:: 
+        
+           The current_page_no attribute is modified only if the page can be reached.
+        
+        :param page: The page number (starting at 1).
+        
+        :return: True on success, False if not (invalid page or document not opened).
         """
         #TODO: go_to_slide should not handle exceptions, but must only
         #      propagate it. Also, if the exceptions is just propagated, the
@@ -206,15 +199,12 @@
 
     
     def is_file_opened(self):
-        """
-        Indicates if a file is currently opened.
-        """
         return self.document is not None
     
     
     def get_file_opened(self):
         """
-        @return: The file path of the document currently opened OR None if
+        :return: The file path of the document currently opened OR None if
                  no document is opened.
         """
         if self.document is None:
@@ -223,7 +213,7 @@
 
     def get_page_for(self, colleague):
         """
-        @return: The current page for the specified colleague OR None if
+        :return: The current page for the specified colleague OR None if
                  no document is opened or the colleague is not registered.
         """
         if self.document is None:

=== added file 'slidepresenterview/ui/widgets/preferred_projectors.py'
--- slidepresenterview/ui/widgets/preferred_projectors.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/ui/widgets/preferred_projectors.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+The preferred projectors configuration widget for SlidePresenterView. 
+"""
+
+from PyQt4 import QtGui
+
+from slidepresenterview.ui.qt_forms.preferred_projectors import Ui_preferred_projectors
+
+
+class PreferredProjectors(QtGui.QDialog, Ui_preferred_projectors): 
+    """
+    The preferred projectors configuration dialog box for SlidePresenterView.
+    """
+
+    def __init__(self, user_config, screen_provider, console_window, parent=None):
+        """
+        :param user_config: User-defined configuration.
+        
+        :param screen_provider: The SPV object that provides access to screens.
+        :type screen_provider: ScreenProviderQt
+        
+        :param console_window: The console to identify in the list of available screens.
+        :type console_window: ConsoleWindow
+        
+        :param parent: The parent of the widget (if it is the default, then
+                       the widget becomes a window).
+        """
+        QtGui.QDialog.__init__(self, parent) # pylint: disable-msg=E1101
+        Ui_preferred_projectors.__init__(self)
+        self.setupUi(self)
+
+        self._user_config = user_config
+        self._screen_provider = screen_provider
+        self._screen_provider.register_observer(self)
+        self._console_window = console_window
+        self._parent = parent
+        self._set_initial_view()
+        
+        
+    def _set_initial_view(self):
+        self._initialize_the_group_of_screen_items_to_be_empty()
+        self._update_group_of_screen_items()
+        self._update_preferred_projectors_options()
+
+        
+    def _initialize_the_group_of_screen_items_to_be_empty(self):
+        self._screen_items = [] # pylint: disable-msg=W0201
+        self._screen_items_spacer = QtGui.QSpacerItem(1, 1, # pylint: disable-msg=W0201 
+                                                      QtGui.QSizePolicy.Minimum, 
+                                                      QtGui.QSizePolicy.Expanding)
+        self.selected_screens_items_layout.addItem(self._screen_items_spacer)
+
+
+    def _update_group_of_screen_items(self):
+        self._clear_old_group_of_screen_items()
+        self._refresh_screen_items_information()
+        self._create_new_screen_items()
+        self._add_new_screen_items_to_group()
+        
+        
+    def _clear_old_group_of_screen_items(self):
+        for screen_item in self._screen_items:
+            self._remove_screen_item_from_group(screen_item)
+        self.selected_screens_items_layout.removeItem(self._screen_items_spacer)
+            
+            
+    def _remove_screen_item_from_group(self, screen_item):
+        self.selected_screens_items_layout.removeWidget(screen_item)
+        screen_item.setParent(None)
+
+
+    def _refresh_screen_items_information(self):
+        # pylint: disable-msg=W0201
+        self._screens_listed = self._screen_provider.get_screen_descriptions()
+       
+    
+    def _create_new_screen_items(self):
+        self._screen_items = [self._create_screen_item(screen) # pylint: disable-msg=W0201
+                              for screen in self._screens_listed]
+
+    
+    def _create_screen_item(self, screen):
+        screen_name = self._identify_if_console_screen(screen) + str(screen)
+        checkbox = QtGui.QCheckBox(screen_name)
+        checkbox.setChecked(
+                        self._user_config.is_screen_selected_as_preferred_projector(screen.number))
+        return checkbox
+
+
+    def _identify_if_console_screen(self, screen):
+        console_screen = self._screen_provider.get_screen_description_containing(
+                                                                            self._console_window)
+        if screen == console_screen:
+            return "(console) "
+        else:
+            return ""
+
+
+    def _add_new_screen_items_to_group(self):
+        for screen_item in self._screen_items:
+            self.selected_screens_items_layout.addWidget(screen_item)
+        self.selected_screens_items_layout.addItem(self._screen_items_spacer)
+
+
+    def _update_preferred_projectors_options(self):
+        self.console_screen_button.setChecked(
+                        self._user_config.is_preferring_presentation_showed_on_console_screen())
+        self.other_screens_button.setChecked(
+                        self._user_config.is_preferring_presentation_showed_on_other_screens())
+        self.selected_screens_button.setChecked(
+                        self._user_config.is_preferring_presentation_showed_on_selected_screens())
+        
+
+    def on_refresh_button_clicked(self, checked=None):
+        """
+        Actions to be taken when the :guilabel:`refresh screens list` button is triggered.
+        """
+        if checked is None: 
+            return
+        self._update_group_of_screen_items()
+        
+
+    def reject(self):
+        """
+        Actions to be taken when the :guilabel:`cancel` button is triggered 
+        and when the window is closed by the close button.
+        """
+        self._screen_provider.unregister_observer(self)
+        QtGui.QDialog.reject(self)
+
+    
+    def accept(self):
+        """
+        Actions to be taken when the :guilabel:`OK` button is triggered.
+        """
+        self._screen_provider.unregister_observer(self)
+        self._set_preferred_projectors_configuration()
+        QtGui.QDialog.accept(self)
+        
+        
+    def _set_preferred_projectors_configuration(self):
+        self._user_config.set_preferred_projectors_configuration(
+          self._get_preferred_projector_type_option(),
+          self._get_all_selected_screens_numbers())
+
+        
+    def _get_preferred_projector_type_option(self):
+        if self.other_screens_button.isChecked():
+            return self._user_config.OPTION_OTHER_SCREENS
+        elif self.selected_screens_button.isChecked():
+            return self._user_config.OPTION_SELECTED_SCREENS
+        else:
+            return self._user_config.OPTION_CONSOLE_SCREEN
+
+
+    def _get_all_selected_screens_numbers(self):
+        return [screen.number for row, screen in enumerate(self._screens_listed) 
+                              if self._screen_items[row].isChecked()]
+
+
+    def on_screen_changed(self):
+        """
+        Actions to be taken when the screen provider warns that the screens changed.
+        """
+        self._update_group_of_screen_items()
\ No newline at end of file

=== added file 'slidepresenterview/ui/widgets/screen.py'
--- slidepresenterview/ui/widgets/screen.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/ui/widgets/screen.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Claude Bolduc <claude.bolduc _AT@. gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+The screen abstraction for SlidePresenterView. 
+"""
+
+##########
+# Imports
+#####
+from PyQt4 import QtCore
+
+from slidepresenterview.utils.observation import Observable
+
+EVENT_SCREEN_CHANGED = 'on_screen_changed'
+
+SPV_FIRST_SCREEN_NUMBER = 1
+
+def convert_to_spv_screen_number(qt_screen_number):
+    return qt_screen_number + SPV_FIRST_SCREEN_NUMBER
+    
+    
+def convert_to_qt_screen_number(spv_screen_number):
+    return spv_screen_number - SPV_FIRST_SCREEN_NUMBER
+
+
+def convert_qrect_to_resolution(qrect):
+    return Resolution(qrect.width(), qrect.height())
+
+
+class ScreenProviderQt(Observable):
+    """
+    Adapter for the QtGui.QDesktopWidget class. 
+    It is responsible to create and manage ScreenDescription objects.
+    
+    **Observable**: To listen to an event, the observer must implement the corresponding method 
+    (see EVENT_* constants). 
+    If the method exists, it will be called. 
+    If not, this observer is simply ignored.
+    
+    :type screen_information_provider: QtGui.QDesktopWidget
+    :param screen_information_provider: In Qt, there is one and only one 
+                                        screen_information_provider that is registered for screen
+                                        modification events.
+                                        It is the one that is linked to the unique 
+                                        QtGui.QApplication. It is accessible via the static method
+                                        desktop() of QtGui.QApplication.
+    """
+
+    def __init__(self, screen_information_provider):
+        Observable.__init__(self)
+        self._screen_information_provider = screen_information_provider
+        self._register_to_events_of_screen_information_provider()
+        
+        
+    def _register_to_events_of_screen_information_provider(self):
+        QtCore.QObject.connect(self._screen_information_provider, 
+                               QtCore.SIGNAL("resized(int)"),
+                               self._notify_screen_changed)
+        QtCore.QObject.connect(self._screen_information_provider, 
+                               QtCore.SIGNAL("screenCountChanged(int)"),
+                               self._notify_screen_changed)
+
+
+    def _notify_screen_changed(self, number):
+        self._notify_observers(EVENT_SCREEN_CHANGED)
+
+
+    def get_screen_descriptions(self):
+        num_screens = self._screen_information_provider.numScreens()
+        return [self._create_screen_description(qt_screen_number) for qt_screen_number 
+                                                                  in range(num_screens)]
+    
+    
+    def _create_screen_description(self, qt_screen_number):
+        spv_screen_number = convert_to_spv_screen_number(qt_screen_number)
+        resolution = self._get_resolution_of(qt_screen_number)
+        return ScreenDescription(spv_screen_number, resolution)
+
+
+    def _get_resolution_of(self, qt_screen_number):
+        resolution_qrect = self._screen_information_provider.screenGeometry(qt_screen_number)
+        return convert_qrect_to_resolution(resolution_qrect)
+    
+
+    def get_screen_description(self, spv_screen_number):
+        return next((screen_description for screen_description in self.get_screen_descriptions() 
+                     if screen_description.number == spv_screen_number), 
+                    None)
+    
+    
+    def get_screen_description_containing(self, widget):
+        qt_screen_number = self._screen_information_provider.screenNumber(widget)
+        spv_screen_number = convert_to_spv_screen_number(qt_screen_number)
+        return self.get_screen_description(spv_screen_number)
+    
+    
+class ScreenDescription(object):
+    """
+    Data transfer object that holds a screen information. 
+    It is immutable.
+    Comparison of ScreenDescriptions is done by comparing attribute values.
+    
+    :ivar number: The screen number identified by SlidePresenterView (spv).
+    
+    :type resolution: Resolution 
+    """
+
+    def __init__(self, number, resolution):
+        self.number = number
+        self.resolution = resolution
+
+
+    def __str__(self):
+        return "Screen #%d (%s)" % (self.number, self.resolution)
+    
+    
+    def __cmp__(self, another_screen):
+        """
+        Comparison is done by checking screen number and resolution (in this order).
+        """
+        if self.number != another_screen.number:
+            return self.number.__cmp__(another_screen.number) 
+        else:
+            return self.resolution.__cmp__(another_screen.resolution)
+
+
+class Resolution(object):
+    """
+    Data transfer object that holds a resolution for a ScreenDescription object. 
+    It is immutable.
+    Comparison of Resolutions is done by comparing attribute values.
+    """
+
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+
+
+    def __str__(self):
+        return "%dx%d" % (self.width, self.height)
+    
+    
+    def __cmp__(self, another_resolution):
+        """
+        Comparison is done by checking width and height (in this order).
+        """
+        if self.width != another_resolution.width:
+            return self.width.__cmp__(another_resolution.width)
+        else:
+            return self.height.__cmp__(another_resolution.height)

=== modified file 'slidepresenterview/ui/widgets/slideview.py'
--- slidepresenterview/ui/widgets/slideview.py	2010-05-03 02:19:38 +0000
+++ slidepresenterview/ui/widgets/slideview.py	2011-01-06 06:25:00 +0000
@@ -23,8 +23,8 @@
 Extension to QGraphicsView that displays a slide using a given format's
 renderer. 
 """
+from PyQt4 import QtGui
 from PyQt4 import QtCore
-from PyQt4 import QtGui
 
 
 class SlideViewStretchable(QtGui.QGraphicsView): # pylint: disable-msg=E1101 

=== added file 'slidepresenterview/utils/observation.py'
--- slidepresenterview/utils/observation.py	1970-01-01 00:00:00 +0000
+++ slidepresenterview/utils/observation.py	2011-01-06 06:25:00 +0000
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# 
+# SlidePresenterView - Console for presenters 
+# https://launchpad.net/slidepresenterview
+#
+# Copyright (C) 2010 Felix-Antoine Bourbonnais <bouf10pub _AT@. rubico.info>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+Add observation behaviour to an object. It allows peers to register to be informed of
+some events.
+"""
+
+__all__ = ['Observable']
+
+
+class Observable(object):
+
+
+    def __init__(self):
+        self._observers = set()
+
+
+    def register_observer(self, observer):
+        self._observers.add(observer)
+
+
+    def unregister_observer(self, observer):
+        self._observers.discard(observer)
+
+
+    def _notify_observers(self, event, *args, **kw):
+        """
+        Notify every observer that has a method matching the name of the event parameter. If
+        not, the observer is ignored. Extra arguments are passed to the event's method.
+        """
+        for observer in self._observers:
+            attr = getattr(observer, event, None)
+            if attr is not None:
+                attr(*args, **kw)

=== modified file 'slidepresenterview/utils/pyqt.py'
--- slidepresenterview/utils/pyqt.py	2009-11-23 05:05:10 +0000
+++ slidepresenterview/utils/pyqt.py	2011-01-06 06:25:00 +0000
@@ -22,9 +22,28 @@
 Helper class/functions for PyQt
 """
 
+import sys
+
+from PyQt4 import QtGui
 from PyQt4 import QtCore
 
 
+_PYQT_APP = None
+def get_application():
+    """
+    :return: The QtGui.QApplication. 
+    
+    .. note::
+       
+       Use this function instead of creating QApplication manually.
+       This will ensure that there will be only one QApplication in the program (singleton).
+    """
+    global _PYQT_APP
+    if _PYQT_APP is None:
+        _PYQT_APP = QtGui.QApplication(sys.argv)
+    return _PYQT_APP
+
+
 def msg_handler_no_message(msg_type, message):
     """
     Don't log any message (except fatal messages)
@@ -37,8 +56,10 @@
     """
     Set a button enabled or not.
     
-    WORKAROUND: This is required because of a bug in QT 4.5.2. In that version
-    disabled buttons are not grayed out...
+    .. warning::
+    
+       WORKAROUND: This is required because of a bug in QT 4.5.2. In that version
+       disabled buttons are not grayed out...
     """
     if enabled:
         button.setEnabled(True)


Follow ups