1
#!/usr/bin/env python2.5
2
# -*- coding: utf-8 -*-
3
""" GUI.
4
5
fMMS - MMS for fremantle
6
Copyright (C) 2010 Nick Leppänen Larsson <frals@frals.se>
7
8
@license: GNU GPLv2, see COPYING file.
9
"""
10
import os
11
import time
12
from cgi import escape
13
14
import gtk
15
import hildon
16
import osso
17
import gobject
18
import gettext
19
20
import controller_gtk as fMMSController
21
import contacts as ContactH
22
import fmms_config as fMMSconf
23
24
import logging
25
log = logging.getLogger('fmms.%s' % __name__)
26
27
_ = gettext.gettext
28
gettext.bindtextdomain('fmms','/opt/fmms/share/locale/')
29
gettext.textdomain('fmms')
30
31
class fMMS_GUI(hildon.Program):
32
	""" GUI class for the application. """
33
34
	def __init__(self):
35
		""" Initializes the GUI, creating all widgets. """
36
		self.cont = fMMSController.fMMS_controllerGTK()
37
		self.config = self.cont.config
38
		self.ch = ContactH.ContactHandler()
39
		
40
		self.osso_c = osso.Context("se.frals.fmms", self.config.get_version(), False)
41
		self.osso_rpc = osso.Rpc(self.osso_c)
42
		self.osso_rpc.set_rpc_callback("se.frals.fmms", "/se/frals/fmms", "se.frals.fmms", self.cb_open_fmms, self.osso_c)
43
		
44
		self.refreshlistview = True
45
		self.viewerimported = False
46
		self.senderimported = False
47
		self._screenwidth = 800
48
		
49
		self.avatarlist = {}
50
		self.namelist = {}
51
		self.nrlist = {}
52
	
53
		hildon.Program.__init__(self)
54
		program = hildon.Program.get_instance()
55
			
56
		self.window = hildon.StackableWindow()
57
		hildon.hildon_gtk_window_set_portrait_flags(self.window, hildon.PORTRAIT_MODE_SUPPORT)
58
		gtk.set_application_name("fMMS")
59
		self.window.set_title("fMMS")
60
		program.add_window(self.window)
61
		self.window.connect("delete_event", self.quit)
62
		
63
		self.pan = hildon.PannableArea()
64
		self.pan.set_property("mov-mode", hildon.MOVEMENT_MODE_VERT)
65
		
66
		# wonder how much memory this is wasting
67
		self.iconcell = gtk.CellRendererPixbuf()
68
		self.photocell = gtk.CellRendererPixbuf()
69
		self.textcell = gtk.CellRendererText()
70
		self.photocell.set_property('xalign', 1.0)
71
		self.textcell.set_property('mode', gtk.CELL_RENDERER_MODE_INERT)
72
		self.textcell.set_property('xalign', 0.0)
73
74
		self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, gtk.gdk.Pixbuf, str, str, str)
75
		self.treeview = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL)
76
		self.treeview.set_property("fixed-height-mode", True)
77
		self.treeview.set_model(self.liststore)
78
		
79
		# create ui
80
		self._create_ui()
81
82
		self.treeview.tap_and_hold_setup(self.liststore_mms_menu())
83
		self.treeview.tap_and_hold_setup(None)
84
		self.tapsignal = self.treeview.connect('hildon-row-tapped', self.show_mms)
85
		self.treeview.connect('button-press-event', self.cb_button_press)
86
87
		mmsBox = gtk.HBox()
88
		icon_theme = gtk.icon_theme_get_default()
89
		envelopePixbuf = icon_theme.load_icon("general_sms_button", 48, 0)
90
		envelopeImage = gtk.Image()
91
		envelopeImage.set_from_pixbuf(envelopePixbuf)
92
		envelopeImage.set_alignment(1, 0.5)
93
		mmsLabel = gtk.Label(gettext.ldgettext('rtcom-messaging-ui', "messaging_ti_new_mms"))
94
		mmsLabel.set_alignment(0, 0.5)
95
96
		mmsBox.pack_start(envelopeImage, True, True, 0)
97
		mmsBox.pack_start(mmsLabel, True, True, 0)
98
		newMsgButton = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
99
100
		newMsgButton.add(mmsBox)
