[conduit] Port from HAL to GUDEV
- From: John Stowers <jstowers src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [conduit] Port from HAL to GUDEV
- Date: Wed, 22 Sep 2010 06:21:08 +0000 (UTC)
commit da3e1dba45fd722aed0bad2f9c9921f404d883c0
Author: John Stowers <john stowers gmail com>
Date: Tue Feb 9 00:49:21 2010 +0100
Port from HAL to GUDEV
conduit/dataproviders/DataProvider.py | 12 ++
conduit/dataproviders/HalFactory.py | 104 ++++++++++--------
conduit/dataproviders/MediaPlayerFactory.py | 66 +++++++++++
conduit/dataproviders/SimpleFactory.py | 2 +
conduit/dataproviders/VolumeFactory.py | 58 ----------
conduit/modules/N800Module/N800Module.py | 2 +-
conduit/modules/iPodModule/iPodModule.py | 161 ++++++++++++++++++++++-----
conduit/utils/Singleton.py | 8 +-
conduit/utils/UDev.py | 13 ++
conduit/utils/__init__.py | 13 ++
configure.ac | 1 +
setup-env.sh | 1 +
12 files changed, 302 insertions(+), 139 deletions(-)
---
diff --git a/conduit/dataproviders/DataProvider.py b/conduit/dataproviders/DataProvider.py
index 839b6e4..5fbb95f 100644
--- a/conduit/dataproviders/DataProvider.py
+++ b/conduit/dataproviders/DataProvider.py
@@ -568,6 +568,11 @@ class DataProviderFactory(gobject.GObject):
gobject.GObject.__init__(self)
def emit_added(self, klass, initargs, category, customKey=None):
+ """
+ Emits the dataprovider-added signal for the given class with the
+ given conctruction arguments
+ """
+ print "emit_added",klass,initargs,category,customKey
dpw = ModuleWrapper.ModuleWrapper(
klass=klass,
initargs=initargs,
@@ -580,10 +585,17 @@ class DataProviderFactory(gobject.GObject):
return key
def emit_removed(self, key):
+ """
+ Emits the dataprovider-removed signal
+ """
log.debug("DataProviderFactory %s: Emitting dataprovider-removed for %s" % (self, key))
self.emit("dataprovider-removed", key)
def probe(self):
+ """
+ Search for appropriate connected devices, calling emit_added or
+ emit_removed for each device
+ """
pass
def quit(self):
diff --git a/conduit/dataproviders/HalFactory.py b/conduit/dataproviders/HalFactory.py
index 16fe4fe..2801ea4 100644
--- a/conduit/dataproviders/HalFactory.py
+++ b/conduit/dataproviders/HalFactory.py
@@ -1,62 +1,74 @@
import gobject
-import dbus
+import gudev
+import gio
import logging
log = logging.getLogger("dataproviders.HalFactory")
import conduit.utils as Utils
+import conduit.utils.UDev as UDev
import conduit.dataproviders.SimpleFactory as SimpleFactory
+log.info("Module Information: %s" % Utils.get_module_information(gudev, "__version__"))
+
class HalFactory(SimpleFactory.SimpleFactory):
+ SUBSYSTEMS = ("usb", "block")
+
def __init__(self, **kwargs):
SimpleFactory.SimpleFactory.__init__(self, **kwargs)
- # Connect to system HAL
- self.bus = dbus.SystemBus()
- self.hal_obj = self.bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager")
- self.hal = dbus.Interface(self.hal_obj, "org.freedesktop.Hal.Manager")
-
- # Hookup signals
- self.hal.connect_to_signal("DeviceAdded", self._device_added)
- self.hal.connect_to_signal("DeviceRemoved", self._device_removed)
- self.hal.connect_to_signal("NewCapability", self._new_capability)
-
- def _maybe_new(self, device_udi):
- props = self._get_properties(device_udi)
- if self.is_interesting(device_udi, props):
- self.item_added(device_udi, **props)
-
- def _device_added(self, device_udi, *args):
- self._maybe_new(device_udi)
-
- def _new_capability(self, device_udi, *args):
- if not device_udi in self.items.keys():
- self._maybe_new(device_udi)
-
- def _device_removed(self, device_udi):
- self.item_removed(device_udi)
-
- def _get_properties(self, device):
- buf = {}
- try:
- device_dbus_obj = self.bus.get_object("org.freedesktop.Hal" ,device)
- for x, y in device_dbus_obj.GetAllProperties(dbus_interface="org.freedesktop.Hal.Device").items():
- #DBus *still* does not marshal dbus.String to str correctly,
- #so we force it to
- buf[str(x)] = y
- except:
- log.warn("Could not get HAL properties for %s" % device)
- return buf
+ assert hasattr(self.SUBSYSTEMS, "__iter__")
+
+ self.gudev = UDev.UDevSingleton(self.SUBSYSTEMS)
+ self.gudev.connect("uevent", self._on_uevent)
+ self.vm = gio.volume_monitor_get()
+
+ def _print_device(self, device):
+ print "subsystem", device.get_subsystem()
+ print "devtype", device.get_devtype()
+ print "name", device.get_name()
+ print "number", device.get_number()
+ print "sysfs_path:", device.get_sysfs_path()
+ print "driver:", device.get_driver()
+ print "action:", device.get_action()
+ print "seqnum:", device.get_seqnum()
+ print "device type:", device.get_device_type()
+ print "device number:", device.get_device_number()
+ print "device file:", device.get_device_file()
+ print "device file symlinks:", ", ".join(device.get_device_file_symlinks())
+ print "device keys:", ", ".join(device.get_property_keys())
+ for device_key in device.get_property_keys():
+ print " device property %s: %s" % (device_key, device.get_property(device_key))
+
+ def _on_uevent(self, client, action, device):
+ self._print_device(device)
+ if action == "add":
+ self._maybe_new(device)
+ elif action == "remove":
+ sysfs_path = self.get_sysfs_path_for_device(device)
+ self.item_removed(sysfs_path)
+
+ def _get_device_properties(self, device):
+ props = {}
+ for key in device.get_property_keys():
+ props[key.upper()] = device.get_property(key)
+ return props
+
+ def _maybe_new(self, device):
+ props = self._get_device_properties(device)
+ sysfs_path = self.get_sysfs_path_for_device(device)
+ if self.is_interesting(sysfs_path, props):
+ self.item_added(sysfs_path, **props)
+
+ def get_udev_device_for_sysfs_path(self, sysfs_path):
+ return self.gudev.query_by_sysfs_path(sysfs_path)
+
+ def get_sysfs_path_for_device(self, device):
+ return device.get_sysfs_path()
def probe(self):
- """
- Enumerate HAL for any entries of interest
- """
- devices = self.hal.GetAllDevices()
- for device in self.hal.GetAllDevices():
- self._maybe_new(str(device))
-
- def get_args(self, udi, **kwargs):
- return (udi,)
+ for s in self.SUBSYSTEMS:
+ for d in self.gudev.query_by_subsystem(s):
+ self._maybe_new(d)
diff --git a/conduit/dataproviders/MediaPlayerFactory.py b/conduit/dataproviders/MediaPlayerFactory.py
new file mode 100644
index 0000000..3273caa
--- /dev/null
+++ b/conduit/dataproviders/MediaPlayerFactory.py
@@ -0,0 +1,66 @@
+import os.path
+import ConfigParser
+import logging
+log = logging.getLogger("dataproviders.MediaPlayerFactory")
+
+import conduit.utils as Utils
+import conduit.dataproviders.HalFactory as HalFactory
+
+class MediaPlayerFactory(HalFactory.HalFactory):
+
+ #keys to interrogate according to the media-player-info spec
+ # section, key name, store as property
+ MPI_ACCESS_PROTOCOL = ("Device", "AccessProtocol", "MPI_ACCESS_PROTOCOL")
+ MPI_ICON = ("Device", "Icon", "MPI_ICON")
+ MPI_KEYS = (MPI_ACCESS_PROTOCOL, MPI_ICON)
+
+ def __init__(self, *args, **kwargs):
+ HalFactory.HalFactory.__init__(self, *args, **kwargs)
+
+ #taken from quodlibet
+ def __get_mpi_dir(self):
+ for d in Utils.get_system_data_dirs():
+ path = os.path.join(d, "media-player-info")
+ if os.path.isdir(path):
+ return path
+
+ #taken from quodlibet
+ def __get_mpi_file(self, mplayer_id):
+ """
+ Returns a SafeConfigParser instance of the mpi file or None.
+ MPI files are INI like files usually located in
+ /usr/local/media-player-info/*.mpi
+ """
+ f = os.path.join( self.__get_mpi_dir() , mplayer_id + ".mpi")
+ if os.path.isfile(f):
+ parser = ConfigParser.SafeConfigParser()
+ read = parser.read(f)
+ if read: return parser
+
+ def _maybe_new(self, device):
+ props = self._get_device_properties(device)
+ sysfs_path = self.get_sysfs_path_for_device(device)
+ try:
+ mplayer_id = props["ID_MEDIA_PLAYER"]
+
+ #taken from quodlibet
+ config = self.__get_mpi_file(mplayer_id)
+ if config is not None:
+ for (section_name, key_name, prop_name) in self.MPI_KEYS:
+ try:
+ props[prop_name] = config.get(section_name, key_name)
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ pass
+
+ if self.is_interesting(sysfs_path, props):
+ self.item_added(sysfs_path, **props)
+
+ except KeyError:
+ log.debug("Device not media player")
+
+ def get_mpi_access_protocol(self, props):
+ return props.get(self.MPI_ACCESS_PROTOCOL[2], "")
+
+ def get_mpi_icon(self, props, fallback="media-player"):
+ return props.get(self.MPI_ICON[2], fallback)
+
diff --git a/conduit/dataproviders/SimpleFactory.py b/conduit/dataproviders/SimpleFactory.py
index c5573a9..d504dd3 100644
--- a/conduit/dataproviders/SimpleFactory.py
+++ b/conduit/dataproviders/SimpleFactory.py
@@ -20,7 +20,9 @@ class SimpleFactory(DataProvider.DataProviderFactory):
idxs = []
for klass in self.get_dataproviders(key, **kwargs):
args = self.get_args(key, **kwargs)
+ print "emit_added"
idx = self.emit_added(klass, args, cat)
+ print "emmitted"
idxs.append(idx)
self.items[key] = idxs
diff --git a/conduit/modules/N800Module/N800Module.py b/conduit/modules/N800Module/N800Module.py
index 036dc6b..0b3cd7e 100644
--- a/conduit/modules/N800Module/N800Module.py
+++ b/conduit/modules/N800Module/N800Module.py
@@ -23,7 +23,7 @@ import conduit.vfs as Vfs
from gettext import gettext as _
MODULES = {
- "N800Factory" : { "type": "dataprovider-factory" },
+# "N800Factory" : { "type": "dataprovider-factory" },
}
diff --git a/conduit/modules/iPodModule/iPodModule.py b/conduit/modules/iPodModule/iPodModule.py
index 48dcc3b..f41e83d 100644
--- a/conduit/modules/iPodModule/iPodModule.py
+++ b/conduit/modules/iPodModule/iPodModule.py
@@ -10,6 +10,7 @@ order to listen to HAL events
Copyright: John Stowers, 2006
License: GPLv2
"""
+import sys
import os
import pickle
import logging
@@ -24,7 +25,8 @@ log = logging.getLogger("modules.iPod")
import conduit
import conduit.dataproviders.DataProvider as DataProvider
import conduit.dataproviders.DataProviderCategory as DataProviderCategory
-import conduit.dataproviders.VolumeFactory as VolumeFactory
+import conduit.dataproviders.MediaPlayerFactory as MediaPlayerFactory
+import conduit.dataproviders.HalFactory as HalFactory
import conduit.utils as Utils
import conduit.datatypes.Note as Note
import conduit.datatypes.Contact as Contact
@@ -38,9 +40,10 @@ from gettext import gettext as _
errormsg = ""
try:
import gpod
- if gpod.version_info >= (0,6,0):
+ if gpod.version_info >= (0,7,0):
MODULES = {
"iPodFactory" : { "type": "dataprovider-factory" },
+ "iPhoneFactory" : { "type": "dataprovider-factory" },
}
log.info("Module Information: %s" % Utils.get_module_information(gpod, 'version_info'))
except ImportError:
@@ -48,6 +51,10 @@ except ImportError:
except locale.Error:
errormsg = "iPod support disabled (Incorrect locale)"
+PROPS_KEY_MOUNT = "CONDUIT_MOUNTPOINT"
+PROPS_KEY_NAME = "CONDUIT_NAME"
+PROPS_KEY_ICON = "CONDUIT_ICON"
+
if errormsg:
MODULES = {}
log.warn(errormsg)
@@ -66,33 +73,104 @@ def _string_to_unqiue_file(txt, base_uri, prefix, postfix=''):
temp.set_UID(os.path.basename(uri))
return temp.get_rid()
-class iPodFactory(VolumeFactory.VolumeFactory):
+def _supports_photos(db):
+ if isinstance(p, gpod.PhotoDatabase) or isinstance(m, gpod.Database):
+ return gpod.itdb_device_supports_photo(db._itdb.device)
+ else:
+ log.critical("could not determine if device supports photos")
+ return False
- def _get_mount_path(self, props):
- return str(props["volume.mount_point"])
+def _get_apple_label(props):
+ return props.get(PROPS_KEY_NAME,
+ "Apple " + props.get("ID_MODEL", "Device"))
+
+def _get_apple_icon(props):
+ return props.get(PROPS_KEY_ICON, "multimedia-player-apple-ipod")
+
+class iPhoneFactory(HalFactory.HalFactory):
+
+ def is_interesting(self, sysfs_path, props):
+ #there is no media-player-info support for the apple iphone, so instead
+ #we have to look for the correct model name instead.
+ if "Apple" in props.get("ID_VENDOR", "") and "iPhone" in props.get("ID_MODEL", ""):
+ #also have to check the iPhone has a valid serial, as that is used
+ #with gvfs to generate the uuid of the moint
+ self._print_device(self.get_udev_device_for_sysfs_path(sysfs_path))
+ if props.get("ID_SERIAL_SHORT"):
+ uuid = "afc://%s/" % props["ID_SERIAL_SHORT"]
+ for m in self.vm.get_mounts():
+ root = m.get_root()
+ uri = root.get_uri()
+ if uuid == uri:
+ #check that gvfs has mounted the volume at the expected location
+ #FIXME: this is not very nice, as it depends on an implementation
+ #detail of gvfs-afc backend. It would be best if there was some UUID
+ #that was present in udev and guarenteed to be present in all gio mounts
+ #but experimentation tells me there is no such uuid, it returns None
+ props[PROPS_KEY_MOUNT] = root.get_path()
+ props[PROPS_KEY_NAME] = m.get_name()
+ props[PROPS_KEY_ICON] = "phone"
+ return True
+ log.warning("iPhone not mounted by gvfs")
+ else:
+ log.critical("iPhone missing ID_SERIAL_SHORT udev property")
+ return False
- def is_interesting(self, udi, props):
- if props.get("info.parent"):
- parent = self._get_properties(props["info.parent"])
- if parent.get("storage.model") == "iPod":
- props.update(parent)
- return True
+ def get_category(self, key, **props):
+ """ Return a category to contain these dataproviders """
+ print "get_category", props.get(PROPS_KEY_MOUNT)
+ return DataProviderCategory.DataProviderCategory(
+ _get_apple_label(props),
+ _get_apple_icon(props),
+ key)
+
+ def get_dataproviders(self, key, **props):
+ """ Return a list of dataproviders for this class of device """
+ print "get_dataproviders", props.get(PROPS_KEY_MOUNT)
+ return [IPodDummy, IPodPhotoSink]
+
+ def get_args(self, key, **props):
+ print "get_args", props.get(PROPS_KEY_MOUNT)
+ return (props[PROPS_KEY_MOUNT], key)
+
+class iPodFactory(MediaPlayerFactory.MediaPlayerFactory):
+
+ def is_interesting(self, sysfs_path, props):
+ #just like rhythmbox, we let media-player-info do the matching, and
+ #instead just check if it has told us that the media player uses the
+ #ipod storage protocol
+ access_protocols = self.get_mpi_access_protocol(props)
+ if "ipod" in access_protocols.split(";"):
+ uuid = props.get("ID_FS_UUID")
+ for vol in self.vm.get_volumes():
+ #is this the disk corresponding to the ipod
+ #FIXME: we should be able to do gio.VolumeMonitor.get_volume_for_uuid()
+ #but that doesnt work
+ if vol.get_identifier('uuid') == uuid:
+ #now check it is mounted
+ mount = vol.get_mount()
+ if mount:
+ f = mount.get_root()
+ props[PROPS_KEY_MOUNT] = f.get_path()
+ props[PROPS_KEY_NAME] = "%s's %s" % (mount.get_name(), props.get("ID_MODEL", "iPod"))
+ props[PROPS_KEY_ICON] = self.get_mpi_icon(props, fallback="multimedia-player-apple-ipod")
+ return True
+ else:
+ log.warn("ipod not mounted")
+ log.warn("could not find volume with udev ID_FS_UUID: %s" % uuid)
return False
- def get_category(self, udi, **kwargs):
- label = kwargs['volume.label']
- if not label:
- label = "Apple iPod Music Player"
+ def get_category(self, key, **props):
return DataProviderCategory.DataProviderCategory(
- label,
- "multimedia-player-ipod-standard-color",
- self._get_mount_path(kwargs))
+ _get_apple_label(props),
+ _get_apple_icon(props),
+ key)
- def get_dataproviders(self, udi, **kwargs):
+ def get_dataproviders(self, udi, **props):
#Read information about the ipod, like if it supports
#photos or not
d = gpod.itdb_device_new()
- gpod.itdb_device_set_mountpoint(d,self._get_mount_path(kwargs))
+ gpod.itdb_device_set_mountpoint(d, props[PROPS_KEY_MOUNT])
supportsPhotos = gpod.itdb_device_supports_photo(d)
gpod.itdb_device_free(d)
if supportsPhotos:
@@ -101,12 +179,24 @@ class iPodFactory(VolumeFactory.VolumeFactory):
log.info("iPod does not report photo support")
return [IPodMusicTwoWay, IPodVideoTwoWay, IPodNoteTwoWay, IPodContactsTwoWay, IPodCalendarTwoWay]
- def get_args(self, udi, **kwargs):
- """
- iPod needs a local path to the DB, not a URI
- """
- kwargs["mount_path"] = self._get_mount_path(kwargs)
- return (kwargs['mount_path'], udi)
+ def get_args(self, key, **props):
+ return (props[PROPS_KEY_MOUNT], key)
+
+class IPodDummy(DataProvider.TwoWay):
+
+ _name_ = "Dummy"
+ _description_ = "Dummy iPod"
+ _module_type_ = "twoway"
+ _in_type_ = "file"
+ _out_type_ = "file"
+
+ def __init__(self, *args):
+ DataProvider.TwoWay.__init__(self)
+ print "CONSTRUCTED ", args
+ self.args = args or "q"
+
+ def get_UID(self):
+ print "-----".join(self.args)
class IPodBase(DataProvider.TwoWay):
def __init__(self, *args):
@@ -383,6 +473,7 @@ class IPodPhotoSink(IPodBase):
)
def _set_sysinfo(self, modelnumstr, model):
+ #this must only be used from TestDataProvideriPod.py
gpod.itdb_device_set_sysinfo(self.db._itdb.device, modelnumstr, model)
def _get_photo_album(self, albumName):
@@ -419,9 +510,13 @@ class IPodPhotoSink(IPodBase):
album.remove(photo)
self.db.remove(album)
- def _empty_all_photos(self):
- for photo in self.db.PhotoAlbums[0][:]:
- self.db.remove(photo)
+ def _delete_all_photos(self):
+ for album in self.db.PhotoAlbums:
+ for photo in album[:]:
+ album.remove(photo)
+ if album.name != self.SAFE_PHOTO_ALBUM:
+ self.db.remove(album)
+ gpod.itdb_photodb_write(self.db._itdb, None)
def _get_photo_albums(self):
i = []
@@ -458,13 +553,19 @@ class IPodPhotoSink(IPodBase):
self._delete_album(album_config.get_value())
album_config.choices = self._get_photo_albums()
+ def _delete_all_click(button):
+ self._delete_all_photos()
+
album_config = config.add_item(_('Album'), 'combotext',
config_name = 'albumName',
choices = self._get_photo_albums(),
)
config.add_item(_("Delete"), "button",
initial_value = _delete_click
- )
+ )
+ config.add_item(_("Delete All Photos"), "button",
+ initial_value = _delete_all_click
+ )
def is_configured (self, isSource, isTwoWay):
diff --git a/conduit/utils/Singleton.py b/conduit/utils/Singleton.py
index d90e400..d0cf31b 100644
--- a/conduit/utils/Singleton.py
+++ b/conduit/utils/Singleton.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: UTF8 -*-
#
-# GObjectSingleton.py
+# Singleton.py
# Copyright (c) 2006 INdT (Instituto Nokia de Tecnologia)
# Author: Eduardo de Barros Lima <eduardo lima indt org br>
#
@@ -22,7 +22,7 @@
import gobject
-class GObjectSingleton(gobject.GObjectMeta):
+class _GObjectSingleton(gobject.GObjectMeta):
def __init__(cls, name, base, dict):
gobject.GObjectMeta.__init__(cls, name, base, dict)
@@ -32,7 +32,7 @@ class GObjectSingleton(gobject.GObjectMeta):
def __call__(cls, *args, **kwargs):
if not cls.__instance:
- cls.__instance = super(GObjectSingleton, cls).__call__(*args, **kwargs)
+ cls.__instance = super(_GObjectSingleton, cls).__call__(*args, **kwargs)
return cls.__instance
class Singleton:
@@ -40,7 +40,7 @@ class Singleton:
A model that implements the Singleton pattern.
"""
- __metaclass__ = GObjectSingleton
+ __metaclass__ = _GObjectSingleton
pass
diff --git a/conduit/utils/UDev.py b/conduit/utils/UDev.py
new file mode 100644
index 0000000..62cb5aa
--- /dev/null
+++ b/conduit/utils/UDev.py
@@ -0,0 +1,13 @@
+import gobject
+import gudev
+
+import logging
+log = logging.getLogger("utils.UDev")
+
+import conduit.utils as Utils
+import conduit.utils.Singleton as Singleton
+
+class UDevSingleton(Singleton.Singleton, gudev.Client):
+ def __init__(self, *args, **kwargs):
+ super(UDevSingleton, self).__init__(*args, **kwargs)
+ log.debug("Constructed: %s" % self)
diff --git a/conduit/utils/__init__.py b/conduit/utils/__init__.py
index 0067620..7f9b7ca 100644
--- a/conduit/utils/__init__.py
+++ b/conduit/utils/__init__.py
@@ -407,3 +407,16 @@ def exec_command_and_return_result(cmd, arg):
except OSError:
return None
+def get_system_data_dirs():
+ """
+ Returns the system data dirs as specified by the XDG spec.
+ http://standards.freedesktop.org/basedir-spec/latest/
+
+ This function should be removed once g_get_system_data_dirs () is wrapped
+ """
+ data_dirs = os.getenv("XDG_DATA_DIRS")
+ if data_dirs:
+ return data_dirs.split(":")
+ else:
+ return ("/usr/local/share/", "/usr/share/")
+
diff --git a/configure.ac b/configure.ac
index f4bec4d..a86a5e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -47,6 +47,7 @@ AM_CHECK_PYMOD([dateutil], , , AC_MSG_ERROR([Python module dateutil required to
AM_CHECK_PYMOD_VERSION([goocanvas], [pygoocanvas_version], [0.9.0], , AC_MSG_ERROR([Python module goocanvas >= 0.9.0 required to run Conduit]))
AM_CHECK_PYMOD_VERSION([dbus], [__version__], [0.80.0], , AC_MSG_ERROR([Python module dbus >= 0.80.0 required to run Conduit]))
AM_CHECK_PYMOD_VERSION([gio], [pygio_version], [2.16.1], , AC_MSG_ERROR([Python module gio >= 2.16.1 required to run Conduit]))
+AM_CHECK_PYMOD_VERSION([gudev], [__version__], [147.1], , AC_MSG_ERROR([Python module gudev >= 147.1 required to run Conduit]))
################################################################################
# DBus
diff --git a/setup-env.sh b/setup-env.sh
new file mode 100644
index 0000000..82d1191
--- /dev/null
+++ b/setup-env.sh
@@ -0,0 +1 @@
+export PYTHONPATH=/opt/gudev/lib/python2.6/site-packages/gtk-2.0/
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]