3
# Copyright (c) 2011 Stefano Palazzo <stefano.palazzo@gmail.com>
4
# This work is licensed under the terms of the GNU General Public
5
# License version 3. If you haven't received a copy of the GPL,
6
# go to <http://www.gnu.org/licenses/gpl.txt>.
11
from gi.repository import GLib
12
from gi.repository import GObject
13
from gi.repository import Gio
14
from gi.repository import Dee
15
# FIXME: Some weird bug in Dee or PyGI makes Dee fail unless we probe
16
# it *before* we import the Unity module... ?!
17
_m = dir(Dee.SequenceModel)
18
from gi.repository import Unity
28
# - The google search api is deprecated, move this to the custom search API
30
# - ---- Make sure to follow the terms of service PRECISELY! ----
34
# - Show more results? Info on how to do that is here:
35
# <http://code.google.com/apis/websearch/docs/reference.html#_intro_fonje>
36
# - Get rid of Loading cursor on launcher after opening a link
37
# - Hide launcher icon (open issue, should it be there by default?)
38
# - Needs Nice icons (Person and Question should stay, monochrome for groups)
39
# - Searching 'Everything' is too slow compared to searching 'Questions'
40
# - Test this on a slower internet connection!
43
print "\33[1m%s\33[m" % message
49
query = urllib.urlencode({'q': q})
50
url = ('http://ajax.googleapis.com/ajax/services/search/web?v=1.0&%s'
52
search_results = urllib2.Request(
53
url, None, {'Referer': "http://unity.ubuntu.com/"}) # TODO
54
results = json.loads(urllib2.urlopen(search_results).read())
55
return results['responseData']['results']
60
# The primary bus name we grab *must* match what we specify in our .place file
62
BUS_NAME = "net.launchpad.AskUbuntuPlace"
64
# Group ids. Must match the offset into the group models
71
# Section ids. Must match the offset into the section model
72
SECTION_EVERYTHING = 0
79
class Daemon (object):
84
# Safe for the signal handlers, I don't know what any of the things
85
# in this method mean. Should have to ask someone.
87
self._entry = Unity.PlaceEntryInfo.new(
88
"/net/launchpad/askubuntuplace/mainentry")
91
# Set up all the datamodels we'll share with the Unity process
92
# See https://wiki.ubuntu.com/Unity/Places
96
# - "sections" A set of disjoint browsable categories.
97
# Fx. "Books", "Film", and "Music"
99
# - "groups" A set of labels that partition the result set into
100
# user-visible chunks. "Popular Books", "Recent Books"
102
sections_model = Dee.SharedModel.new(BUS_NAME + ".SectionsModel")
103
sections_model.set_schema("s", "s")
104
self._entry.props.sections_model = sections_model
106
groups_model = Dee.SharedModel.new(BUS_NAME + ".GroupsModel")
107
groups_model.set_schema("s", "s", "s")
108
self._entry.props.entry_renderer_info.props.groups_model = groups_model
110
global_groups_model = Dee.SharedModel.new(BUS_NAME +
111
".GlobalGroupsModel")
112
global_groups_model.set_schema("s", "s", "s")
113
self._entry.props.global_renderer_info.props.groups_model = (
116
results_model = Dee.SharedModel.new(BUS_NAME + ".ResultsModel")
117
results_model.set_schema("s", "s", "u", "s", "s", "s")
118
self._entry.props.entry_renderer_info.props.results_model = (
121
global_results_model = Dee.SharedModel.new(BUS_NAME +
122
".GlobalResultsModel")
123
global_results_model.set_schema("s", "s", "u", "s", "s", "s")
124
self._entry.props.global_renderer_info.props.results_model = (
125
global_results_model)
128
# Populate the sections and groups once we are in sync with Unity
130
sections_model.connect("notify::synchronized",
131
self._on_sections_synchronized)
132
groups_model.connect("notify::synchronized",
133
self._on_groups_synchronized)
134
global_groups_model.connect("notify::synchronized",
135
self._on_global_groups_synchronized)
138
# Set up the signals we'll receive when Unity starts to talk to us
141
# The 'active-search' property is changed when the users searches
142
# within this particular place
143
self._entry.connect("notify::active-search", self._on_search_changed)
145
# The 'active-global-search' property is changed when the user searches
146
# from the Dash aka Home Screen
147
self._entry.connect("notify::active-global-search",
148
self._on_global_search_changed)
150
# Listen for changes to the secion (Everything, Questions, Tags...)
151
self._entry.connect("notify::active-section", self._on_section_change)
153
# PlaceEntries are housed by PlaceControllers.
154
# You may have mutiple entries per controller if you like.
155
# The controller *must* have the DBus Object path you specify
156
# in your .place file
158
self._ctrl = Unity.PlaceController.new("/net/launchpad/askubuntuplace")
159
self._ctrl.add_entry(self._entry)
162
def get_search_string(self):
163
search = self._entry.props.active_search
164
return search.get_search_string() if search else None
166
def get_global_search_string(self):
167
search = self._entry.props.active_global_search
168
return search.get_search_string() if search else None
170
def _on_section_change(self, entry_info, section):
171
print "section change"
172
self.active_section = self._entry.props.active_section
174
def _on_sections_synchronized(self, sections_model, *args):
175
# Column0: display name
176
# Column1: GIcon in string format
177
sections_model.clear()
178
sections_model.append("Everything",
179
Gio.ThemedIcon.new("help").to_string())
180
sections_model.append("Questions",
181
Gio.ThemedIcon.new("help").to_string())
182
sections_model.append("Tags",
183
Gio.ThemedIcon.new("help").to_string())
184
sections_model.append("Users",
185
Gio.ThemedIcon.new("stock-person").to_string())
186
sections_model.append("Badges",
187
Gio.ThemedIcon.new("help").to_string())
189
def _on_groups_synchronized(self, groups_model, *args):
190
# Column0: group renderer
191
# Column1: display name
192
# Column2: GIcon in string format
195
# !! The row offsets must match the GROUP_* constants !!
196
groups_model.append("UnityDefaultRenderer",
198
Gio.ThemedIcon.new("help").to_string())
199
groups_model.append("UnityDefaultRenderer",
201
Gio.ThemedIcon.new("tag-new").to_string())
202
groups_model.append("UnityDefaultRenderer",
204
Gio.ThemedIcon.new("distributor-logo").to_string())
205
groups_model.append("UnityDefaultRenderer",
207
Gio.ThemedIcon.new("stock-person").to_string())
208
groups_model.append("UnityDefaultRenderer",
210
Gio.ThemedIcon.new("distributor-logo").to_string())
212
def _on_global_groups_synchronized(self, global_groups_model, *args):
213
# Just the same as the normal groups
214
self._on_groups_synchronized(global_groups_model)
216
def _on_search_changed(self, *args):
217
search = self.get_search_string()
218
results = self._entry.props.entry_renderer_info.props.results_model
219
print "Search changed to: '%s'" % search
220
self._update_results_model(search, results)
222
def _on_global_search_changed(self, entry, param_spec):
223
search = self.get_global_search_string()
224
results = self._entry.props.global_renderer_info.props.results_model
225
print "Global search changed to: '%s'" % search
226
self._update_results_model(search, results)
228
def add_default_results(self, search, model):
229
icon_hint = Gio.ThemedIcon.new("distributor-logo").to_string()
230
model.append("http://askubuntu.com/questions/ask",
231
Gio.ThemedIcon.new("distributor-logo").to_string(),
233
"text/html", "Ask your own question",
236
model.append("http://askubuntu.com/search?q=" + search.strip(),
237
Gio.ThemedIcon.new("search").to_string(), GROUP_EVERYTHING,
238
"text/html", "Search on AskUbuntu.com",
239
"Find '%s' on AskUbuntu.com" % search)
241
def clean_title(self, title):
242
# This method is very sensitive to changes in the google search API.
243
# It should be the first place to look if results "look odd".
244
title = title.replace("- Ask Ubuntu", "")
245
title = title.replace("- Ask ...", "")
246
title = title.replace("- Stack Exchange", "")
247
title = title.replace("- Stack ...", "")
248
title = title.replace("'", "'")
249
title = title.replace("&", "&")
250
title = title.replace("<", "<")
251
title = title.replace(">", ">")
254
def add_questions_results(self, search, model):
255
icon_hint = Gio.ThemedIcon.new("help").to_string()
256
print "starting search on google"
257
data = google_search("%s site:askubuntu.com/questions/" %
259
print "finished searching"
262
if (url.startswith("http://askubuntu.com/questions/") and
263
not "/tagged" in url):
264
title = i['titleNoFormatting']
265
if (title.count(" - ") >= 1 and title.split(" - ")[0] ==
266
title.split(" - ")[0].lower()): # this is a hack, but it's
267
title = ' - '.join(title.split(" - ")[1:]) # acceptable.
268
title = self.clean_title(title)
269
model.append(url, icon_hint, GROUP_QUESTIONS, "text/html",
272
def add_tags_results(self, search, model):
273
icon_hint = Gio.ThemedIcon.new("tag-new").to_string()
274
print "starting search tags on google"
275
data = google_search("%s site:http://askubuntu.com/tags/" %
277
print "finished searching tags"
280
title = i['titleNoFormatting'].replace("'", "'")
281
if "Hottest" in title and "Answers" in title:
282
title = title.split("Answers")[0].replace("Hottest ", "")
283
title = title.strip()[1:-1] # hopefully removes quotes
284
title = self.clean_title(title)
285
model.append(url, icon_hint, GROUP_TAGS, "text/html",
288
def add_badges_results(self, search, model):
289
icon_hint = Gio.ThemedIcon.new("help").to_string()
290
print "starting search for badges on google"
291
data = google_search("%s site:askubuntu.com/badges/" %
293
print "finished searching"
296
if url.startswith("http://askubuntu.com/badges/"):
297
title = i['titleNoFormatting']
299
title = title.split(" - ")[0]
300
title = self.clean_title(title)
301
model.append(url, icon_hint, GROUP_BADGES, "text/html",
304
def add_users_results(self, search, model):
305
icon_hint = Gio.ThemedIcon.new("stock_person").to_string()
306
print "starting search for users on google"
307
data = google_search("%s site:askubuntu.com/users/" %
309
print "finished searching"
313
if (url.startswith("http://askubuntu.com/users/") and
314
not "/stats" in url):
315
title = i['titleNoFormatting']
317
title = title.split(" - ")[0]
318
title = self.clean_title(title)
319
model.append(url, icon_hint, GROUP_USERS, "text/html",
322
def _update_results_model(self, search, model):
324
if self.active_section == SECTION_EVERYTHING:
325
self.add_default_results(search, model)
327
if self.active_section in (SECTION_EVERYTHING, SECTION_QUESTIONS):
328
self.add_questions_results(search, model)
329
if self.active_section in (SECTION_EVERYTHING, SECTION_TAGS):
330
self.add_tags_results(search, model)
331
if self.active_section in (SECTION_EVERYTHING, SECTION_BADGES):
332
self.add_badges_results(search, model)
333
if self.active_section in (SECTION_EVERYTHING, SECTION_USERS):
334
self.add_users_results(search, model)
337
if __name__ == "__main__":
338
session_bus_connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
339
session_bus = Gio.DBusProxy.new_sync(session_bus_connection, 0, None,
340
'org.freedesktop.DBus', '/org/freedesktop/DBus',
341
'org.freedesktop.DBus', None)
342
result = session_bus.call_sync('RequestName',
343
GLib.Variant("(su)", (BUS_NAME, 0x4)), 0, -1, None)
344
result = result.unpack()[0]
346
print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
349
GObject.MainLoop().run()