101
		newMsgButton.connect('clicked', self.new_mms_button_clicked)
102
103
		""" gets the newMsgButton on top of the treeview """
104
		actionbox = self.treeview.get_action_area_box()
105
		self.treeview.set_action_area_visible(True)
106
		actionbox.add(newMsgButton)
107
108
		self.pan.add(self.treeview)
109
110
		self.livefilter = hildon.LiveSearch()
111
		modelfilter = self.liststore.filter_new()
112
		modelfilter.set_visible_func(self.cb_filter_row)
113
		self.treeview.set_model(modelfilter)
114
		self.livefilter.set_filter(modelfilter)
115
		self.livefilter.widget_hook(self.window, self.treeview)
116
117
		contBox = gtk.VBox()
118
		contBox.pack_start(self.pan, True, True, 0)
119
		contBox.pack_start(self.livefilter, False, False, 0)
120
121
		align = gtk.Alignment(1, 1, 1, 1)
122
		align.set_padding(2, 2, 10, 10)		
123
		align.add(contBox)
124
		self.window.add(align)
125
126
		menu = self.cont.create_menu(self.window)
127
		self.window.set_app_menu(menu)
128
		self.window.connect('focus-in-event', self.cb_on_focus)
129
		self.window.connect('configure-event', self._onOrientationChange)
130
		self.window.show_all()
131
		self.add_window(self.window)
132
	
133
	def _onOrientationChange(self, window, event):
134
		newwidth = window.get_screen().get_width()
135
		if newwidth == self._screenwidth:
136
			return
137
		else:
138
			self._screenwidth = newwidth
139
		self._create_ui()
140
141
	def _create_ui(self):
142
		for col in self.treeview.get_columns():
143
			self.treeview.remove_column(col)
144
	
145
		icon_col = gtk.TreeViewColumn('Icon')
146
		placeholder_col = gtk.TreeViewColumn('Photo')
147
		self.sender_col = gtk.TreeViewColumn('Sender')
148
149
		icon_col.pack_start(self.iconcell, False)
150
		icon_col.set_attributes(self.iconcell, pixbuf=0)
151
		icon_col.set_fixed_width(64)
152
		icon_col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
153
154
		self.sender_col.pack_start(self.textcell, True)
155
		self.sender_col.set_attributes(self.textcell, markup=1)
156
		# this is kinda ugly
157
		self.sender_col.set_fixed_width(self._screenwidth - 64 - 64 - 32)
158
		self.sender_col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
159
160
		placeholder_col.pack_end(self.photocell, False)
161
		placeholder_col.set_attributes(self.photocell, pixbuf=2)
162
		placeholder_col.set_fixed_width(64)
163
		placeholder_col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
164
165
		self.treeview.append_column(icon_col)
166
		self.treeview.append_column(self.sender_col)
167
		self.treeview.append_column(placeholder_col)
168
169
	def cb_filter_row(self, model, iter, user_data=None):
170
		txt = str(model.get_value(iter, 4)).lower()
171
		desc = str(model.get_value(iter, 5)).lower()
172
		filter = str(self.livefilter.get_text()).lower()
173
		if filter in txt or filter in desc:
174
			return True
175
		else:
176
			return False
177
178
	def import_viewer(self):
179
		""" This is used to import viewer only when we need it
180
		    as its quite a hog """
181
		if not self.viewerimported:
182
			import fmms_viewer as fMMSViewer
183
			global fMMSViewer
184
			self.viewerimported = True
185
186
	def import_sender(self):
187
		""" This is used to import sender_ui only when we need it
188
		    as its quite a hog """
189
		if not self.senderimported:
190
			import fmms_sender_ui as fMMSSenderUI
191
			global fMMSSenderUI
192
			self.senderimported = True
193
194
	def take_ss(self):
195
		""" Takes a screenshot of the application used by hildon to show while loading.
196
197
		inspired by Andrew Flegg and WimpWorks.
198
		@see http://maemo.org/api_refs/5.0/5.0-final/hildon/hildon-Additions-to-GTK+.html#hildon-gtk-window-take-screenshot 
199
		"""
200
		if os.path.isfile("/home/user/.cache/launch/se.frals.fmms.pvr"):
201
			gobject.timeout_add(10, hildon.hildon_gtk_window_take_screenshot, self.window, False)
