gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #03030
[Merge] lp:~gtg-user/gtg/backends-window into lp:gtg
Luca Invernizzi has proposed merging lp:~gtg-user/gtg/backends-window into lp:gtg.
Requested reviews:
Gtg developers (gtg)
This merge contains all the code relative to the window used to add, remove and edit backends.
All the functions should be documented.
--
https://code.launchpad.net/~gtg-user/gtg/backends-window/+merge/32642
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/backends-window into lp:gtg.
=== modified file 'CHANGELOG'
--- CHANGELOG 2010-08-04 00:30:22 +0000
+++ CHANGELOG 2010-08-13 23:44:45 +0000
@@ -4,6 +4,7 @@
* Fixed bug with data consistency #579189, by Marko Kevac
* Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
* Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
+ * Added a window to add/delete/edit backends by Luca Invernizzi
2010-03-01 Getting Things GNOME! 0.2.2
* Autostart on login, by Luca Invernizzi
=== added file 'GTG/backends/backendsignals.py'
--- GTG/backends/backendsignals.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backendsignals.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gobject
+
+from GTG.tools.borg import Borg
+
+
+
+class BackendSignals(Borg):
+ '''
+ This class handles the signals that involve backends.
+ In particular, it's a wrapper Borg class around a _BackendSignalsGObject
+ class, and all method of the wrapped class can be used as if they were part
+ of this class
+ '''
+
+ #error codes to send along with the BACKEND_FAILED signal
+ ERRNO_AUTHENTICATION = "authentication failed"
+ ERRNO_NETWORK = "network is down"
+ ERRNO_DBUS = "Dbus interface cannot be connected"
+
+ def __init__(self):
+ '''Checks that this is the only instance, and instantiates the
+ gobject'''
+ super(BackendSignals, self).__init__()
+ if hasattr(self, "_gobject"):
+ return
+ self._gobject = _BackendSignalsGObject()
+
+ def __getattr__(self, attr):
+ '''
+ From outside the class, there should be no difference between self's
+ attributes and self._gobject's attributes.
+ '''
+ if attr == "_gobject" and not "_gobject" in self.__dict__:
+ raise AttributeError
+ return getattr(self._gobject, attr)
+
+
+def signal_type_factory(*args):
+ '''
+ Simply returns a gobject signal type
+
+ @returns tuple
+ '''
+ return (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, args)
+
+
+
+class _BackendSignalsGObject(gobject.GObject):
+
+ #signal name constants
+ BACKEND_STATE_TOGGLED = 'backend-state-toggled' #emitted when a
+ #backend is
+ #enabled or disabled
+ BACKEND_RENAMED = 'backend-renamed' #emitted when a backend is renamed
+ BACKEND_ADDED = 'backend-added'
+ BACKEND_REMOVED = 'backend-added' #when a backend is deleted
+ DEFAULT_BACKEND_LOADED = 'default-backend-loaded' #emitted after all
+ # tasks have been
+ # loaded from the
+ # default backend
+ BACKEND_FAILED = 'backend-failed' #something went wrong with a backend
+ BACKEND_SYNC_STARTED = 'backend-sync-started'
+ BACKEND_SYNC_ENDED = 'backend-sync-ended'
+ INTERACTION_REQUESTED = 'user-interaction-requested'
+
+ INTERACTION_CONFIRM = 'confirm'
+ INTERACTION_TEXT = 'text'
+
+ __gsignals__ = {BACKEND_STATE_TOGGLED : signal_type_factory(str), \
+ BACKEND_RENAMED : signal_type_factory(str), \
+ BACKEND_ADDED : signal_type_factory(str), \
+ BACKEND_REMOVED : signal_type_factory(str), \
+ BACKEND_SYNC_STARTED : signal_type_factory(str), \
+ BACKEND_SYNC_ENDED : signal_type_factory(str), \
+ DEFAULT_BACKEND_LOADED: signal_type_factory(), \
+ BACKEND_FAILED : signal_type_factory(str, str), \
+ INTERACTION_REQUESTED : signal_type_factory(str, str, \
+ str, str)}
+
+ def __init__(self):
+ super(_BackendSignalsGObject, self).__init__()
+ self.backends_currently_syncing = []
+
+ ############# Signals #########
+ #connecting to signals is fine, but keep an eye if you should emit them.
+ #As a general rule, signals should only be emitted in the GenericBackend
+ #class
+
+ def _emit_signal(self, signal, backend_id):
+ gobject.idle_add(self.emit, signal, backend_id)
+
+ def backend_state_changed(self, backend_id):
+ self._emit_signal(self.BACKEND_STATE_TOGGLED, backend_id)
+
+ def backend_renamed(self, backend_id):
+ self._emit_signal(self.BACKEND_RENAMED, backend_id)
+
+ def backend_added(self, backend_id):
+ self._emit_signal(self.BACKEND_ADDED, backend_id)
+
+ def backend_removed(self, backend_id):
+ self._emit_signal(self.BACKEND_REMOVED, backend_id)
+
+ def default_backend_loaded(self):
+ gobject.idle_add(self.emit, self.DEFAULT_BACKEND_LOADED)
+
+ def backend_failed(self, backend_id, error_code):
+ gobject.idle_add(self.emit, self.BACKEND_FAILED, backend_id, \
+ error_code)
+
+ def interaction_requested(self, backend_id, description, \
+ interaction_type, callback_str):
+ gobject.idle_add(self.emit, self.INTERACTION_REQUESTED, \
+ backend_id, description, interaction_type, callback_str)
+
+ def backend_sync_started(self, backend_id):
+ self._emit_signal(self.BACKEND_SYNC_STARTED, backend_id)
+ self.backends_currently_syncing.append(backend_id)
+
+ def backend_sync_ended(self, backend_id):
+ self._emit_signal(self.BACKEND_SYNC_ENDED, backend_id)
+ try:
+ self.backends_currently_syncing.remove(backend_id)
+ except:
+ pass
+
+ def is_backend_syncing(self, backend_id):
+ return backend_id in self.backends_currently_syncing
+
=== removed file 'GTG/backends/backendsignals.py'
--- GTG/backends/backendsignals.py 2010-06-23 12:49:28 +0000
+++ GTG/backends/backendsignals.py 1970-01-01 00:00:00 +0000
@@ -1,127 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Getting Things Gnome! - a personal organizer for the GNOME desktop
-# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
-#
-# 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/>.
-# -----------------------------------------------------------------------------
-
-import gobject
-
-from GTG.tools.borg import Borg
-
-
-
-class BackendSignals(Borg):
- '''
- This class handles the signals that involve backends.
- In particular, it's a wrapper Borg class around a _BackendSignalsGObject
- class, and all method of the wrapped class can be used as if they were part
- of this class
- '''
-
- #error codes to send along with the BACKEND_FAILED signal
- ERRNO_AUTHENTICATION = "authentication failed"
- ERRNO_NETWORK = "network is down"
- ERRNO_DBUS = "Dbus interface cannot be connected"
-
- def __init__(self):
- super(BackendSignals, self).__init__()
- if hasattr(self, "_gobject"):
- return
- self._gobject = _BackendSignalsGObject()
-
- def __getattr__(self, attr):
- if attr == "_gobject" and not "_gobject" in self.__dict__:
- raise AttributeError
- return getattr(self._gobject, attr)
-
-
-class _BackendSignalsGObject(gobject.GObject):
-
- #signal name constants
- BACKEND_STATE_TOGGLED = 'backend-state-toggled' #emitted when a
- #backend is
- #enabled or disabled
- BACKEND_RENAMED = 'backend-renamed' #emitted when a backend is renamed
- BACKEND_ADDED = 'backend-added'
- BACKEND_REMOVED = 'backend-added' #when a backend is deleted
- DEFAULT_BACKEND_LOADED = 'default-backend-loaded' #emitted after all
- # tasks have been
- # loaded from the
- # default backend
- BACKEND_FAILED = 'backend-failed' #something went wrong with a backend
- BACKEND_SYNC_STARTED = 'backend-sync-started'
- BACKEND_SYNC_ENDED = 'backend-sync-ended'
-
- __string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, ))
- __none_signal__ = (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, ( ))
- __string_string_signal__ = (gobject.SIGNAL_RUN_FIRST, \
- gobject.TYPE_NONE, (str, str, ))
-
- __gsignals__ = {BACKEND_STATE_TOGGLED : __string_signal__, \
- BACKEND_RENAMED : __string_signal__, \
- BACKEND_ADDED : __string_signal__, \
- BACKEND_REMOVED : __string_signal__, \
- BACKEND_SYNC_STARTED : __string_signal__, \
- BACKEND_SYNC_ENDED : __string_signal__, \
- DEFAULT_BACKEND_LOADED: __none_signal__, \
- BACKEND_FAILED : __string_string_signal__}
-
- def __init__(self):
- super(_BackendSignalsGObject, self).__init__()
- self.backends_currently_syncing = []
-
- ############# Signals #########
- #connecting to signals is fine, but keep an eye if you should emit them.
- #As a general rule, signals should only be emitted in the GenericBackend
- #class
-
- def _emit_signal(self, signal, backend_id):
- gobject.idle_add(self.emit, signal, backend_id)
-
- def backend_state_changed(self, backend_id):
- self._emit_signal(self.BACKEND_STATE_TOGGLED, backend_id)
-
- def backend_renamed(self, backend_id):
- self._emit_signal(self.BACKEND_RENAMED, backend_id)
-
- def backend_added(self, backend_id):
- self._emit_signal(self.BACKEND_ADDED, backend_id)
-
- def backend_removed(self, backend_id):
- self._emit_signal(self.BACKEND_REMOVED, backend_id)
-
- def default_backend_loaded(self):
- gobject.idle_add(self.emit, self.DEFAULT_BACKEND_LOADED)
-
- def backend_failed(self, backend_id, error_code):
- gobject.idle_add(self.emit, self.BACKEND_FAILED, backend_id, \
- error_code)
-
- def backend_sync_started(self, backend_id):
- self._emit_signal(self.BACKEND_SYNC_STARTED, backend_id)
- self.backends_currently_syncing.append(backend_id)
-
- def backend_sync_ended(self, backend_id):
- self._emit_signal(self.BACKEND_SYNC_ENDED, backend_id)
- try:
- self.backends_currently_syncing.remove(backend_id)
- except:
- pass
-
- def is_backend_syncing(self, backend_id):
- return backend_id in self.backends_currently_syncing
=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py 2010-06-22 19:55:15 +0000
+++ GTG/core/requester.py 2010-08-13 23:44:45 +0000
@@ -284,3 +284,6 @@
def backend_change_attached_tags(self, backend_id, tags):
return self.ds.backend_change_attached_tags(backend_id, tags)
+
+ def save_datastore(self):
+ return self.ds.save()
=== modified file 'GTG/gtk/__init__.py'
--- GTG/gtk/__init__.py 2010-06-02 18:12:23 +0000
+++ GTG/gtk/__init__.py 2010-08-13 23:44:45 +0000
@@ -28,7 +28,9 @@
class ViewConfig:
+
+
current_rep = os.path.dirname(os.path.abspath(__file__))
DELETE_GLADE_FILE = os.path.join(current_rep, "deletion.glade")
PREFERENCES_GLADE_FILE = os.path.join(current_rep, "preferences.glade")
-
+ BACKENDS_GLADE_FILE = os.path.join(current_rep, "backends_dialog.glade")
=== added directory 'GTG/gtk/backends_dialog'
=== added file 'GTG/gtk/backends_dialog.glade'
--- GTG/gtk/backends_dialog.glade 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog.glade 2010-08-13 23:44:45 +0000
@@ -0,0 +1,166 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkWindow" id="backends_dialog">
+ <property name="window_position">mouse</property>
+ <signal name="delete_event" handler="on_BackendsDialog_delete_event"/>
+ <child>
+ <object class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="top_padding">10</property>
+ <property name="bottom_padding">10</property>
+ <property name="left_padding">10</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkHBox" id="big_central_hbox">
+ <property name="visible">True</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkAlignment" id="treeview_window">
+ <property name="height_request">400</property>
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="height_request">30</property>
+ <property name="visible">True</property>
+ <property name="yalign">1</property>
+ <property name="top_padding">20</property>
+ <property name="bottom_padding">10</property>
+ <property name="left_padding">10</property>
+ <property name="right_padding">10</property>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox3">
+ <property name="visible">True</property>
+ <property name="spacing">10</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_add_button_clicked"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_remove_button_clicked"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="central_pane_window">
+ <property name="width_request">450</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vadjustment">adjustment1</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkViewport" id="central_pane1">
+ <property name="visible">True</property>
+ <property name="resize_mode">queue</property>
+ <child>
+ <object class="GtkAlignment" id="central_pane">
+ <property name="visible">True</property>
+ <property name="left_padding">10</property>
+ <property name="right_padding">10</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox2">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="quit_button">
+ <property name="label">gtk-quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_quit_button_clicked"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="upper">100</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ <property name="page_size">10</property>
+ </object>
+</interface>
=== added file 'GTG/gtk/backends_dialog/__init__.py'
--- GTG/gtk/backends_dialog/__init__.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/__init__.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,294 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+This file contains BackendsDialog, a class that manages the window that
+lets you add and configure backends.
+This window is divided in two:
+ - a treeview of the currently loaded backends (the ones added by the user)
+ - a big space, that can be filled by the configuration panel or the add
+ panel (these are called also "views" in this class)
+'''
+
+import gtk
+
+from GTG.gtk import ViewConfig
+from GTG.core import CoreConfig
+from GTG.gtk.backends_dialog.backendstree import BackendsTree
+from GTG.gtk.backends_dialog.addpanel import AddPanel
+from GTG.gtk.backends_dialog.configurepanel import ConfigurePanel
+from GTG.backends import BackendFactory
+from GTG.tools.logger import Log
+from GTG import _
+from GTG.backends.genericbackend import GenericBackend
+
+
+
+class BackendsDialog(object):
+ '''
+ BackendsDialog manages a window that lets you manage and configure backends.
+ It can display two "views", or "panels":
+ - the backend configuration view
+ - the backend adding view
+ '''
+
+
+ def __init__(self, req):
+ '''
+ Initializes the gtk objects and signals.
+ @param req: a Requester object
+ '''
+ self.req = req
+ self._configure_icon_theme()
+ builder = gtk.Builder()
+ self._load_widgets_from_glade(builder)
+ self._create_widgets_for_add_panel()
+ self._create_widgets_for_configure_panel()
+ self._setup_signal_connections(builder)
+ self._create_widgets_for_backends_tree()
+
+########################################
+### INTERFACE WITH THE VIEWMANAGER #####
+########################################
+
+ def activate(self):
+ '''Shows this window, refreshing the current view'''
+ self.config_panel.set_hidden(False)
+ self.dialog.show_all()
+ self.backends_tv.refresh()
+ self.backends_tv.select_backend()
+ self.dialog.present()
+
+ def on_close(self, widget, data = None):
+ '''
+ Hides this window, saving the backends configuration.
+
+ @param widget: not used, here only for using this as signal callback
+ @param data: same as widget, disregard the content
+ '''
+ self.dialog.hide()
+ self.config_panel.set_hidden(True)
+ self.req.save_datastore()
+
+########################################
+### HELPER FUNCTIONS ###################
+########################################
+
+ def get_requester(self):
+ '''
+ Helper function: returns the requester.
+ It's used by the "views" displayed by this class (backend editing and
+ adding views) to access the requester
+ '''
+ return self.req
+
+ def get_pixbuf_from_icon_name(self, name, height, width):
+ '''
+ Helper function: returns a pixbuf of an icon given its name in the
+ loaded icon theme
+
+ @param name: the name of the icon
+ @param height: the height of the returned pixbuf
+ @param width: the width of the returned pixbuf
+
+ @returns gtk.gdk.Pixbuf: a pixbuf containing the wanted icon, or None
+ (if the icon is not present)
+ '''
+ #NOTE: loading icons directly from the theme and scaling them results in
+ # blurry icons. So, instead of doing that, I'm loading them
+ # directly from file.
+ icon_info = self.icon_theme.lookup_icon(name, gtk.ICON_SIZE_MENU, 0)
+ if icon_info == None:
+ return None
+ pixbuf = gtk.gdk.pixbuf_new_from_file(icon_info.get_filename())
+ return pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
+
+ def _show_panel(self, panel_name):
+ '''
+ Helper function to switch between panels.
+
+ @param panel_name: the name of the wanted panel. Choose between
+ "configuration" or "add"
+ '''
+ if panel_name == "configuration":
+ panel_to_remove = self.add_panel
+ panel_to_add = self.config_panel
+ side_is_enabled = True
+ elif panel_name == "add":
+ panel_to_remove = self.config_panel
+ panel_to_add = self.add_panel
+ side_is_enabled = False
+ else:
+ Log.error("panel name unknown")
+ return
+ ##Central pane
+ #NOTE: self.central_pane is the gtk.Container in which we load panels
+ if panel_to_remove in self.central_pane:
+ self.central_pane.remove(panel_to_remove)
+ if not panel_to_add in self.central_pane:
+ self.central_pane.add(panel_to_add)
+ self.central_pane.show_all()
+ #Side treeview
+ # disabled if we're adding a new backend
+ try:
+ #when this is called upon initialization of this class, the
+ # backends_tv object has not been created yet.
+ self.add_button.set_sensitive(side_is_enabled)
+ self.remove_button.set_sensitive(side_is_enabled)
+ self.backends_tv.set_sensitive(side_is_enabled)
+ except AttributeError:
+ pass
+
+########################################
+### WIDGETS AND SIGNALS ################
+########################################
+
+ def _load_widgets_from_glade(self, builder):
+ '''
+ Loads widgets from the glade file
+
+ @param builder: a gtk.Builder
+ '''
+ builder.add_from_file(ViewConfig.BACKENDS_GLADE_FILE)
+ widgets = {
+ 'dialog' : 'backends_dialog',
+ 'treeview_window' : 'treeview_window',
+ 'central_pane' : 'central_pane',
+ 'add_button' : 'add_button',
+ 'remove_button' : 'remove_button',
+ }
+ for attr, widget in widgets.iteritems():
+ setattr(self, attr, builder.get_object(widget))
+
+ def _setup_signal_connections(self, builder):
+ '''
+ Creates some GTK signals connections
+
+ @param builder: a gtk.Builder
+ '''
+ signals = {
+ 'on_add_button_clicked': self.on_add_button,
+ 'on_BackendsDialog_delete_event': self.on_close,
+ 'on_quit_button_clicked': self.on_close,
+ 'on_remove_button_clicked': self.on_remove_button,
+ }
+ builder.connect_signals(signals)
+
+ def _configure_icon_theme(self):
+ '''
+ Inform gtk on the location of the backends icons (which is in
+ the GTG directory tree, and not in the default location for icons
+ '''
+ self.icon_theme = gtk.icon_theme_get_default()
+ for directory in CoreConfig().get_icons_directories():
+ self.icon_theme.prepend_search_path(directory)
+
+ def _create_widgets_for_backends_tree(self):
+ '''
+ Creates the widgets for the lateral treeview displaying the
+ backends the user has added
+ '''
+ self.backends_tv = BackendsTree(self)
+ self.treeview_window.add(self.backends_tv)
+
+ def _create_widgets_for_configure_panel(self):
+ '''simply creates the panel to configure backends'''
+ self.config_panel = ConfigurePanel(self)
+
+ def _create_widgets_for_add_panel(self):
+ '''simply creates the panel to add backends'''
+ self.add_panel = AddPanel(self)
+
+########################################
+### EVENT HANDLING #####################
+########################################
+
+ def on_backend_selected(self, backend_id):
+ '''
+ When a backend in the treeview gets selected, show
+ its configuration pane
+
+ @param backend_id: the id of the selected backend
+ '''
+ if backend_id:
+ self._show_panel("configuration")
+ self.config_panel.set_backend(backend_id)
+ backend = self.req.get_backend(backend_id)
+ self.remove_button.set_sensitive(not backend.is_default())
+
+ def on_add_button(self, widget = None, data = None):
+ '''
+ When the add button is pressed, the add panel is shown
+
+ @param widget: not used, here only for using this as signal callback
+ @param data: same as widget, disregard the content
+ '''
+ self._show_panel("add")
+ self.add_panel.refresh_backends()
+
+ def on_backend_added(self, backend_name):
+ '''
+ When a backend is added, it is created and registered in the Datastore.
+ Also, the configuration panel is shown.
+
+ @param backend_name: the name of the type of the backend to add
+ (identified as BACKEND_NAME in the Backend class)
+ '''
+ backend_id = None
+ #Create Backend
+ backend_dic = BackendFactory().get_new_backend_dict(backend_name)
+ if backend_dic:
+ backend_id = backend_dic["backend"].get_id()
+ backend_dic[GenericBackend.KEY_ENABLED] = False
+ self.req.register_backend(backend_dic)
+ #Restore UI
+ self._show_panel("configuration")
+
+ def show_config_for_backend(self, backend_id):
+ '''
+ Selects a backend in the lateral treeview
+
+ @param backend_id: the id of the backend that must be selected
+ '''
+ self.backends_tv.select_backend(backend_id)
+
+ def on_remove_button(self, widget = None, data = None):
+ '''
+ When the remove button is pressed, a confirmation dialog is shown,
+ and if the answer is positive, the backend is deleted.
+ '''
+ backend_id = self.backends_tv.get_selected_backend_id()
+ if backend_id == None:
+ #no backend selected
+ return
+ backend = self.req.get_backend(backend_id)
+ dialog = gtk.MessageDialog( \
+ parent = self.dialog,
+ flags = gtk.DIALOG_DESTROY_WITH_PARENT,
+ type = gtk.MESSAGE_QUESTION,
+ buttons = gtk.BUTTONS_YES_NO,
+ message_format = \
+ _("Do you really want to remove the backend '%s'?") % \
+ backend.get_human_name())
+ response = dialog.run()
+ dialog.destroy()
+ if response == gtk.RESPONSE_YES:
+ #delete the backend and remove it from the lateral treeview
+ self.req.remove_backend(backend_id)
+ self.backends_tv.remove_backend(backend_id)
=== added file 'GTG/gtk/backends_dialog/addpanel.py'
--- GTG/gtk/backends_dialog/addpanel.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/addpanel.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG.gtk.backends_dialog.backendscombo import BackendsCombo
+from GTG.backends import BackendFactory
+from GTG import _, ngettext
+
+#The code for showing the required modules has been disabled since it
+# seems that backends will be packaged separately (as plugins). I'm
+# leaving this here in case we change that decision (invernizzi).
+#from GTG.tools.moduletopackage import ModuleToPackage
+
+
+
+class AddPanel(gtk.VBox):
+ '''
+ A VBox filled with gtk widgets to let the user choose a new backend.
+ '''
+
+
+ def __init__(self, backends_dialog):
+ '''
+ Constructor, just initializes the gtk widgets
+
+ @param backends_dialog: a reference to the dialog in which this is
+ loaded
+ '''
+ super(AddPanel, self).__init__()
+ self.dialog = backends_dialog
+ self._create_widgets()
+
+ def _create_widgets(self):
+ '''
+ gtk widgets initialization
+ '''
+ #Division of the available space in three segments:
+ # top, middle and bottom.
+ top = gtk.HBox()
+ middle = gtk.HBox()
+ bottom = gtk.HBox()
+ self._fill_top_hbox(top)
+ self._fill_middle_hbox(middle)
+ self._fill_bottom_hbox(bottom)
+ self.pack_start(top, False)
+ self.pack_start(middle, True)
+ self.pack_start(bottom, True)
+
+ def _fill_top_hbox(self, hbox):
+ '''
+ Helper function to fill and hbox with a combobox that lists the
+ available backends and a gtk.Label.
+
+ @param hbox: the gtk.HBox to fill
+ '''
+ label = gtk.Label("Select a backend")
+ label.set_size_request(-1, 30)
+ self.combo_types = BackendsCombo(self.dialog)
+ self.combo_types.child.connect('changed', self.on_combo_changed)
+ hbox.pack_start(label, True, True)
+ hbox.pack_start(self.combo_types, False, True)
+
+ def _fill_middle_hbox(self, hbox):
+ '''
+ Helper function to fill an hbox with a label describing the backend
+ and a gtk.Image (that loads the backend image)
+
+ @param hbox: the gtk.HBox to fill
+ '''
+ self.label_name = gtk.Label("name")
+ self.label_name.set_alignment(xalign = 0.5, yalign = 1)
+ self.label_description = gtk.Label()
+ self.label_description.set_justify(gtk.JUSTIFY_FILL)
+ self.label_description.set_line_wrap(True)
+ self.label_description.set_size_request(300, -1)
+ self.label_description.set_alignment(xalign = 0, yalign = 0.5)
+ self.label_author = gtk.Label("")
+ self.label_author.set_line_wrap(True)
+ self.label_author.set_alignment(xalign = 0, yalign = 0)
+ self.label_modules = gtk.Label("")
+ self.label_modules.set_line_wrap(True)
+ self.label_modules.set_alignment(xalign = 0, yalign = 0)
+ self.image_icon = gtk.Image()
+ self.image_icon.set_size_request(100, 100)
+ align_image = gtk.Alignment(xalign = 1, yalign = 0)
+ align_image.add(self.image_icon)
+ labels_vbox = gtk.VBox()
+ labels_vbox.pack_start(self.label_description, True, True)
+ labels_vbox.pack_start(self.label_author, True, True)
+ labels_vbox.pack_start(self.label_modules, True, True)
+ low_hbox = gtk.HBox()
+ low_hbox.pack_start(labels_vbox, True, True)
+ low_hbox.pack_start(align_image, True, True)
+ vbox = gtk.VBox()
+ vbox.pack_start(self.label_name, True, True)
+ vbox.pack_start(low_hbox, True, True)
+ hbox.pack_start(vbox, True, True)
+
+ def _fill_bottom_hbox(self, hbox):
+ '''
+ Helper function to fill and hbox with a buttonbox, featuring
+ and ok and cancel buttons.
+
+ @param hbox: the gtk.HBox to fill
+ '''
+ cancel_button = gtk.Button(stock = gtk.STOCK_CANCEL)
+ cancel_button.connect('clicked', self.on_cancel)
+ self.ok_button = gtk.Button(stock = gtk.STOCK_OK)
+ self.ok_button.connect('clicked', self.on_confirm)
+ align =gtk.Alignment(xalign = 0.5, \
+ yalign = 1, \
+ xscale = 1)
+ align.set_padding(0, 10, 0, 0)
+ buttonbox = gtk.HButtonBox()
+ buttonbox.set_layout(gtk.BUTTONBOX_EDGE)
+ buttonbox.add(cancel_button)
+ buttonbox.set_child_secondary(cancel_button, False)
+ buttonbox.add(self.ok_button)
+ align.add(buttonbox)
+ hbox.pack_start(align, True, True)
+
+ def refresh_backends(self):
+ '''Populates the combo box containing the available backends'''
+ self.combo_types.refresh()
+
+ def on_confirm(self, widget = None):
+ '''
+ Notifies the dialog holding this VBox that a backend has been
+ chosen
+
+ @param widget: just to make this function usable as a signal callback.
+ Not used.
+ '''
+ backend_name = self.combo_types.get_selected()
+ self.dialog.on_backend_added(backend_name)
+
+ def on_cancel(self, widget = None):
+ '''
+ Aborts the addition of a new backend. Shows the configuration panel
+ previously loaded.
+
+ @param widget: just to make this function usable as a signal callback.
+ Not used.
+ '''
+ self.dialog.show_config_for_backend(None)
+
+ def on_combo_changed(self, widget = None):
+ '''
+ Updates the backend description and icon.
+
+ @param widget: just to make this function usable as a signal callback.
+ Not used.
+ '''
+ backend_name = self.combo_types.get_selected()
+ if backend_name == None:
+ return
+ backend = BackendFactory().get_backend(backend_name)
+ self.label_description.set_markup(backend.Backend.get_description())
+
+ label = _('Syncing is <span color="red">disabled</span>')
+ markup = '<big><big><big><b>%s</b></big></big></big>' % \
+ backend.Backend.get_human_default_name()
+ self.label_name.set_markup(markup)
+ authors = backend.Backend.get_authors()
+ author_txt = '<b>%s</b>:\n - %s' % \
+ (ngettext("Author", "Authors", len(authors)),
+ reduce(lambda a, b: a + "\n" + " - " + b, authors))
+ self.label_author.set_markup(author_txt)
+ #The code for showing the required modules has been disabled since it
+ # seems that backends will be packaged separately (as plugins). I'm
+ # leaving this here in case we change that decision (invernizzi).
+ #self._build_module_list(backend.Backend)
+ pixbuf = self.dialog.get_pixbuf_from_icon_name(backend_name, 100, 100)
+ self.image_icon.set_from_pixbuf(pixbuf)
+ self.show_all()
+
+ #The code for showing the required modules has been disabled since it
+ # seems that backends will be packaged separately (as plugins). I'm
+ # leaving this here in case we change that decision (invernizzi).
+# def _build_module_list(self, backend):
+# missing_modules = []
+# for module in backend.get_required_modules():
+# try:
+# __import__(module)
+# except ImportError:
+# missing_modules.append(module)
+# if missing_modules:
+# text = "<b> Missing modules:</b>\n - "
+# module2package = ModuleToPackage()
+# missing_modules = map(lambda a: \
+# "<span color='red'>" + \
+# module2package.lookup(a) +\
+# "</span>", missing_modules)
+# text += reduce(lambda a, b: a + "\n - " + b, missing_modules)
+# self.label_modules.set_markup(text)
+# self.ok_button.set_sensitive(missing_modules == [])
+
=== added file 'GTG/gtk/backends_dialog/backendscombo.py'
--- GTG/gtk/backends_dialog/backendscombo.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/backendscombo.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG.backends import BackendFactory
+
+
+
+class BackendsCombo(gtk.ComboBoxEntry):
+ '''
+ A combobox listing all the available backends types
+ '''
+
+
+ COLUMN_NAME = 0 #unique name for the backend type. It's never
+ # displayed, it's used to find which backend has
+ # been selected
+ COLUMN_HUMAN_NAME = 1 #human friendly name (which is localized).
+ COLUMN_ICON = 2
+
+ def __init__(self, backends_dialog):
+ '''
+ Constructor, itializes gtk widgets.
+ @param backends_dialog: reference to the dialog in which this combo is
+ loaded.
+ '''
+ super(BackendsCombo, self).__init__()
+ self.dialog = backends_dialog
+ self._liststore_init()
+ self._renderers_init()
+ self.set_size_request(-1, 30)
+ self.show_all()
+
+ def _liststore_init(self):
+ '''Setup the gtk.ListStore'''
+ self.liststore = gtk.ListStore(str, str, gtk.gdk.Pixbuf)
+ self.set_model(self.liststore)
+
+ def _renderers_init(self):
+ '''Configure the cell renderers'''
+ #Text renderer
+ text_cell = gtk.CellRendererText()
+ self.pack_start(text_cell, False)
+ self.set_text_column(self.COLUMN_HUMAN_NAME)
+ #Icon renderer
+ pixbuf_cell = gtk.CellRendererPixbuf()
+ self.pack_start(pixbuf_cell, False)
+ self.add_attribute(pixbuf_cell, "pixbuf", self.COLUMN_ICON)
+
+ def refresh(self):
+ '''
+ Populates the combo box with the available backends
+ '''
+ self.liststore.clear()
+ backend_types = BackendFactory().get_all_backends()
+ for name, module in backend_types.iteritems():
+ pixbuf = self.dialog.get_pixbuf_from_icon_name(name, 16, 16)
+ self.liststore.append((name, \
+ module.Backend.get_human_default_name(), \
+ pixbuf))
+ if backend_types:
+ #triggers a "changed" signal, which is used in the AddPanel to
+ #refresh the backend description and icon
+ self.set_active(0)
+
+ def get_selected(self):
+ '''
+ Returns the name of the selected backend, or None
+ '''
+ selected_iter = self.get_active_iter()
+ if selected_iter:
+ return self.liststore.get_value(selected_iter, \
+ BackendsCombo.COLUMN_NAME)
+ else:
+ return None
=== added file 'GTG/gtk/backends_dialog/backendstree.py'
--- GTG/gtk/backends_dialog/backendstree.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/backendstree.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG.gtk.colors import get_colored_tags_markup
+from GTG.backends.genericbackend import GenericBackend
+from GTG.backends.backendsignals import BackendSignals
+
+
+
+class BackendsTree(gtk.TreeView):
+ '''
+ gtk.TreeView that shows the currently loaded backends.
+ '''
+
+
+ COLUMN_BACKEND_ID = 0 #never shown, used for internal lookup.
+ COLUMN_ICON = 1
+ COLUMN_TEXT = 2 # holds the backend "human-readable" name
+ COLUMN_TAGS = 3
+
+ def __init__(self, backendsdialog):
+ '''
+ Constructor, just initializes the gtk widgets
+
+ @param backends_dialog: a reference to the dialog in which this is
+ loaded
+ '''
+ super(BackendsTree,self).__init__()
+ self.dialog = backendsdialog
+ self.req = backendsdialog.get_requester()
+ self._init_liststore()
+ self._init_renderers()
+ self._init_signals()
+ self.refresh()
+
+ def refresh(self):
+ '''refreshes the gtk.Liststore'''
+ self.backendid_to_iter = {}
+ self.liststore.clear()
+ for backend in self.req.get_all_backends(disabled = True):
+ self.add_backend(backend)
+ self.on_backend_state_changed(None, backend.get_id())
+
+ def on_backend_added(self, sender, backend_id):
+ '''
+ Signal callback executed when a new backend is loaded
+
+ @param sender: not used, only here to let this function be used as a
+ callback
+ @param backend_id: the id of the backend to add
+ '''
+ #Add
+ backend = self.req.get_backend(backend_id)
+ if not backend:
+ return
+ self.add_backend(backend)
+ #Select
+ self.select_backend(backend_id)
+ #Update it's enabled state
+ self.on_backend_state_changed(None, backend.get_id())
+
+ def add_backend(self, backend):
+ '''
+ Adds a new backend to the list
+
+ @param backend_id: the id of the backend to add
+ '''
+ if backend:
+ backend_iter = self.liststore.append([ \
+ backend.get_id(), \
+ self.dialog.get_pixbuf_from_icon_name(backend.get_name(), \
+ 16, 16), \
+ backend.get_human_name(), \
+ self._get_markup_for_tags(backend.get_attached_tags()), \
+ ])
+ self.backendid_to_iter[backend.get_id()] = backend_iter
+
+
+ def on_backend_state_changed(self, sender, backend_id):
+ '''
+ Signal callback executed when a backend is enabled/disabled.
+
+ @param sender: not used, only here to let this function be used as a
+ callback
+ @param backend_id: the id of the backend to add
+ '''
+ if backend_id in self.backendid_to_iter:
+ style = self.get_style()
+ b_iter = self.backendid_to_iter[backend_id]
+ b_path = self.liststore.get_path(b_iter)
+ backend = self.req.get_backend(backend_id)
+ backend_name = backend.get_human_name()
+ if backend.is_enabled():
+ text = backend_name
+ else:
+ color = str(style.text[gtk.STATE_INSENSITIVE])
+ text = "<span color='%s'>%s</span>" % \
+ (color, backend_name)
+ self.liststore[b_path][self.COLUMN_TEXT] = text
+
+ def _get_markup_for_tags(self, tag_names):
+ '''Given a list of tags names, generates the pango markup to render that
+ list with the tag colors used in GTG
+
+ @param tag_names: the list of the tags (strings)
+ @return str: the pango markup string
+ '''
+ if GenericBackend.ALLTASKS_TAG in tag_names:
+ tags_txt = ""
+ else:
+ tags_txt = get_colored_tags_markup(self.req, tag_names)
+ return "<small>" + tags_txt + "</small>"
+
+
+ def remove_backend(self, backend_id):
+ ''' Removes a backend from the treeview, and selects the first (to show
+ something in the configuration panel
+
+ @param backend_id: the id of the backend to remove
+ '''
+ if backend_id in self.backendid_to_iter:
+ self.liststore.remove(self.backendid_to_iter[backend_id])
+ del self.backendid_to_iter[backend_id]
+ self.select_backend()
+
+ def _init_liststore(self):
+ '''Creates the liststore'''
+ self.liststore = gtk.ListStore(object, gtk.gdk.Pixbuf, str, str)
+ self.set_model(self.liststore)
+
+ def _init_renderers(self):
+ '''Initializes the cell renderers'''
+ # We hide the columns headers
+ self.set_headers_visible(False)
+ # For the backend icon
+ pixbuf_cell = gtk.CellRendererPixbuf()
+ tvcolumn_pixbuf = gtk.TreeViewColumn('Icon', pixbuf_cell)
+ tvcolumn_pixbuf.add_attribute(pixbuf_cell, 'pixbuf', self.COLUMN_ICON)
+ self.append_column(tvcolumn_pixbuf)
+ # For the backend name
+ text_cell = gtk.CellRendererText()
+ tvcolumn_text = gtk.TreeViewColumn('Name', text_cell)
+ tvcolumn_text.add_attribute(text_cell, 'markup', self.COLUMN_TEXT)
+ self.append_column(tvcolumn_text)
+ text_cell.connect('edited', self.cell_edited_callback)
+ text_cell.set_property('editable', True)
+ # For the backend tags
+ tags_cell = gtk.CellRendererText()
+ tvcolumn_tags = gtk.TreeViewColumn('Tags', tags_cell)
+ tvcolumn_tags.add_attribute(tags_cell, 'markup', self.COLUMN_TAGS)
+ self.append_column(tvcolumn_tags)
+
+ def cell_edited_callback(self, text_cell, path, new_text):
+ '''If a backend name is changed, it saves the changes in the Backend
+
+ @param text_cell: not used. The gtk.CellRendererText that emitted the
+ signal. Only here because it's passed by the signal
+ @param path: the gtk.TreePath of the edited cell
+ @param new_text: the new name of the backend
+ '''
+ #we strip everything not permitted in backend names
+ new_text = ''.join(c for c in new_text if (c.isalnum() or\
+ c in [" ", "-", "_"]))
+ selected_iter = self.liststore.get_iter(path)
+ # update the backend name
+ backend_id = self.liststore.get_value(selected_iter, \
+ self.COLUMN_BACKEND_ID)
+ backend = self.dialog.get_requester().get_backend(backend_id)
+ if backend:
+ backend.set_human_name(new_text)
+ # update the text in the liststore
+ self.liststore.set(selected_iter, self.COLUMN_TEXT, new_text)
+
+ def _init_signals(self):
+ '''Initializes the backends and gtk signals '''
+ self.connect("cursor-changed", self.on_select_row)
+ _signals = BackendSignals()
+ _signals.connect(_signals.BACKEND_ADDED, self.on_backend_added)
+ _signals.connect(_signals.BACKEND_STATE_TOGGLED,
+ self.on_backend_state_changed)
+
+ def on_select_row(self, treeview = None):
+ '''When a row is selected, displays the corresponding editing panel
+
+ @treeview: not used
+ '''
+ self.dialog.on_backend_selected(self.get_selected_backend_id())
+
+ def _get_selected_path(self):
+ '''
+ Helper function to get the selected path
+
+ @return gtk.TreePath : returns exactly one path for the selected object or
+ None
+ '''
+ selection = self.get_selection()
+ if selection:
+ model, selected_paths = self.get_selection().get_selected_rows()
+ if selected_paths:
+ return selected_paths[0]
+ return None
+
+ def select_backend(self, backend_id = None):
+ '''
+ Selects the backend corresponding to backend_id.
+ If backend_id is none, refreshes the current configuration panel.
+
+ @param backend_id: the id of the backend to select
+ '''
+ if backend_id in self.backendid_to_iter:
+ backend_iter = self.backendid_to_iter[backend_id]
+ selection = self.get_selection()
+ if selection:
+ selection.select_iter(backend_iter)
+ else:
+ if self._get_selected_path():
+ #We just reselect the currently selected entry
+ self.on_select_row()
+ else:
+ #If nothing is selected, we select the first entry
+ self.get_selection().select_path("0")
+ self.dialog.on_backend_selected(self.get_selected_backend_id())
+
+ def get_selected_backend_id(self):
+ '''
+ returns the selected backend id, or none
+
+ @return string: the selected backend id (or None)
+ '''
+ selected_path = self._get_selected_path()
+ if not selected_path:
+ return None
+ selected_iter = self.liststore.get_iter(selected_path)
+ return self.liststore.get_value(selected_iter, self.COLUMN_BACKEND_ID)
=== added file 'GTG/gtk/backends_dialog/configurepanel.py'
--- GTG/gtk/backends_dialog/configurepanel.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/configurepanel.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,298 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG.gtk.colors import get_colored_tags_markup
+from GTG import _, ngettext
+from GTG.backends.genericbackend import GenericBackend
+from GTG.gtk.backends_dialog.parameters_ui import ParametersUI
+from GTG.backends.backendsignals import BackendSignals
+
+
+class ConfigurePanel(gtk.VBox):
+ '''
+ A VBox that lets you configure a backend
+ '''
+
+
+ def __init__(self, backends_dialog):
+ '''
+ Constructor, creating all the gtk widgets
+
+ @param backends_dialog: a reference to the dialog in which this is
+ loaded
+ '''
+ super(ConfigurePanel, self).__init__()
+ self.dialog = backends_dialog
+ self.should_spinner_be_shown = False
+ self.task_deleted_handle = None
+ self.task_added_handle = None
+ self.req = backends_dialog.get_requester()
+ self._create_widgets()
+ self._connect_signals()
+
+ def _connect_signals(self):
+ ''' Connects the backends generated signals '''
+ _signals = BackendSignals()
+ _signals.connect(_signals.BACKEND_RENAMED, self.refresh_title)
+ _signals.connect(_signals.BACKEND_STATE_TOGGLED, \
+ self.refresh_sync_status)
+ _signals.connect(_signals.BACKEND_SYNC_STARTED, self.on_sync_started)
+ _signals.connect(_signals.BACKEND_SYNC_ENDED, self.on_sync_ended)
+
+ def _create_widgets(self):
+ '''
+ This function fills this Vbox with widgets
+ '''
+ #Division of the available space in three segments:
+ # top, middle and bottom
+ top = gtk.HBox()
+ middle = gtk.HBox()
+ self._fill_top_hbox(top)
+ self._fill_middle_hbox(middle)
+ self.pack_start(top, False)
+ self.pack_start(middle, False)
+ align = gtk.Alignment(xalign = 0, yalign = 0, xscale = 1)
+ align.set_padding(10, 0, 0, 0)
+ self.parameters_ui = ParametersUI(self.req)
+ align.add(self.parameters_ui)
+ self.pack_start(align, False)
+
+ def _fill_top_hbox(self, hbox):
+ '''
+ Helper function to fill an hbox with an image, a spinner and
+ three labels
+
+ @param hbox: the gtk.HBox to fill
+ '''
+ hbox.set_spacing(10)
+ self.image_icon = gtk.Image()
+ self.image_icon.set_size_request(100, 100)
+ vbox = gtk.VBox()
+ hbox_top = gtk.HBox()
+ self.human_name_label = gtk.Label()
+ self.human_name_label.set_alignment(xalign = 0, yalign = 0.5)
+ self.spinner = gtk.Spinner()
+ self.spinner.set_size_request(32, 32)
+ self.spinner.connect("show", self.on_spinner_show)
+ align_spin = gtk.Alignment(xalign = 1, yalign = 0)
+ align_spin.add(self.spinner)
+ hbox_top.pack_start(self.human_name_label, True)
+ hbox_top.pack_start(align_spin, False)
+ self.sync_desc_label = gtk.Label()
+ self.sync_desc_label.set_alignment(xalign = 0, yalign = 1)
+ self.sync_desc_label.set_line_wrap(True)
+ vbox.pack_start(hbox_top, True)
+ vbox.pack_start(self.sync_desc_label, True)
+ hbox.pack_start(self.image_icon, False)
+ align_vbox = gtk.Alignment(xalign = 0, yalign = 0, xscale = 1)
+ align_vbox.set_padding(10, 0, 20, 0)
+ align_vbox.add(vbox)
+ hbox.pack_start(align_vbox, True)
+
+ def _fill_middle_hbox(self, hbox):
+ '''
+ Helper function to fill an hbox with a label and a button
+
+ @param hbox: the gtk.HBox to fill
+ '''
+ self.sync_status_label = gtk.Label()
+ self.sync_status_label.set_alignment(xalign = 0.8, yalign = 0.5)
+ self.sync_button = gtk.Button()
+ self.sync_button.connect("clicked", self.on_sync_button_clicked)
+ hbox.pack_start(self.sync_status_label, True)
+ hbox.pack_start(self.sync_button, True)
+
+ def set_backend(self, backend_id):
+ '''Changes the backend to configure, refreshing this view.
+
+ @param backend_id: the id of the backend to configure
+ '''
+ self.backend = self.dialog.get_requester().get_backend(backend_id)
+ self.refresh_title()
+ self.refresh_sync_status()
+ self.parameters_ui.refresh(self.backend)
+ self.image_icon.set_from_pixbuf(self.dialog.get_pixbuf_from_icon_name(\
+ self.backend.get_name(), 80, 80))
+
+ def refresh_title(self, sender = None, data = None):
+ '''
+ Callback for the signal that notifies backends name changes. It changes
+ the title of this view
+
+ @param sender: not used, here only for signal callback compatibility
+ @param data: not used, here only for signal callback compatibility
+ '''
+ markup = "<big><big><big><b>%s</b></big></big></big>" % \
+ self.backend.get_human_name()
+ self.human_name_label.set_markup(markup)
+
+ def refresh_number_of_tasks(self):
+ '''refreshes the number of synced tasks by this backend'''
+ #FIXME: disabled for now. I'm not sure that this is nice because the
+ # count is correct only after the backend has synced all the pending
+ # tasks, and this is quite misleading (invernizzi)
+ return
+ #This will have to be changed for import/export..
+ tags = self.backend.get_attached_tags()
+ tasks_number = self.backend.get_number_of_tasks()
+ if GenericBackend.ALLTASKS_TAG in tags:
+ if tasks_number == 0:
+ markup = _("Ready to start syncing")
+ else:
+ markup = ngettext("Syncing your only task", \
+ "Syncing all %d tasks" % tasks_number, tasks_number)
+ else:
+ tags_txt = get_colored_tags_markup(self.req, tags)
+ if tasks_number == 0:
+ markup = _("There's no task tagged %s") % tags_txt
+ else:
+ markup = ngettext("Syncing a task tagged %s" % tags_txt, \
+ "Syncing %d tasks tagged %s" % (tasks_number, tags_txt), \
+ tasks_number)
+ self.sync_desc_label.set_markup(markup)
+
+ def refresh_sync_button(self):
+ '''
+ Refreshes the state of the button that enables the backend
+ '''
+ self.sync_button.set_sensitive(not self.backend.is_default())
+ if self.backend.is_enabled():
+ label = _("Disable syncing")
+ else:
+ label = _("Enable syncing")
+ self.sync_button.set_label(label)
+
+ def refresh_sync_status_label(self):
+ '''
+ Refreshes the gtk.Label that shows the current state of this backend
+ '''
+ if self.backend.is_default():
+ label = _("This is the default backend")
+ else:
+ if self.backend.is_enabled():
+ label = _("Syncing is enabled")
+ else:
+ label = _('Syncing is <span color="red">disabled</span>')
+ self.sync_status_label.set_markup(label)
+
+ def refresh_sync_status(self, sender = False, data = False):
+ '''Signal callback function, called when a backend state
+ (enabled/disabled) changes. Refreshes this view.
+
+ @param sender: not used, here only for signal callback compatibility
+ @param data: not used, here only for signal callback compatibility
+ '''
+ self.refresh_number_of_tasks()
+ self.refresh_sync_button()
+ self.refresh_sync_status_label()
+
+ def set_hidden(self, is_hidden):
+ '''
+ Notifies this pane if it's hidden or not. We disconnect signals when
+ hidden, since there is no need to keep the UI updated.
+ Hopefully, this should make GTG faster :)
+
+ @param is_hidden: boolean, True if the window is not visible
+ '''
+ #These is only needed to refresh the number of synced tasks.
+ #since that is disabled for now, there is no need for this
+
+# if is_hidden:
+# if self.task_added_handle:
+# self.req.disconnect(self.task_added_handle)
+# self.task_added_handle = None
+# if self.task_deleted_handle:
+# self.req.disconnect(self.task_deleted_handle)
+# self.task_deleted_handle = None
+# else:
+# self.task_added_handle = self.req.connect("task-added", \
+# self.__on_task_changed)
+# self.task_added_handle = self.req.connect("task-modified", \
+# self.__on_task_changed)
+# self.task_deleted_handle = self.req.connect("task-deleted", \
+# self.__on_task_changed)
+#
+# def __on_task_changed(self, sender, task_id):
+# '''
+# If tasks are added, modified or removed, updates the number of
+# tasks of the current backend
+# '''
+# self.refresh_sync_status()
+
+ def on_sync_button_clicked(self, sender):
+ '''
+ Signal callback when a backend is enabled/disabled via the UI button
+
+ @param sender: not used, here only for signal callback compatibility
+ '''
+ self.parameters_ui.commit_changes()
+ self.req.set_backend_enabled(self.backend.get_id(), \
+ not self.backend.is_enabled())
+
+ def on_sync_started(self, sender, backend_id):
+ '''
+ If the backend has started syncing tasks, update the state of the
+ gtk.Spinner
+
+ @param sender: not used, here only for signal callback compatibility
+ @param backend_id: the id of the backend that emitted this signal
+ '''
+ if backend_id == self.backend.get_id():
+ self.spinner_set_active(True)
+
+ def on_sync_ended(self, sender, backend_id):
+ '''
+ If the backend has stopped syncing tasks, update the state of the
+ gtk.Spinner
+
+ @param sender: not used, here only for signal callback compatibility
+ @param backend_id: the id of the backend that emitted this signal
+ '''
+
+ if backend_id == self.backend.get_id():
+ self.spinner_set_active(False)
+
+ def on_spinner_show(self, sender):
+ '''This signal callback hides the spinner if it's not supposed to be
+ seen. It's a workaround to let us call show_all on the whole window
+ while keeping this hidden (it's the only widget that requires special
+ attention)
+
+ @param sender: not used, here only for signal callback compatibility
+ '''
+ if self.should_spinner_be_shown == False:
+ self.spinner.hide()
+
+ def spinner_set_active(self, active):
+ '''
+ Enables/disables the gtk.Spinner, while showing/hiding it at the same
+ time
+
+ @param active: True if the spinner should spin
+ '''
+ self.should_spinner_be_shown = active
+ if active:
+ self.spinner.start()
+ self.spinner.show()
+ else:
+ self.spinner.hide()
+ self.spinner.stop()
+
=== added directory 'GTG/gtk/backends_dialog/parameters_ui'
=== added file 'GTG/gtk/backends_dialog/parameters_ui/__init__.py'
--- GTG/gtk/backends_dialog/parameters_ui/__init__.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/__init__.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+'''
+This modules reads a bakcn configuration and generates a series of widgets to
+let the user see the configuration and modify it.
+In this manner, backends do not need to know anything about their UI since it's
+built for them: it should play along the lines of the separation between GTG
+server and client
+'''
+
+#FIXME: all the parameters have one function in common (2 lines total).
+# Evaluate if there is a clean way to avoid duplication of this code,
+# without becoming too difficult to understand.
+# (invernizzi)
+
+import gtk
+import functools
+
+from GTG import _
+from GTG.backends.genericbackend import GenericBackend
+from GTG.gtk.backends_dialog.parameters_ui.importtagsui import ImportTagsUI
+from GTG.gtk.backends_dialog.parameters_ui.textui import TextUI
+from GTG.gtk.backends_dialog.parameters_ui.passwordui import PasswordUI
+from GTG.gtk.backends_dialog.parameters_ui.periodui import PeriodUI
+from GTG.gtk.backends_dialog.parameters_ui.checkboxui import CheckBoxUI
+from GTG.gtk.backends_dialog.parameters_ui.pathui import PathUI
+
+
+
+class ParametersUI(gtk.VBox):
+ '''
+ Given a bakcend, this gtk.VBox populates itself with all the necessary
+ widgets to view and edit a backend configuration
+ '''
+
+
+ COMMON_WIDTH = 150
+
+ def __init__(self, requester):
+ '''Constructs the list of the possible widgets.
+
+ @param requester: a GTG.core.requester.Requester object
+ '''
+ super(ParametersUI, self).__init__(False)
+ self.req = requester
+ self.set_spacing(10)
+
+ #builds a list of widget generators. More precisely, it's a
+ # list of tuples: (backend_parameter_name, widget_generator)
+ self.parameter_widgets = ( \
+ ("import-tags", self.UI_generator(ImportTagsUI, \
+ {"title": _("Import tags"), \
+ "anybox_text": _("All tags"), \
+ "somebox_text": _("Just these tags"), \
+ "parameter_name": "import-tags"}) \
+ ),\
+ ("attached-tags", self.UI_generator(ImportTagsUI, \
+ {"title": _("Tags to sync"), \
+ "anybox_text": _("All tasks"), \
+ "somebox_text": _("Tasks with these tags"), \
+ "parameter_name": "attached-tags"}) \
+ ),\
+ ("path", self.UI_generator(PathUI)), \
+ ("username", self.UI_generator(TextUI, \
+ {"description": _("Username"),
+ "parameter_name": "username"})
+ ), \
+ ("password" , self.UI_generator(PasswordUI)), \
+ ("period" , self.UI_generator(PeriodUI)), \
+ ("import-from-replies", self.UI_generator(CheckBoxUI, \
+ {"text": _("Import tasks from @ replies " + \
+ "directed to you"), \
+ "parameter": "import-from-replies"}) \
+ ),\
+ ("import-from-direct-messages", self.UI_generator(CheckBoxUI, \
+ {"text": _("Import tasks from direct messages"), \
+ "parameter": "import-from-direct-messages"}) \
+ ),\
+ ("import-from-my-tweets", self.UI_generator(CheckBoxUI, \
+ {"text": _("Import tasks from your tweets"), \
+ "parameter": "import-from-my-tweets"}) \
+ ),\
+ ("import-bug-tags", self.UI_generator(CheckBoxUI, \
+ {"text": _("Tag your tasks with the bug tags"), \
+ "parameter": "import-bug-tags"}) \
+ ),\
+ )
+ def UI_generator(self, param_type, special_arguments = {}):
+ '''A helper function to build a widget type from a template.
+ It passes to the created widget generator a series of common parameters,
+ plus the ones needed to specialize the given template
+
+ @param param_type: the template to specialize
+ @param special_arguments: the arguments used for this particular widget
+ generator.
+
+ @return function: return a widget generator, not a widget. the widget can
+ be obtained by calling widget_generator(backend)
+ '''
+ return lambda backend: param_type(req = self.req, \
+ backend = backend, \
+ width = self.COMMON_WIDTH, \
+ **special_arguments)
+
+ def refresh(self, backend):
+ '''Builds the widgets necessary to configure the backend. If it doesn't
+ know how to render a widget, it simply skips it.
+
+ @param backend: the backend that is being configured
+ '''
+ #remove the old parameters UIs
+ def _remove_child(self, child):
+ self.remove(child)
+ self.foreach(functools.partial(_remove_child, self))
+ #add new widgets
+ backend_parameters = backend.get_parameters()
+ if backend_parameters[GenericBackend.KEY_DEFAULT_BACKEND]:
+ #if it's the default backend, the user should not mess with it
+ return
+ for parameter_name, widget in self.parameter_widgets:
+ if parameter_name in backend_parameters:
+ self.pack_start(widget(backend), True)
+ self.show_all()
+
+ def commit_changes(self):
+ '''
+ Saves all the parameters at their current state (the user may have
+ modified them)
+ '''
+ def _commit_changes(child):
+ child.commit_changes()
+ self.foreach(_commit_changes)
+
=== added file 'GTG/gtk/backends_dialog/parameters_ui/checkboxui.py'
--- GTG/gtk/backends_dialog/parameters_ui/checkboxui.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/checkboxui.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+
+
+class CheckBoxUI(gtk.HBox):
+ '''
+ It's a widget displaying a simple checkbox, with some text to explain its
+ meaning
+ '''
+
+
+ def __init__(self, req, backend, width, text, parameter):
+ '''
+ Creates the checkbox and the related label.
+
+ @param req: a Requester
+ @param backend: a backend object
+ @param width: the width of the gtk.Label object
+ @param parameter: the backend parameter this checkbox should display and
+ modify
+ '''
+ super(CheckBoxUI, self).__init__()
+ self.backend = backend
+ self.req = req
+ self.text = text
+ self.parameter = parameter
+ self._populate_gtk(width)
+
+ def _populate_gtk(self, width):
+ '''Creates the checkbox and the related label
+
+ @param width: the width of the gtk.Label object
+ '''
+ self.checkbutton =gtk.CheckButton(label = self.text)
+ self.checkbutton.set_active(self.backend.get_parameters()[self.parameter])
+ self.checkbutton.connect("toggled", self.on_modified)
+ self.pack_start(self.checkbutton, False)
+
+ def commit_changes(self):
+ '''Saves the changes to the backend parameter'''
+ self.backend.set_parameter(self.parameter,\
+ self.checkbutton.get_active())
+
+ def on_modified(self, sender = None):
+ ''' Signal callback, executed when the user clicks on the checkbox.
+ Disables the backend. The user will re-enable it to confirm the changes
+ (s)he made.
+
+ @param sender: not used, only here for signal compatibility
+ '''
+ if self.backend.is_enabled() and not self.backend.is_default():
+ self.req.set_backend_enabled(self.backend.get_id(), False)
+
=== added file 'GTG/gtk/backends_dialog/parameters_ui/importtagsui.py'
--- GTG/gtk/backends_dialog/parameters_ui/importtagsui.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/importtagsui.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG.backends.genericbackend import GenericBackend
+
+
+
+class ImportTagsUI(gtk.VBox):
+ '''
+ It's a widget displaying a couple of radio buttons, a label and a textbox
+ to let the user change the attached tags (or imported)
+ '''
+
+
+ def __init__(self, req, backend, width, title, anybox_text, somebox_text, \
+ parameter_name):
+ '''Populates the widgets and refresh the tags to display
+
+ @param req: a requester
+ @param backend: the backend to configure
+ @param width: the length of the radio buttons
+ @param title: the text for the label describing what this collection
+ of gtk widgets is used for
+ @param anybox_text: the text for the "Any tag matches" radio button
+ @param somebox_text: the text for the "only this set of tags matches"
+ radio button
+ @param parameter_name: the backend parameter this widget should modify
+ '''
+ super(ImportTagsUI, self).__init__()
+ self.backend = backend
+ self.req = req
+ self.title = title
+ self.anybox_text = anybox_text
+ self.somebox_text = somebox_text
+ self.parameter_name = parameter_name
+ self._populate_gtk(width)
+ self._refresh_tags()
+ self._connect_signals()
+
+ def _populate_gtk(self, width):
+ '''
+ Populates the widgets
+
+ @param width: the length of the radio buttons
+ '''
+ title_label = gtk.Label()
+ title_label.set_alignment(xalign = 0, yalign = 0)
+ title_label.set_markup("<big><b>%s</b></big>" % self.title)
+ self.pack_start(title_label, True)
+ align = gtk.Alignment(xalign = 0, yalign = 0, xscale = 1)
+ align.set_padding(0, 0, 10, 0)
+ self.pack_start(align, True)
+ vbox = gtk.VBox()
+ align.add(vbox)
+ self.all_tags_radio = gtk.RadioButton(group = None, \
+ label = self.anybox_text)
+ vbox.pack_start(self.all_tags_radio, True)
+ self.some_tags_radio = gtk.RadioButton(group = self.all_tags_radio,
+ label = self.somebox_text)
+ self.some_tags_radio.set_size_request(width = width, height = -1)
+ hbox = gtk.HBox()
+ vbox.pack_start(hbox, True)
+ hbox.pack_start(self.some_tags_radio, False)
+ self.tags_entry = gtk.Entry()
+ hbox.pack_start(self.tags_entry, True)
+
+ def on_changed(self, radio, data = None):
+ ''' Signal callback, executed when the user modifies something.
+ Disables the backend. The user will re-enable it to confirm the changes
+ (s)he made.
+
+ @param sender: not used, only here for signal compatibility
+ @param data: not used, only here for signal compatibility
+ '''
+ #every change in the config disables the backend
+ self.req.set_backend_enabled(self.backend.get_id(), False)
+ self._refresh_textbox_state()
+
+ def commit_changes(self):
+ '''Saves the changes to the backend parameter'''
+ if self.all_tags_radio.get_active():
+ tags = [GenericBackend.ALLTASKS_TAG]
+ else:
+ tags = self.tags_entry.get_text().split(",")
+ tags = filter(lambda t: t, tags)
+ self.backend.set_parameter(self.parameter_name, tags)
+
+ def _refresh_textbox_state(self):
+ '''Refreshes the content of the textbox'''
+ self.tags_entry.set_sensitive(self.some_tags_radio.get_active())
+
+ def _refresh_tags(self):
+ '''
+ Refreshes the list of tags to display in the textbox, and selects
+ the correct radio button
+ '''
+ tags_list = self.backend.get_parameters()[self.parameter_name]
+ has_all_tasks = GenericBackend.ALLTASKS_TAG in tags_list
+ self.all_tags_radio.set_active(has_all_tasks)
+ self.some_tags_radio.set_active(not has_all_tasks)
+ self._refresh_textbox_state()
+ if not has_all_tasks:
+ tags_text = ""
+ if tags_list:
+ tags_text = reduce(lambda a, b: a + ", " + b, tags_list)
+ self.tags_entry.set_text(tags_text)
+
+ def _connect_signals(self):
+ '''Connects the gtk signals'''
+ self.some_tags_radio.connect("toggled", self.on_changed)
+ self.all_tags_radio.connect("toggled", self.on_changed)
+ self.tags_entry.connect("changed", self.on_changed)
+
=== added file 'GTG/gtk/backends_dialog/parameters_ui/passwordui.py'
--- GTG/gtk/backends_dialog/parameters_ui/passwordui.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/passwordui.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG import _
+
+
+
+class PasswordUI(gtk.HBox):
+ '''Widget displaying a gtk.Label and a textbox to input a password'''
+
+
+ def __init__(self, req, backend, width):
+ '''Creates the gtk widgets and loads the current password in the text
+ field
+
+ @param req: a Requester
+ @param backend: a backend object
+ @param width: the width of the gtk.Label object
+ '''
+ super(PasswordUI, self).__init__()
+ self.backend = backend
+ self.req = req
+ self._populate_gtk(width)
+ self._load_password()
+ self._connect_signals()
+
+ def _populate_gtk(self, width):
+ '''Creates the text box and the related label
+
+ @param width: the width of the gtk.Label object
+ '''
+ password_label = gtk.Label(_("Password:"))
+ password_label.set_alignment(xalign = 0, yalign = 0.5)
+ password_label.set_size_request(width = width, height = -1)
+ self.pack_start(password_label, False)
+ align = gtk.Alignment(xalign = 0, yalign = 0.5, xscale = 1)
+ align.set_padding(0, 0, 10, 0)
+ self.pack_start(align, True)
+ self.password_textbox = gtk.Entry()
+ align.add(self.password_textbox)
+
+ def _load_password(self):
+ '''Loads the password from the backend'''
+ password = self.backend.get_parameters()['password']
+ self.password_textbox.set_invisible_char('*')
+ self.password_textbox.set_visibility(False)
+ self.password_textbox.set_text(password)
+
+ def _connect_signals(self):
+ '''Connects the gtk signals'''
+ self.password_textbox.connect('changed', self.on_password_modified)
+
+ def commit_changes(self):
+ '''Saves the changes to the backend parameter ('password')'''
+ self.backend.set_parameter('password', self.password_textbox.get_text())
+
+ def on_password_modified(self, sender):
+ ''' Signal callback, executed when the user edits the password.
+ Disables the backend. The user will re-enable it to confirm the changes
+ (s)he made.
+
+ @param sender: not used, only here for signal compatibility
+ '''
+ if self.backend.is_enabled() and not self.backend.is_default():
+ self.req.set_backend_enabled(self.backend.get_id(), False)
+
=== added file 'GTG/gtk/backends_dialog/parameters_ui/pathui.py'
--- GTG/gtk/backends_dialog/parameters_ui/pathui.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/pathui.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+import os.path
+
+from GTG import _
+
+
+
+
+class PathUI(gtk.HBox):
+ '''Gtk widgets to show a path in a textbox, and a button to bring up a
+ filesystem explorer to modify that path (also, a label to describe those)
+ '''
+
+
+ def __init__(self, req, backend, width):
+ '''
+ Creates the textbox, the button and loads the current path.
+
+ @param req: a Requester
+ @param backend: a backend object
+ @param width: the width of the gtk.Label object
+ '''
+ super(PathUI, self).__init__()
+ self.backend = backend
+ self.req = req
+ self._populate_gtk(width)
+
+ def _populate_gtk(self, width):
+ '''Creates the gtk.Label, the textbox and the button
+
+ @param width: the width of the gtk.Label object
+ '''
+ label = gtk.Label(_("Filename:"))
+ label.set_alignment(xalign = 0, yalign = 0.5)
+ label.set_size_request(width = width, height = -1)
+ self.pack_start(label, False)
+ align = gtk.Alignment(xalign = 0, yalign = 0.5, xscale = 1)
+ align.set_padding(0, 0, 10, 0)
+ self.pack_start(align, True)
+ self.textbox = gtk.Entry()
+ self.textbox.set_text(self.backend.get_parameters()['path'])
+ self.textbox.connect('changed', self.on_path_modified)
+ align.add(self.textbox)
+ self.button = gtk.Button(stock = gtk.STOCK_EDIT)
+ self.button.connect('clicked', self.on_button_clicked)
+ self.pack_start(self.button, False)
+
+ def commit_changes(self):
+ '''Saves the changes to the backend parameter'''
+ self.backend.set_parameter('path', self.textbox.get_text())
+
+ def on_path_modified(self, sender):
+ ''' Signal callback, executed when the user edits the path.
+ Disables the backend. The user will re-enable it to confirm the changes
+ (s)he made.
+
+ @param sender: not used, only here for signal compatibility
+ '''
+ if self.backend.is_enabled() and not self.backend.is_default():
+ self.req.set_backend_enabled(self.backend.get_id(), False)
+
+ def on_button_clicked(self, sender):
+ '''Shows the filesystem explorer to choose a new file
+
+ @param sender: not used, only here for signal compatibility
+ '''
+ self.chooser = gtk.FileChooserDialog( \
+ title=None,
+ action=gtk.FILE_CHOOSER_ACTION_SAVE,
+ buttons=(gtk.STOCK_CANCEL,
+ gtk.RESPONSE_CANCEL, \
+ gtk.STOCK_OK, \
+ gtk.RESPONSE_OK))
+ self.chooser.set_default_response(gtk.RESPONSE_OK)
+ #set default file as the current self.path
+ self.chooser.set_current_name(os.path.basename(self.textbox.get_text()))
+ self.chooser.set_current_folder(os.path.dirname(self.textbox.get_text()))
+
+ #filter files
+ afilter = gtk.FileFilter()
+ afilter.set_name("All files")
+ afilter.add_pattern("*")
+ self.chooser.add_filter(afilter)
+ afilter = gtk.FileFilter()
+ afilter.set_name("XML files")
+ afilter.add_mime_type("text/plain")
+ afilter.add_pattern("*.xml")
+ self.chooser.add_filter(afilter)
+ response = self.chooser.run()
+ if response == gtk.RESPONSE_OK:
+ self.textbox.set_text(self.chooser.get_filename())
+ self.chooser.destroy()
=== added file 'GTG/gtk/backends_dialog/parameters_ui/periodui.py'
--- GTG/gtk/backends_dialog/parameters_ui/periodui.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/periodui.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+from GTG import _
+
+
+
+class PeriodUI(gtk.HBox):
+ '''A widget to change the frequency of a backend synchronization
+ '''
+
+
+ def __init__(self, req, backend, width):
+ '''
+ Creates the gtk.Adjustment and the related label. Loads the current
+ period.
+
+ @param req: a Requester
+ @param backend: a backend object
+ @param width: the width of the gtk.Label object
+ '''
+ super(PeriodUI, self).__init__()
+ self.backend = backend
+ self.req = req
+ self._populate_gtk(width)
+ self._connect_signals()
+
+ def _populate_gtk(self, width):
+ '''Creates the gtk widgets
+
+ @param width: the width of the gtk.Label object
+ '''
+ period_label = gtk.Label(_("Period:"))
+ period_label.set_alignment(xalign = 0, yalign = 0.5)
+ period_label.set_size_request(width = width, height = -1)
+ self.pack_start(period_label, False)
+ align = gtk.Alignment(xalign = 0, yalign = 0.5, xscale = 1)
+ align.set_padding(0, 0, 10, 0)
+ self.pack_start(align, False)
+ period = self.backend.get_parameters()['period']
+ self.adjustment = gtk.Adjustment(value = period,
+ lower = 1,
+ upper = 120,
+ step_incr = 1,
+ page_incr = 0,
+ page_size = 0)
+ self.period_spin = gtk.SpinButton(adjustment = self.adjustment,
+ climb_rate = 0.3,
+ digits = 0)
+ align.add(self.period_spin)
+ self.show_all()
+
+ def _connect_signals(self):
+ '''Connects the gtk signals'''
+ self.period_spin.connect('changed', self.on_spin_changed)
+
+ def commit_changes(self):
+ '''Saves the changes to the backend parameter'''
+ self.backend.set_parameter('period', int(self.adjustment.get_value()))
+
+ def on_spin_changed(self, sender):
+ ''' Signal callback, executed when the user changes the period.
+ Disables the backend. The user will re-enable it to confirm the changes
+ (s)he made.
+
+ @param sender: not used, only here for signal compatibility
+ '''
+ if self.backend.is_enabled() and not self.backend.is_default():
+ self.req.set_backend_enabled(self.backend.get_id(), False)
+
=== added file 'GTG/gtk/backends_dialog/parameters_ui/textui.py'
--- GTG/gtk/backends_dialog/parameters_ui/textui.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/textui.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+
+
+
+class TextUI(gtk.HBox):
+ '''A widget to display a simple textbox and a label to describe its content
+ '''
+
+
+ def __init__(self, req, backend, width, description, parameter_name):
+ '''
+ Creates the textbox and the related label. Loads the current
+ content.
+
+ @param req: a Requester
+ @param backend: a backend object
+ @param width: the width of the gtk.Label object
+ '''
+ super(TextUI, self).__init__()
+ self.backend = backend
+ self.req = req
+ self.parameter_name = parameter_name
+ self.description = description
+ self._populate_gtk(width)
+
+ def _populate_gtk(self, width):
+ '''Creates the gtk widgets
+
+ @param width: the width of the gtk.Label object
+ '''
+ label = gtk.Label("%s:" % self.description)
+ label.set_alignment(xalign = 0, yalign = 0.5)
+ label.set_size_request(width = width, height = -1)
+ self.pack_start(label, False)
+ align = gtk.Alignment(xalign = 0, yalign = 0.5, xscale = 1)
+ align.set_padding(0, 0, 10, 0)
+ self.pack_start(align, True)
+ self.textbox = gtk.Entry()
+ self.textbox.set_text(\
+ self.backend.get_parameters()[self.parameter_name])
+ self.textbox.connect('changed', self.on_text_modified)
+ align.add(self.textbox)
+
+ def commit_changes(self):
+ '''Saves the changes to the backend parameter'''
+ self.backend.set_parameter(self.parameter_name,\
+ self.textbox.get_text())
+
+ def on_text_modified(self, sender):
+ ''' Signal callback, executed when the user changes the text.
+ Disables the backend. The user will re-enable it to confirm the changes
+ (s)he made.
+
+ @param sender: not used, only here for signal compatibility
+ '''
+ if self.backend.is_enabled() and not self.backend.is_default():
+ self.req.set_backend_enabled(self.backend.get_id(), False)
+
=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py 2010-08-10 17:30:24 +0000
+++ GTG/gtk/browser/browser.py 2010-08-13 23:44:45 +0000
@@ -34,7 +34,9 @@
#our own imports
import GTG
-from GTG.core import CoreConfig
+from GTG.backends.backendsignals import BackendSignals
+from GTG.gtk.browser.custominfobar import CustomInfoBar
+from GTG.core import CoreConfig
from GTG import _, info, ngettext
from GTG.core.task import Task
from GTG.gtk.browser import GnomeConfig, tasktree, tagtree
@@ -206,6 +208,7 @@
self.sidebar_notebook = self.builder.get_object("sidebar_notebook")
self.main_notebook = self.builder.get_object("main_notebook")
self.accessory_notebook = self.builder.get_object("accessory_notebook")
+ self.vbox_toolbars = self.builder.get_object("vbox_toolbars")
self.closed_pane = None
@@ -313,6 +316,8 @@
self.on_nonworkviewtag_toggled,
"on_preferences_activate":
self.open_preferences,
+ "on_edit_backends_activate":
+ self.open_edit_backends,
}
self.builder.connect_signals(SIGNAL_CONNECTIONS_DIC)
@@ -346,6 +351,16 @@
# Connect requester signals to TreeModels
self.req.connect("task-added", self.on_task_added)
self.req.connect("task-deleted", self.on_task_deleted)
+ #this causes changed be shouwn only on save
+ #tree = self.task_tree_model.get_tree()
+ #tree.connect("task-added-inview", self.on_task_added)
+ #tree.connect("task-deleted-inview", self.on_task_deleted)
+ b_signals = BackendSignals()
+ b_signals.connect(b_signals.BACKEND_FAILED, self.on_backend_failed)
+ b_signals.connect(b_signals.BACKEND_STATE_TOGGLED, \
+ self.remove_backend_infobar)
+ b_signals.connect(b_signals.INTERACTION_REQUESTED, \
+ self.on_backend_needing_interaction)
# Connect signals from models
self.task_modelsort.connect("row-has-child-toggled",\
@@ -425,9 +440,12 @@
### HELPER FUNCTIONS ########################################################
- def open_preferences(self,widget):
+ def open_preferences(self, widget):
self.vmanager.open_preferences(self.priv)
+ def open_edit_backends(self, widget):
+ self.vmanager.open_edit_backends()
+
def quit(self,widget=None):
self.vmanager.close_browser()
@@ -522,7 +540,7 @@
col_id,\
self.priv["tasklist"]["sort_order"])
except:
- print "Invalid configuration for sorting columns"
+ Log.error("Invalid configuration for sorting columns")
if "view" in self.config["browser"]:
view = self.config["browser"]["view"]
@@ -953,7 +971,9 @@
text = \
text.replace("%s%s:%s" % (spaces, attribute, args), "")
# Create the new task
- task = self.req.new_task(tags=[t.get_name() for t in tags], newtask=True)
+ task = self.req.new_task( newtask=True)
+ for tag in tags:
+ task.add_tag(tag.get_name())
if text != "":
task.set_title(text.strip())
task.set_to_keep()
@@ -1516,3 +1536,82 @@
""" Returns true if window is the currently active window """
return self.window.get_property("is-active")
+## BACKENDS RELATED METHODS ##################################################
+
+ def on_backend_failed(self, sender, backend_id, error_code):
+ '''
+ Signal callback.
+ When a backend fails to work, loads a gtk.Infobar to alert the user
+
+ @param sender: not used, only here for signal compatibility
+ @param backend_id: the id of the failing backend
+ @param error_code: a backend error code, as specified in BackendsSignals
+ '''
+ infobar = self._new_infobar(backend_id)
+ infobar.set_error_code(error_code)
+
+ def on_backend_needing_interaction(self, sender, backend_id, description, \
+ interaction_type, callback):
+ '''
+ Signal callback.
+ When a backend needs some kind of feedback from the user,
+ loads a gtk.Infobar to alert the user.
+ This is used, for example, to request confirmation after authenticating
+ via OAuth.
+
+ @param sender: not used, only here for signal compatibility
+ @param backend_id: the id of the failing backend
+ @param description: a string describing the interaction needed
+ @param interaction_type: a string describing the type of interaction
+ (yes/no, only confirm, ok/cancel...)
+ @param callback: the function to call when the user provides the
+ feedback
+ '''
+ infobar = self._new_infobar(backend_id)
+ infobar.set_interaction_request(description, interaction_type, callback)
+
+
+ def __remove_backend_infobar(self, child, backend_id):
+ '''
+ Helper function to remove an gtk.Infobar related to a backend
+
+ @param child: a gtk.Infobar
+ @param backend_id: the id of the backend which gtk.Infobar should be
+ removed.
+ '''
+ if isinstance(child, CustomInfoBar) and\
+ child.get_backend_id() == backend_id:
+ if self.vbox_toolbars:
+ self.vbox_toolbars.remove(child)
+
+ def remove_backend_infobar(self, sender, backend_id):
+ '''
+ Signal callback.
+ Deletes the gtk.Infobars related to a backend
+
+ @param sender: not used, only here for signal compatibility
+ @param backend_id: the id of the backend which gtk.Infobar should be
+ removed.
+ '''
+ backend = self.req.get_backend(backend_id)
+ if not backend or (backend and backend.is_enabled()):
+ #remove old infobar related to backend_id, if any
+ if self.vbox_toolbars:
+ self.vbox_toolbars.foreach(self.__remove_backend_infobar, \
+ backend_id)
+
+ def _new_infobar(self, backend_id):
+ '''
+ Helper function to create a new infobar for a backend
+
+ @param backend_id: the backend for which we're creating the infobar
+ @returns gtk.Infobar: the created infobar
+ '''
+ #remove old infobar related to backend_id, if any
+ if not self.vbox_toolbars:
+ return
+ self.vbox_toolbars.foreach(self.__remove_backend_infobar, backend_id)
+ #add a new one
+ infobar = CustomInfoBar(self.req, self, self.vmanager, backend_id)
+ self.vbox_toolbars.pack_start(infobar, True)
+ return infobar
=== added file 'GTG/gtk/browser/custominfobar.py'
--- GTG/gtk/browser/custominfobar.py 1970-01-01 00:00:00 +0000
+++ GTG/gtk/browser/custominfobar.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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/>.
+# -----------------------------------------------------------------------------
+
+import gtk
+import threading
+
+from GTG import _
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.networkmanager import is_connection_up
+
+
+
+class CustomInfoBar(gtk.InfoBar):
+ '''
+ A gtk.InfoBar specialized for displaying errors and requests for
+ interaction coming from the backends
+ '''
+
+
+ AUTHENTICATION_MESSAGE = _("The <b>%s</b> backend cannot login with the "
+ "supplied authentication data and has been"
+ " disabled. To retry the login, re-enable the backend.")
+
+ NETWORK_MESSAGE = _("Due to a network problem, I cannot contact "
+ "the <b>%s</b> backend.")
+
+ DBUS_MESSAGE = _("Cannot connect to DBUS, I've disabled "
+ "the <b>%s</b> backend.")
+
+ def __init__(self, req, browser, vmanager, backend_id):
+ '''
+ Constructor, Prepares the infobar.
+
+ @param req: a Requester object
+ @param browser: a TaskBrowser object
+ @param vmanager: a ViewManager object
+ @param backend_id: the id of the backend linked to the infobar
+ '''
+ super(CustomInfoBar, self).__init__()
+ self.req = req
+ self.browser = browser
+ self.vmanager = vmanager
+ self.backend_id = backend_id
+ self.backend = self.req.get_backend(backend_id)
+
+ def get_backend_id(self):
+ '''
+ Getter function to return the id of the backend for which this
+ gtk.InfoBar was created
+ '''
+ return self.backend_id
+
+ def _populate(self):
+ '''Setting up gtk widgets'''
+ content_hbox = self.get_content_area()
+ content_hbox.set_homogeneous(False)
+ self.label = gtk.Label()
+ self.label.set_line_wrap(True)
+ self.label.set_alignment(0.5, 0.5)
+ self.label.set_justify(gtk.JUSTIFY_FILL)
+ content_hbox.pack_start(self.label, True, True)
+
+ def _on_error_response(self, widget, event):
+ '''
+ Signal callback executed when the user acknowledges the error displayed
+ in the infobar
+
+ @param widget: not used, here for compatibility with signals callbacks
+ @param event: the code of the gtk response
+ '''
+ self.hide()
+ if event == gtk.RESPONSE_ACCEPT:
+ self.vmanager.configure_backend(backend_id = self.backend_id)
+
+ def set_error_code(self, error_code):
+ '''
+ Sets this infobar to show an error to the user
+
+ @param error_code: the code of the error to show. Error codes are listed
+ in BackendSignals
+ '''
+ self._populate()
+ self.connect("response", self._on_error_response)
+ backend_name = self.backend.get_human_name()
+
+ if error_code == BackendSignals.ERRNO_AUTHENTICATION:
+ self.set_message_type(gtk.MESSAGE_ERROR)
+ self.label.set_markup(self.AUTHENTICATION_MESSAGE % backend_name)
+ self.add_button(_('Configure backend'), gtk.RESPONSE_ACCEPT)
+ self.add_button(_('Ignore'), gtk.RESPONSE_CLOSE)
+
+ elif error_code == BackendSignals.ERRNO_NETWORK:
+ if not is_connection_up():
+ return
+ self.set_message_type(gtk.MESSAGE_WARNING)
+ self.label.set_markup(self.NETWORK_MESSAGE % backend_name)
+ #FIXME: use gtk stock button instead
+ self.add_button(_('Ok'), gtk.RESPONSE_CLOSE)
+
+ elif error_code == BackendSignals.ERRNO_DBUS:
+ self.set_message_type(gtk.MESSAGE_WARNING)
+ self.label.set_markup(self.DBUS_MESSAGE % backend_name)
+ self.add_button(_('Ok'), gtk.RESPONSE_CLOSE)
+
+ self.show_all()
+
+ def set_interaction_request(self, description, interaction_type, callback):
+ '''
+ Sets this infobar to request an interaction from the user
+
+ @param description: a string describing the interaction needed
+ @param interaction_type: a string describing the type of interaction
+ (yes/no, only confirm, ok/cancel...)
+ @param callback: the function to call when the user provides the
+ feedback
+ '''
+ self._populate()
+ self.callback = callback
+ self.set_message_type(gtk.MESSAGE_INFO)
+ self.label.set_markup(description)
+ self.connect("response", self._on_interaction_response)
+ self.interaction_type = interaction_type
+ if interaction_type == BackendSignals().INTERACTION_CONFIRM:
+ self.add_button(_('Confirm'), gtk.RESPONSE_ACCEPT)
+ elif interaction_type == BackendSignals().INTERACTION_TEXT:
+ self.add_button(_('Continue'), gtk.RESPONSE_ACCEPT)
+ self.show_all()
+
+ def _on_interaction_response(self, widget, event):
+ '''
+ Signal callback executed when the user gives the feedback for a
+ requested interaction
+
+ @param widget: not used, here for compatibility with signals callbacks
+ @param event: the code of the gtk response
+ '''
+ if event == gtk.RESPONSE_ACCEPT:
+ if self.interaction_type == BackendSignals().INTERACTION_TEXT:
+ self._prepare_textual_interaction()
+ print "done"
+ elif self.interaction_type == BackendSignals().INTERACTION_CONFIRM:
+ self.hide()
+ threading.Thread(target = getattr(self.backend,
+ self.callback)).start()
+
+ def _prepare_textual_interaction(self):
+ '''
+ Helper function. gtk calls to populate the infobar in the case of
+ interaction request
+ '''
+ title, description\
+ = getattr(self.backend, self.callback)("get_title")
+ self.dialog = gtk.Window()#type = gtk.WINDOW_POPUP)
+ self.dialog.set_title(title)
+ self.dialog.set_transient_for(self.browser.window)
+ self.dialog.set_destroy_with_parent(True)
+ self.dialog.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
+ self.dialog.set_modal(True)
+ # self.dialog.set_size_request(300,170)
+ vbox = gtk.VBox()
+ self.dialog.add(vbox)
+ description_label = gtk.Label()
+ description_label.set_justify(gtk.JUSTIFY_FILL)
+ description_label.set_line_wrap(True)
+ description_label.set_markup(description)
+ align = gtk.Alignment(0.5, 0.5, 1, 1)
+ align.set_padding(10, 0, 20, 20)
+ align.add(description_label)
+ vbox.pack_start(align)
+ self.text_box = gtk.Entry()
+ self.text_box.set_size_request(-1, 40)
+ align = gtk.Alignment(0.5, 0.5, 1, 1)
+ align.set_padding(20, 20, 20, 20)
+ align.add(self.text_box)
+ vbox.pack_start(align)
+ button = gtk.Button(stock = gtk.STOCK_OK)
+ button.connect("clicked", self._on_text_confirmed)
+ button.set_size_request(-1, 40)
+ vbox.pack_start(button, False)
+ self.dialog.show_all()
+ self.hide()
+
+ def _on_text_confirmed(self, widget):
+ '''
+ Signal callback, used when the interaction needs a textual input to be
+ completed (e.g, the twitter OAuth, requesting a pin)
+
+ @param widget: not used, here for signal callback compatibility
+ '''
+ text = self.text_box.get_text()
+ self.dialog.destroy()
+ threading.Thread(target = getattr(self.backend, self.callback),
+ args = ("set_text", text)).start()
+
=== modified file 'GTG/gtk/browser/taskbrowser.glade'
--- GTG/gtk/browser/taskbrowser.glade 2010-05-22 22:41:44 +0000
+++ GTG/gtk/browser/taskbrowser.glade 2010-08-13 23:44:45 +0000
@@ -154,6 +154,17 @@
<signal name="activate" handler="on_preferences_activate"/>
</object>
</child>
+ <child>
+ <object class="GtkImageMenuItem" id="backends_mi">
+ <property name="label">_Backends</property>
+ <property name="visible">True</property>
+ <property name="use_underline">True</property>
+ <property name="image">image4</property>
+ <property name="use_stock">False</property>
+ <property name="accel_group">accelgroup1</property>
+ <signal name="activate" handler="on_edit_backends_activate"/>
+ </object>
+ </child>
</object>
</child>
</object>
=== modified file 'GTG/gtk/colors.py'
--- GTG/gtk/colors.py 2010-06-07 21:14:45 +0000
+++ GTG/gtk/colors.py 2010-08-13 23:44:45 +0000
@@ -20,7 +20,7 @@
#Take list of Tags and give the background color that should be applied
#The returned color might be None (in which case, the default is used)
-def background_color(tags, bgcolor=None):
+def background_color(tags, bgcolor = None):
if not bgcolor:
bgcolor = gtk.gdk.color_parse("#FFFFFF")
# Compute color
@@ -52,3 +52,29 @@
my_color = gtk.gdk.Color(red, green, blue).to_string()
return my_color
+def get_colored_tag_markup(req, tag_name):
+ '''
+ Given a tag name, returns a string containing the markup to color the
+ tag name
+ '''
+ tag = req.get_tag(tag_name)
+ if tag is None:
+ #no task loaded with that tag, color cannot be taken
+ return tag_name
+ else:
+ tag_color = tag.get_attribute("color")
+ if tag_color:
+ return '<span color="%s">%s</span>' % (tag_color, tag_name)
+ else:
+ return tag_name
+
+def get_colored_tags_markup(req, tag_names):
+ '''
+ Calls get_colored_tag_markup for each tag_name in tag_names
+ '''
+ tag_markups = map(lambda t: get_colored_tag_markup(req, t), tag_names)
+ tags_txt = ""
+ if tag_markups:
+ #reduce crashes if applied to an empty list
+ tags_txt = reduce(lambda a, b: a + ", " + b, tag_markups)
+ return tags_txt
=== modified file 'GTG/gtk/manager.py'
--- GTG/gtk/manager.py 2010-08-03 17:07:31 +0000
+++ GTG/gtk/manager.py 2010-08-13 23:44:45 +0000
@@ -39,10 +39,11 @@
from GTG.core.plugins.engine import PluginEngine
from GTG.core.plugins.api import PluginAPI
from GTG.tools.logger import Log
-
-
-
-class Manager:
+from GTG.gtk.backends_dialog import BackendsDialog
+
+
+
+class Manager(object):
############## init #####################################################
@@ -80,6 +81,7 @@
#Preferences and Backends windows
# Initialize dialogs
self.preferences_dialog = None
+ self.edit_backends_dialog = None
#DBus
DBusTaskWrapper(self.req, self)
@@ -196,6 +198,16 @@
################ Others dialog ############################################
+ def open_edit_backends(self, sender = None, backend_id = None):
+ if not self.edit_backends_dialog:
+ self.edit_backends_dialog = BackendsDialog(self.req)
+ self.edit_backends_dialog.activate()
+ if backend_id != None:
+ self.edit_backends_dialog.show_config_for_backend(backend_id)
+
+ def configure_backend(self, backend_id):
+ self.open_edit_backends(None, backend_id)
+
def open_preferences(self, config_priv, sender=None):
if not hasattr(self, "preferences"):
self.preferences = PreferencesDialog(self.pengine, self.p_apis, \
@@ -211,7 +223,8 @@
self.close_task(t)
### MAIN ###################################################################
- def main(self, once_thru=False):
+
+ def main(self, once_thru = False):
gobject.threads_init()
if once_thru:
gtk.main_iteration()
@@ -219,7 +232,6 @@
gtk.main()
return 0
-
def quit(self,sender=None):
gtk.main_quit()
#save opened tasks and their positions.
=== added file 'GTG/tests/test_interruptible.py'
--- GTG/tests/test_interruptible.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_interruptible.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# 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 interrupting cooperative threads
+'''
+
+import unittest
+import time
+from threading import Thread, Event
+
+from GTG.tools.interruptible import interruptible, _cancellation_point
+
+
+class TestInterruptible(unittest.TestCase):
+ '''
+ Tests for interrupting cooperative threads
+ '''
+
+ def test_interruptible_decorator(self):
+ self.quit_condition = False
+ cancellation_point = lambda: _cancellation_point(\
+ lambda: self.quit_condition)
+ self.thread_started = Event()
+ @interruptible
+ def never_ending(cancellation_point):
+ self.thread_started.set()
+ while True:
+ time.sleep(0.1)
+ cancellation_point()
+ thread = Thread(target = never_ending, args = (cancellation_point, ))
+ thread.start()
+ self.thread_started.wait()
+ self.quit_condition = True
+ countdown = 10
+ while thread.is_alive() and countdown > 0:
+ time.sleep(0.1)
+ countdown -= 1
+ self.assertFalse(thread.is_alive())
+
+
+
+
+
+
+
+
+
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestInterruptible)
+
=== added file 'GTG/tools/networkmanager.py'
--- GTG/tools/networkmanager.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/networkmanager.py 2010-08-13 23:44:45 +0000
@@ -0,0 +1,57 @@
+#!/bin/env python
+#
+# 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 2 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Copyright (C) 2010 Red Hat, Inc.
+#
+
+import dbus
+
+
+def is_connection_up():
+ '''
+ Returns True if network-manager reports that at least one connection is up
+
+ @returns bool
+ '''
+ state = False
+ bus = dbus.SystemBus()
+
+ proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
+ manager = dbus.Interface(proxy, "org.freedesktop.NetworkManager")
+
+ manager_prop_iface = dbus.Interface(proxy, "org.freedesktop.DBus.Properties")
+ active = manager_prop_iface.Get("org.freedesktop.NetworkManager", "ActiveConnections")
+ for a in active:
+ ac_proxy = bus.get_object("org.freedesktop.NetworkManager", a)
+ prop_iface = dbus.Interface(ac_proxy, "org.freedesktop.DBus.Properties")
+ state = prop_iface.Get("org.freedesktop.NetworkManager.ActiveConnection", "State")
+
+ # Connections in NM are a collection of settings that describe everything
+ # needed to connect to a specific network. Lets get those details so we
+ # can find the user-readable name of the connection.
+ con_path = prop_iface.Get("org.freedesktop.NetworkManager.ActiveConnection", "Connection")
+ con_service = prop_iface.Get("org.freedesktop.NetworkManager.ActiveConnection", "ServiceName")
+
+ # ask the provider of the connection for its details
+ service_proxy = bus.get_object(con_service, con_path)
+ con_iface = dbus.Interface(service_proxy, "org.freedesktop.NetworkManagerSettings.Connection")
+ con_details = con_iface.GetSettings()
+ con_name = con_details['connection']['id']
+
+ if state == 2: # activated
+ state = True
+ return state
+
=== added file 'data/icons/hicolor/scalable/apps/backend_localfile.png'
Binary files data/icons/hicolor/scalable/apps/backend_localfile.png 1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_localfile.png 2010-08-13 23:44:45 +0000 differ
Follow ups