From 7c7200d0a1eab3305ab7530cd16601bf4be2b4d4 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 17 Jul 2013 15:38:38 +0200 Subject: [PATCH 4/5] PIM: adapt to locale changes at runtime (FDO #66618) Listen to signals from localed D-Bus system service and update all internal state which depends on the current locale. This state includes: - pre-computed data in all loaded contacts - filtering (for example, case sensitivity is locale dependent) - the sort order In the current implementation, the entire LocaleFactory gets replaced when the environment changes. The new instance gets installed in FullView, then the sort comparison gets created anew and the existing setSortOrder() updates the main view, which includes re-computing all pre-computed data. As a last step, the search filter is re-recreated and filtering gets updated in all active filter views. There is a minor risk that unnecessary changes get emitted because first filtered views react to modified contacts and/or reshuffling them, then later their filter gets replaced. --- src/dbus/server/pim/folks.cpp | 10 +++++ src/dbus/server/pim/folks.h | 6 +++ src/dbus/server/pim/full-view.cpp | 9 +++++ src/dbus/server/pim/full-view.h | 6 +++ src/dbus/server/pim/localed.py | 76 +++++++++++++++++++++++++++++++++++++ src/dbus/server/pim/manager.cpp | 60 ++++++++++++++++++++++++++++- src/dbus/server/pim/manager.h | 7 ++++ src/dbus/server/pim/testpim.py | 50 ++++++++++++++++++++++++ 8 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 src/dbus/server/pim/localed.py diff --git a/src/dbus/server/pim/folks.cpp b/src/dbus/server/pim/folks.cpp index 6fd4dba..a28f29e 100644 --- a/src/dbus/server/pim/folks.cpp +++ b/src/dbus/server/pim/folks.cpp @@ -275,6 +275,16 @@ void IndividualAggregator::setCompare(const boost::shared_ptr } } +void IndividualAggregator::setLocale(const boost::shared_ptr &locale) +{ + m_locale = locale; + + if (m_view) { + m_view->setLocale(m_locale); + } +} + + void IndividualAggregator::start() { if (!m_view) { diff --git a/src/dbus/server/pim/folks.h b/src/dbus/server/pim/folks.h index 853240b..4b2bc5e 100644 --- a/src/dbus/server/pim/folks.h +++ b/src/dbus/server/pim/folks.h @@ -315,6 +315,12 @@ class IndividualAggregator void setCompare(const boost::shared_ptr &compare); /** + * Change current locale. Must be followed by setCompare() to update + * any pre-computed data. + */ + void setLocale(const boost::shared_ptr &locale); + + /** * Starts pulling and sorting of contacts. * Creates m_view and starts populating it. * Can be called multiple times. diff --git a/src/dbus/server/pim/full-view.cpp b/src/dbus/server/pim/full-view.cpp index b0d5d7a..fdc7fa7 100644 --- a/src/dbus/server/pim/full-view.cpp +++ b/src/dbus/server/pim/full-view.cpp @@ -104,6 +104,15 @@ boost::shared_ptr FullView::create(const FolksIndividualAggregatorCXX return view; } + +void FullView::setLocale(const boost::shared_ptr &locale) +{ + m_locale = locale; + + // Don't recompute all IndividualData content. That will be done + // as part of setCompare(), which must be called later. +} + void FullView::individualsChanged(GeeSet *added, GeeSet *removed, gchar *message, diff --git a/src/dbus/server/pim/full-view.h b/src/dbus/server/pim/full-view.h index 3035fa5..9c17fd8 100644 --- a/src/dbus/server/pim/full-view.h +++ b/src/dbus/server/pim/full-view.h @@ -78,6 +78,12 @@ class FullView : public IndividualView static boost::shared_ptr create(const FolksIndividualAggregatorCXX &folks, const boost::shared_ptr &locale); + /** + * Change locale. Updating pre-computed data must be triggered by + * calling setCompare() later. + */ + void setLocale(const boost::shared_ptr &locale); + /** FolksIndividualAggregator "individuals-changed" slot */ void individualsChanged(GeeSet *added, GeeSet *removed, diff --git a/src/dbus/server/pim/localed.py b/src/dbus/server/pim/localed.py new file mode 100644 index 0000000..e196812 --- /dev/null +++ b/src/dbus/server/pim/localed.py @@ -0,0 +1,76 @@ +import dbus.service +import os + +class Localed(dbus.service.Object): + """a fake localed systemd implementation""" + + LOCALED_INTERFACE = "org.freedesktop.locale1" + LOCALED_NAME = "org.freedesktop.locale1" + LOCALED_PATH = "/org/freedesktop/locale1" + LOCALED_LOCALE = "Locale" + + def __init__(self, bus=None): + if bus is None: + bus = dbus.SystemBus() + bus_name = dbus.service.BusName(self.LOCALED_NAME, bus=bus) + dbus.service.Object.__init__(self, bus_name, self.LOCALED_PATH) + + locale = [] + for name in ( + "LANG", + "LC_CTYPE", + "LC_NUMERIC", + "LC_TIME", + "LC_COLLATE", + "LC_MONETARY", + "LC_MESSAGES", + "LC_PAPER", + "LC_NAME", + "LC_ADDRESS", + "LC_TELEPHONE", + "LC_MEASUREMENT", + "LC_IDENTIFICATION", + ): + value = os.environ.get(name, None) + if value != None: + locale.append('%s=%s' % (name, value)) + + self.properties = { + self.LOCALED_LOCALE : locale, + } + + def SetLocale(self, locale, invalidate=False): + self.properties[self.LOCALED_LOCALE] = locale + if invalidate: + self.PropertiesChanged(self.LOCALED_INTERFACE, + { }, + [ self.LOCALED_LOCALE ]) + else: + self.PropertiesChanged(self.LOCALED_INTERFACE, + { self.LOCALED_LOCALE: locale }, + []) + + # Properties + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ss', + out_signature='v') + def Get(self, interface_name, property_name): + return self.GetAll(interface_name)[property_name] + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='s', + out_signature='a{sv}') + def GetAll(self, interface_name): + if interface_name == self.LOCALED_INTERFACE: + return self.properties + else: + raise dbus.exceptions.DBusException( + 'org.syncevolution.UnknownInterface', + 'The fake localed object does not implement the %s interface' + % interface_name) + + @dbus.service.signal(dbus.PROPERTIES_IFACE, + signature='sa{sv}as') + def PropertiesChanged(self, interface_name, changed_properties, + invalidated_properties): + pass diff --git a/src/dbus/server/pim/manager.cpp b/src/dbus/server/pim/manager.cpp index 7bc626d..c597530 100644 --- a/src/dbus/server/pim/manager.cpp +++ b/src/dbus/server/pim/manager.cpp @@ -27,6 +27,7 @@ #include "../resource.h" #include "../client.h" #include "../session.h" +#include "../localed-listener.h" #include #include @@ -81,8 +82,15 @@ Manager::Manager(const boost::shared_ptr &server) : m_mainThread(g_thread_self()), m_server(server), m_locale(LocaleFactory::createFactory()), + m_localedListener(LocaledListener::create()), emitSyncProgress(*this, "SyncProgress") { + // Update our own environment and sorting on each locale change. + m_localedListener->m_localeValues.connect(boost::bind(&LocaledListener::setLocale, m_localedListener.get(), _1)); + m_localedListener->m_localeChanged.connect(boost::bind(&Manager::localeChanged, this)); + + // Get the environment once from localed, just to be sure. + m_localedListener->check(boost::bind(&LocaledListener::setLocale, boost::weak_ptr(m_localedListener), _1)); } Manager::~Manager() @@ -257,6 +265,23 @@ void Manager::initSorting(const std::string &order) m_folks->setCompare(compare); } +void Manager::localeChanged() +{ + // First update locale. + m_locale = LocaleFactory::createFactory(); + // Change sorting. First install new locale in + // IndividualAggregator and through it in FullView. + if (m_folks) { + m_folks->setLocale(m_locale); + } + // Then update IndividualData of all loaded individuals by + // changing the sort order. + initSorting(m_sortOrder); + + // Now update views. + m_localeChanged(m_locale); +} + boost::shared_ptr Manager::create(const boost::shared_ptr &server) { boost::shared_ptr manager(new Manager(server)); @@ -366,6 +391,7 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper boost::shared_ptr m_view; boost::shared_ptr m_locale; boost::weak_ptr m_owner; + LocaleFactory::Filter_t m_filter; struct Change { Change() : m_start(0), m_call(NULL) {} @@ -381,6 +407,7 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper ViewResource(const boost::shared_ptr view, const boost::shared_ptr &locale, const boost::shared_ptr &owner, + const LocaleFactory::Filter_t &filter, GDBusCXX::connection_type *connection, const GDBusCXX::Caller_t &ID, const GDBusCXX::DBusObject_t &agentPath) : @@ -394,6 +421,7 @@ class ViewResource : public Resource, public GDBusCXX::DBusObjectHelper m_view(view), m_locale(locale), m_owner(owner), + m_filter(filter), // use ViewAgent interface m_quiescent(m_viewAgent, "Quiescent"), @@ -707,6 +735,7 @@ public: static boost::shared_ptr create(const boost::shared_ptr &view, const boost::shared_ptr &locale, const boost::shared_ptr &owner, + const LocaleFactory::Filter_t &filter, GDBusCXX::connection_type *connection, const GDBusCXX::Caller_t &ID, const GDBusCXX::DBusObject_t &agentPath) @@ -714,6 +743,7 @@ public: boost::shared_ptr viewResource(new ViewResource(view, locale, owner, + filter, connection, ID, agentPath)); @@ -770,10 +800,32 @@ public: void replaceSearch(const std::vector &filterArray, bool refine) { // Same as in Search(). - LocaleFactory::Filter_t filter = filterArray; - boost::shared_ptr individualFilter = m_locale->createFilter(filter, 0); + m_filter = filterArray; + redoSearch(refine); + } + + /** + * Start filtering again, using the current environment. To be + * called after a locale change or when m_filter changed. + * + * @param refine true only if it is known to the caller that the new result set is + * a subset of the current one, false if uncertain + */ + void redoSearch(bool refine) + { + boost::shared_ptr individualFilter = m_locale->createFilter(m_filter, 0); m_view->replaceFilter(individualFilter, refine); } + + /** + * Change locale, then refilter because the filter may have changed. + */ + void setLocale(const boost::shared_ptr &locale) + { + m_locale = locale; + redoSearch(true); + } + }; unsigned int ViewResource::m_counter; @@ -897,11 +949,15 @@ void Manager::doSearch(const ESourceRegistryCXX ®istry, boost::shared_ptr viewResource(ViewResource::create(view, m_locale, client, + filter, getConnection(), ID, agentPath)); client->attach(boost::shared_ptr(viewResource)); + // Redo search when locale changes. + m_localeChanged.connect(LocaleChangedSignal::slot_type(&ViewResource::setLocale, viewResource.get(), _1).track(viewResource)); + // created local resource result->done(viewResource->getPath()); } diff --git a/src/dbus/server/pim/manager.h b/src/dbus/server/pim/manager.h index d43a523..e9fee00 100644 --- a/src/dbus/server/pim/manager.h +++ b/src/dbus/server/pim/manager.h @@ -34,6 +34,8 @@ #include SE_BEGIN_CXX +class LocaledListener; + /** * Implementation of org._01.pim.contacts.Manager. */ @@ -44,6 +46,7 @@ class Manager : public GDBusCXX::DBusObjectHelper boost::shared_ptr m_server; boost::shared_ptr m_folks; boost::shared_ptr m_locale; + boost::shared_ptr m_localedListener; /** Stores "sort" property in XDG ~/.config/syncevolution/pim-manager.ini'. */ boost::shared_ptr m_configNode; std::string m_sortOrder; @@ -64,6 +67,10 @@ class Manager : public GDBusCXX::DBusObjectHelper void initFolks(); void initDatabases(); void initSorting(const std::string &order); + void localeChanged(); + + typedef boost::signals2::signal &locale)> LocaleChangedSignal; + LocaleChangedSignal m_localeChanged; public: /** Manager.Start() */ diff --git a/src/dbus/server/pim/testpim.py b/src/dbus/server/pim/testpim.py index 4ee4f06..2c1d18a 100755 --- a/src/dbus/server/pim/testpim.py +++ b/src/dbus/server/pim/testpim.py @@ -45,6 +45,8 @@ import codecs import pprint import shutil +import localed + # Update path so that testdbus.py can be found. pimFolder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])) if pimFolder not in sys.path: @@ -2579,6 +2581,54 @@ END:VCARD (), ) + @timeout(60) + @property("ENV", "LC_TYPE=zh_CN.UTF-8 LANG=zh_CN.UTF-8 DBUS_TEST_LOCALED=session") + def testLocaled(self): + # Use mixed Chinese/Western names, because then the locale really matters. + namespinyin = ('Adams', 'Jeffries', u'江', 'Meadows', u'鳥', u'女性' ) + namesgerman = ('Adams', 'Jeffries', 'Meadows', u'女性', u'江', u'鳥' ) + numtestcases = len(namespinyin) + self.doFilter(namespinyin, ()) + + daemon = localed.Localed(bus) + msg = None + try: + # Broadcast Locale value together with PropertiesChanged signal. + self.view.quiescentCount = 0 + daemon.SetLocale(['LC_TYPE=de_DE.UTF-8', 'LANG=de_DE.UTF-8'], False) + logging.log('reading contacts, German') + self.runUntil('German sorting', + check=lambda: self.assertEqual([], self.view.errors), + until=lambda: self.view.quiescentCount > 0) + self.view.read(0, numtestcases) + self.runUntil('German contacts', + check=lambda: self.assertEqual([], self.view.errors), + until=lambda: self.view.haveData(0, numtestcases)) + for i, name in enumerate(namesgerman): + msg = u'contact #%d with name %s in\n%s' % (i, name, pprint.pformat(self.stripDBus(self.view.contacts, sortLists=False))) + self.assertEqual(name, self.view.contacts[i]['full-name']) + + # Switch back to Pinyin without including the new value. + self.view.quiescentCount = 0 + daemon.SetLocale(['LC_TYPE=zh_CN.UTF-8', 'LANG=zh_CN.UTF-8'], True) + logging.log('reading contacts, Pinyin') + self.runUntil('Pinyin sorting', + check=lambda: self.assertEqual([], self.view.errors), + until=lambda: self.view.quiescentCount > 0) + self.view.read(0, numtestcases) + self.runUntil('Pinyin contacts', + check=lambda: self.assertEqual([], self.view.errors), + until=lambda: self.view.haveData(0, numtestcases)) + for i, name in enumerate(namespinyin): + msg = u'contact #%d with name %s in\n%s' % (i, name, pprint.pformat(self.stripDBus(self.view.contacts, sortLists=False))) + self.assertEqual(name, self.view.contacts[i]['full-name']) + except Exception, ex: + if msg: + info = sys.exc_info() + raise Exception('%s:\n%s' % (msg, repr(ex))), None, info[2] + else: + raise + # Not supported correctly by ICU? # See icu-support "Subject: Austrian phone book sorting" # @timeout(60) -- 1.7.10.4