202
		
203
		gobject.timeout_add(10, hildon.hildon_gtk_window_take_screenshot, self.window, True)
204
205
206
	def cb_button_press(self, widget, event):
207
		""" Used to keep track of the current selection in the treeview. """
208
		try:
209
			(self.curPath, tvcolumn, x, y) = self.treeview.get_path_at_pos(int(event.x), int(event.y))
210
		except:
211
			self.curPath = None
212
		return False
213
214
215
	def cb_on_focus(self, widget, event):
216
		""" Checks if the listview needs to be refreshed and takes screenshot. """
217
		if self.refreshlistview == True:
218
			t1 = time.clock()
219
			hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
220
			self.force_ui_update()
221
			self.liststore.clear()
222
			self.add_buttons_liststore()
223
			hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
224
			t2 = time.clock()
225
			log.info("liststore time: %s" % round(t2-t1, 3))
226
			self.refreshlistview = False
227
			modelfilter = self.liststore.filter_new()
228
			modelfilter.set_visible_func(self.cb_filter_row)
229
			self.treeview.set_model(modelfilter)
230
			self.livefilter.set_filter(modelfilter)
231
			
232
			if self.config.get_firstlaunch() < 2:
233
				settings = self.config.get_apn_settings()
234
				if settings.get('apn', '') == '' or settings.get('mmsc', '') == '':
235
					auto = self.cont.get_apn_settings_automatically()
236
					self.config.set_apn_settings(auto)
237
					settings = self.config.get_apn_settings()
238
				if settings.get('apn', '') == '' or settings.get('mmsc', '') == '':
239
					self.cont.import_configdialog()
240
					self.cont.fMMSConfigDialog.fMMS_ConfigDialog(self.window)
241
				self.config.set_firstlaunch(2)
242
				log.info("Seems this is the first time we are running.")
243
				self.config.switcharoo()
244
245
			self.take_ss()
246
247
		return False
248
249
250
	def cb_open_fmms(self, interface, method, args, user_data):
251
		""" Determines what action should be done when a dbus-call is made. """
252
		if method == 'open_mms':
253
			filename = args[0]
254
			self.refreshlistview = True
255
			self.import_viewer()
256
			if self.cont.is_fetched_push_by_transid(filename):
257
				hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
258
				self.force_ui_update()
259
				fMMSViewer.fMMS_Viewer(filename)
260
				hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
261
				return
262
			else:
263
				return
264
		elif method == 'open_gui':
265
			return
266
		elif method == 'send_mms':
267
			log.info("launching sender with args: %s", args)
268
			self.refreshlistview = False
269
			self.import_sender()
270
			fMMSSenderUI.fMMS_SenderUI(tonumber=args[0]).run()
271
			return
272
		elif method == 'send_via_service':
273
			log.info("launching sendviaservice with args: %s", args)
274
			self.refreshlistview = False
275
			self.import_sender()
276
			fMMSSenderUI.fMMS_SenderUI(withfile=args[0], subject=args[1], message=args[2]).run()
277
			self.quit()
278
		else:
279
			return
280
			
281
	def new_mms_button_clicked(self, button):
282
		""" Fired when the 'New MMS' button is clicked. """
283
		self.refreshlistview = True
284
		self.import_sender()
285
		fMMSSenderUI.fMMS_SenderUI(self.window).run()
286
		
287
	def add_buttons_liststore(self):
288
		""" Adds all messages to the liststore. """
289
		icon_theme = gtk.icon_theme_get_default()
290
291
		pushlist = self.cont.get_push_list()
292
293
		#primarytxt = self.cont.get_primary_font().to_string()
294
		#primarycolor = self.cont.get_primary_color().to_string()
295
		highlightcolor = self.cont.get_active_color().to_string()
296
		secondarytxt = self.cont.get_secondary_font().to_string()
297
		secondarycolor = self.cont.get_secondary_color().to_string()
298
		
299
		replied_icon = icon_theme.load_icon("chat_replied_sms", 48, 0)
300
		read_icon = icon_theme.load_icon("general_sms", 48, 0)
301
		unread_icon = icon_theme.load_icon("chat_unread_sms", 48, 0)
302
		default_avatar = icon_theme.load_icon("general_default_avatar", 48, 0)
303
		
304
		for varlist in pushlist:
305
			mtime = varlist['Time']
306
			# TODO: Remove date if date == today
307
			# TODO: get locale format?
308
			mtime = self.cont.convert_timeformat(mtime, "%Y-%m-%d | %H:%M")
309
310
			fname = varlist['Transaction-Id']
311
			direction = self.cont.get_direction_mms(fname)
312
313
			isread = self.cont.is_mms_read(fname)
314
315
			if direction == fMMSController.MSG_DIRECTION_OUT:
316
				sender = self.cont.get_mms_headers(varlist['Transaction-Id'])
317
				sender = sender['To'].replace("/TYPE=PLMN", "")
318
			else:
319
				sender = varlist.get('From', '00000').replace("/TYPE=PLMN", "")
320
321
			sendernr = sender
322
323
			senderuid = self.nrlist.get(sender, -1)
324
			if senderuid == -1:
325
				senderuid = self.ch.get_uid_from_number(sender)
326
				self.nrlist[sender] = senderuid
327
328
			avatar = default_avatar
329
			# compare with -1 as thats invalid contactuid
330
			if senderuid != -1 and senderuid != None:
331
				sender = self.namelist.get(senderuid, None)
332
				if not sender:
333
					sender = self.ch.get_displayname_from_uid(senderuid)
334
				if not sender:
335
					sender = sendernr
336
				
337
				avatar = self.avatarlist.get(senderuid, None)
338
				if not avatar:
339
					avatar = self.ch.get_photo_from_uid(senderuid, 48)
340
					if not avatar:
341
						avatar = default_avatar
342
343
				self.namelist[senderuid] = sender
344
				self.avatarlist[senderuid] = avatar
345
346
			if direction == fMMSController.MSG_DIRECTION_OUT:
347
				icon = replied_icon
348
			elif self.cont.is_fetched_push_by_transid(fname) and isread:
349
				icon = read_icon
350
			else:
351
				icon = unread_icon
352
353
			try:
354
				headerlist = self.cont.get_mms_headers(fname)
355
				description = headerlist['Description']
356
			except:
357
				description = varlist.get('Subject', '')
358
359
			description = description.decode('utf-8', 'ignore')
360
			primarytext = ' <span font_desc="%s" foreground="%s"><sup>%s</sup></span>' % (secondarytxt, secondarycolor, mtime)
361
			secondarytext = '\n<span font_desc="%s" foreground="%s">%s</span>' % (secondarytxt, secondarycolor, escape(description))
362
			if not isread and direction == fMMSController.MSG_DIRECTION_IN:
363
				sender = '<span foreground="%s">%s</span>' % (highlightcolor, escape(sender))
364
			else:
365
				sender = escape(sender)
366
			stringline = "%s%s%s" % (sender, primarytext, secondarytext)
367
			self.liststore.append([icon, stringline, avatar, fname, sender, description])
368
369
	def quit(self, *args):
370
		""" Quits the application. """
371
		gtk.main_quit()
372
	
373
	def force_ui_update(self):
374
		""" Force a UI update if events are pending. """
375
		while gtk.events_pending():
376
			gtk.main_iteration(False)
377
		
378
	def delete_push(self, fname):
379
		""" Deletes the given push message. """
380
		self.cont.delete_push_message(fname)
381
		
382
	def delete_mms(self, fname):
383
		""" Deletes the given MMS message. """
384
		self.cont.delete_mms_message(fname)
385
386
	def delete_push_mms(self, fname):
387
		""" Deletes both the MMS and the PUSH message. """
388
		try:
389
			self.cont.wipe_message(fname)
390
		except Exception, e:
391
			log.exception("failed to delete push mms")
392
			hildon.hildon_banner_show_information(self.window, "", gettext.ldgettext('hildon-common-strings', "sfil_ni_operation_failed"))
393
394
	def liststore_delete_clicked(self, widget):
395
		""" Shows a confirm dialog when Delete menu is clicked.
396
397
		Deletes the message if the user accepts the dialog.
398
399
		"""
400
		if self.curPath == None:
401
			return
402
			
403
		model = self.treeview.get_model().get_model()
404
		miter = model.get_iter(self.curPath)
405
		# the 4th value is the transactionid (start counting at 0)
406
		filename = model.get_value(miter, 3)
407
		
408
		confirmtxt = gettext.ldgettext('rtcom-messaging-ui', "messaging_fi_delete_1_sms")
409
		
410
		dialog = gtk.Dialog()
411
		dialog.set_transient_for(self.window)
412
		dialog.set_title(confirmtxt)
413
		dialog.add_button(gtk.STOCK_YES, 1)
414
		dialog.add_button(gtk.STOCK_NO, 0)
415
		label = gtk.Label(confirmtxt)
416
		dialog.vbox.add(label)
417
		dialog.show_all()
418
		ret = dialog.run()
419
		if ret == 1:
420
			log.info("Deleting %s", filename)
421
			self.delete_push_mms(filename)
422
			model.remove(miter)
423
		dialog.destroy()
424
		hildon.hildon_gtk_window_take_screenshot(self.window, False)
425
		hildon.hildon_gtk_window_take_screenshot(self.window, True)
426
		self.force_ui_update()
427
		return
428
	
429
	def liststore_mms_menu(self):
430
		""" Creates the context menu and shows it. """
431
		menu = gtk.Menu()
432
		menu.set_property("name", "hildon-context-sensitive-menu")
433
434
		openItem = gtk.MenuItem(gettext.ldgettext('hildon-libs', "wdgt_bd_delete"))
435
		menu.append(openItem)
436
		openItem.connect("activate", self.liststore_delete_clicked)
437
		openItem.show()
438
		
439
		menu.show_all()
440
		return menu
441
442
	def show_mms(self, treeview, path):
443
		""" Shows the message at the current selection in the treeview. """
444
		self.treeview.set_sensitive(False)
445
		# Show loading indicator
446
		hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
447
		self.force_ui_update()
448
		
449
		log.info("showing mms: %s", path)
450
		model = treeview.get_model()
451
		miter = model.get_iter(path)
452
		# the 4th value is the transactionid (start counting at 0)
453
		transactionid = model.get_value(miter, 3)
454
		
455
		switch = False
456
		if not self.cont.is_fetched_push_by_transid(transactionid) and self.config.get_connmode() == fMMSconf.CONNMODE_ICDSWITCH:
457
			if not self.cont.get_current_connection_iap_id() == self.config.get_apn():
458
				switch = self.show_switch_conn_dialog()
459
		
460
		if switch:
461
			self.cont.disconnect_current_connection()
462
		#if not self.cont.is_mms_read(transactionid) and not self.cont.get_direction_mms(transactionid) == fMMSController.MSG_DIRECTION_OUT:
463
			#self.refreshlistview = True
464
		
465
		self.import_viewer()
466
		try:
467
			fMMSViewer.fMMS_Viewer(transactionid, spawner=self)
468
		except Exception, e:
469
			log.exception("Failed to open viewer with transaction id: %s" % transactionid)
470
			hildon.hildon_banner_show_information(self.window, "", gettext.ldgettext('hildon-common-strings', "sfil_ni_operation_failed"))
471
			#raise
472
		hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
473
		self.treeview.set_sensitive(True)
474
475
	def show_switch_conn_dialog(self):
476
		""" Show confirmation dialog asking if we should disconnect """
477
		self.refreshlistview = False
478
		dialog = gtk.Dialog()
479
		dialog.set_title(gettext.ldgettext('osso-connectivity-ui', 'conn_mngr_me_int_conn_change_iap'))
480
		dialog.set_transient_for(self.window)
481
		label = gtk.Label(_("To retrieve the MMS your active connection will need to change. Switch connection?"))
482
		label.set_line_wrap(True)
483
		dialog.vbox.add(label)
484
		dialog.add_button(gtk.STOCK_YES, 1)
485
		dialog.add_button(gtk.STOCK_NO, 0)
486
		dialog.vbox.show_all()
487
		ret = dialog.run()
488
		switch = False
489
		if ret == 1:
490
			switch = True
491
		dialog.destroy()
492
		self.force_ui_update()
493
		return switch
494
		
495
	def run(self):
496
		""" Run. """
497
		self.window.show_all()
498
		gtk.main()
499
500
if __name__ == "__main__":
501
	try:
502
		app = fMMS_GUI()
503
		app.run()
504
	except:
505
		log.exception("General failure.")
506
		raise