2 # -*- coding: utf-8 -*-
14 from subprocess import *
17 t = gettext.translation('ussd-widget', '/usr/share/locale')
20 print "Translation file for your language not found"
25 ussd_languages = ["German", "English", "Italian", "French", "Spanish", "Dutch", "Swedish", "Danish", "Portuguese", "Finnish", "Norwegian", "Greek", "Turkish", "Reserved1", "Reserved2", "Unspecified"]
27 # TODO Cutt off too long messages and show them in separate dialog
28 # how TODO widget vertical minimum size policy
29 # TODO interface for queryng from scripts. For example query from one widget can change color of another widget
31 class USSD_Controller:
32 def __init__( self, widget ) :
34 # number, parser, chain, interval, regexp, width, execute_at_start, retry pattern, font, [name, always show], language
35 self.default_config = ["", "", "", 0, "", 0, True, [], pango.FontDescription("Nokia Sans 18"), [_("Click to update"), False], 15]
36 self.config = self.default_config
37 self.timeout_version = 0
38 self.retry_version = 0
41 def save_config( self ) :
42 configname = os.getenv("HOME")+"/.ussdWidget.conf"
44 lockf = open(configname+".lock", 'a')
45 fcntl.flock(lockf,fcntl.LOCK_EX)
49 fconfig = open(configname,"r")
50 #Read configuration of other instances
54 my_section = line[1:].strip() == self.id
59 print _("Couldn't read previous config")
61 fconfig = open(configname,"w")
63 fconfig.write(oldconfig)
64 fconfig.write("%"+self.id+"\n");
65 fconfig.writelines(["# USSD query to be run by widget\n", "number="+self.config[0], "\n"])
66 fconfig.writelines(["#Parser command\n", "parser="+self.config[1], "\n"])
67 fconfig.writelines(["#Chain command\n", "chain="+self.config[2], "\n"])
68 fconfig.writelines(["#Update interval in minutes\n", "interval="+str(self.config[3]), "\n"])
69 fconfig.writelines(["#RegExp pattern\n", "regexp="+self.config[4], "\n"])
70 fconfig.writelines(["#Widget width\n", "width="+str(self.config[5]), "\n"])
71 fconfig.writelines(["#Execute query at start\n", "query_at_start="+str(self.config[6]), "\n"])
72 fconfig.writelines(["#Retry pattern\n"])
73 fconfig.write("retry=")
75 for i in self.config[7]:
81 fconfig.writelines(["#Font description\n", "font="+self.config[8].to_string(), "\n"])
82 fconfig.writelines(["#Font color\n", "text_color="+self.widget.get_text_color().to_string(), "\n"])
83 fconfig.writelines(["#Background color\n", "bg_color="+self.widget.get_bg_color().to_string(), "\n"])
84 fconfig.writelines(["#Widget name\n", "name="+self.config[9][0], "\n"])
85 fconfig.writelines(["#Always show widget name\n", "show_name="+str(self.config[9][1]), "\n"])
86 fconfig.writelines(["#USSD reply language\n", "language="+str(self.config[10]), "\n"])
89 fcntl.flock(lockf,fcntl.LOCK_UN)
95 def read_config( self, id ):
98 config = open(os.getenv("HOME")+"/.ussdWidget.conf","r")
108 my_section = line[1:].strip() == id
112 # This is config for another instace
115 line=line.split('=', 1)
119 print _("Error reading config on line %(line)d. = or # expected.")%{"line":i}
121 if line[0] == "number" :
122 self.config[0] = line[1].strip()
123 elif line[0] == "parser" :
124 self.config[1] = line[1].strip()
125 elif line[0] == "chain" :
126 self.config[2] = line[1].strip()
127 elif line[0] == "interval" :
129 self.config[3] = int(line[1].strip())
132 print _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
134 elif line[0] == "regexp" :
135 self.config[4] = line[1].strip()
136 elif line[0] == "width" :
138 self.config[5] = int(line[1].strip())
141 print _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
143 elif line[0] == "query_at_start" :
144 if line[1].strip() == "True" :
145 self.config[6] = True
147 self.config[6] = False
148 elif line[0] == "retry" :
149 line[1] = line[1].strip()
151 line[1] = line[1].split("-")
153 while i < len(line[1]) :
155 line[1][i] = int(line[1][i])
158 print _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
160 self.config[7] = line[1]
164 elif line[0] == "font" :
166 self.config[8] = pango.FontDescription(line[1].strip())
169 print _("Error reading config on line %(line)d. Pango font description expected.")%{"line":i}
171 elif line[0] == "bg_color" :
173 self.widget.set_bg_color(gtk.gdk.color_parse(line[1].strip()))
176 print _("Error reading config on line %(line)d. Expected color definition.")%{"line":i}
177 elif line[0] == "text_color" :
179 self.widget.set_text_color(gtk.gdk.color_parse(line[1].strip()))
182 print _("Error reading config on line %(line)d. Expected color definition.")%{"line":i}
183 elif line[0] == "name" :
184 self.config[9][0] = line[1].strip()
185 elif line[0] == "show_name" :
186 if line[1].strip() == "True" :
187 self.config[9][1] = True
189 self.config[9][1] = False
190 elif line[0] == "language" :
192 if int(line[1].strip()) >=0 and int(line[1].strip()) < len(ussd_languages):
193 self.config[10] = int(line[1].strip())
196 print _("Error reading config on line %(line)d. Unknown language code.")%{"line":i}
199 print _("Error reading config on line %(line)d. Integer expected.")%{"line":i}
202 print _("Error reading config on line %(line)d. Unexpected variable: ")%{"line":i}+line[0]
208 self.widget.error = 1
209 self.widget.set_text (_("Config error"), 5000)
213 self.widget.error = 1
214 self.widget.set_text (_("Config error"), 0)
215 print _("IO error while reading config")
217 return self.default_config
219 def on_show_settings( self, widget ) :
220 dialog = UssdConfigDialog(self.config, self.widget.get_bg_color(), self.widget.get_text_color())
223 if dialog.run() != gtk.RESPONSE_OK :
227 test = check_regexp(dialog.regexp.get_text())
229 dialog.on_error_regexp(test)
232 # Check, that we have ussd number
233 if not check_number(dialog.ussdNumber.get_text()):
234 dialog.on_error_ussd_number()
237 # Parse retry pattern
238 retry = dialog.retryEdit.get_text().strip()
240 retry = retry.split("-")
242 while i < len(retry) :
244 retry[i] = int(retry[i])
246 dialog.on_error_retry_pattern()
258 dialog.ussdNumber.get_text(),
259 dialog.parser.get_text(),
260 dialog.chain.get_text(),
261 dialog.update_interval.get_value(),
262 dialog.regexp.get_text(),
263 dialog.widthEdit.get_value(),
264 dialog.query_at_start.get_active(),
267 [dialog.wname.get_text(), dialog.show_name.get_active()],
268 dialog.language.get_active()
271 widget.set_bg_color(dialog.bg_color)
272 widget.set_text_color(dialog.text_color)
276 widget.set_width(self.config[5])
277 self.reset_timed_renew()
278 self.widget.label.modify_font(self.config[8])
282 # Before running this function widget wasn't configured
283 if self.config == self.default_config:
284 self.widget.set_text(_("Click to update"))
287 def callback_ussd_data( self, source, condition ):
288 if condition == gobject.IO_IN or condition == gobject.IO_PRI :
289 data = source.read( )
291 self.cb_reply += data
297 elif condition == gobject.IO_HUP or condition == gobject.IO_ERR :
302 print (_("serious problems in program logic"))
303 # This will force widget to show error message
310 def call_external_script( self, ussd_code, language ):
313 p = Popen(['/usr/bin/ussdquery.py', ussd_code, ussd_languages[language]], stdout=PIPE)
314 gobject.io_add_watch( p.stdout, gobject.IO_IN | gobject.IO_PRI | gobject.IO_HUP | gobject.IO_ERR , self.callback_ussd_data )
316 def ussd_renew(self, widget, event):
317 if self.widget.processing == 0:
319 widget.processing = 1
320 widget.set_text(_("Processing"), 0)
321 self.call_external_script( self.config[0], self.config[10] )
323 widget.processing = 0
325 widget.set_text(_("No config"), 0)
327 def process_reply( self ):
328 reply = self.cb_reply.strip()
331 self.widget.error = 1
332 self.widget.set_text (_("Error"), 5000)
333 if self.retry_state == len(self.config[7]):
334 self.retry_version += 1
337 self.retry_timer = gobject.timeout_add (1000*self.config[7][self.retry_state], self.retry_renew, self.retry_version)
338 self.retry_state += 1
340 self.widget.error = 0
342 if self.config[1] != "":
343 p = Popen(smart_split_string(self.config[1], reply), stdout=PIPE)
344 reply = p.communicate(reply+"\n")[0].strip()
346 if self.config[2] != "":
347 p = Popen(smart_split_string(self.config[2], reply))
349 if self.config[4] != "":
351 r = re.match( self.config[4], reply ).group( 1 )
353 r = _("Regexp Error: ") + str( e ) + "\n" + reply
357 self.widget.set_text(reply)
358 self.widget.processing = 0
360 def timed_renew(self, version):
361 if version < self.timeout_version :
363 self.ussd_renew(self.widget, None)
366 def retry_renew(self,version):
367 if self.widget.error == 0 or self.widget.processing == 1 or version < self.retry_version :
369 self.ussd_renew(self.widget, None)
372 def reset_timed_renew (self) :
373 self.timeout_version += 1
374 if self.config[3] != 0 :
375 self.timer = gobject.timeout_add (60000*self.config[3], self.timed_renew, self.timeout_version)
377 class pHelpDialog(gtk.Dialog):
378 def __init__(self, heading, text):
379 gtk.Dialog.__init__(self, heading, None,
380 gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
381 (_("OK").encode("utf-8"), gtk.RESPONSE_OK))
382 label = gtk.Label(text)
383 label.set_line_wrap (True)
388 class UssdConfigDialog(gtk.Dialog):
389 def __init__(self, config, bg_color, text_color):
390 gtk.Dialog.__init__(self, _("USSD widget"), None,
391 gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
392 (_("Save").encode("utf-8"), gtk.RESPONSE_OK))
394 self.font = config[8]
395 self.bg_color = bg_color
396 self.text_color = text_color
398 self.set_size_request(-1, 400)
399 self.ussdNumber = hildon.Entry(gtk.HILDON_SIZE_AUTO)
400 self.ussdNumber.set_text(config[0])
401 self.parser = hildon.Entry(gtk.HILDON_SIZE_AUTO)
402 self.parser.set_text(config[1])
403 self.chain = hildon.Entry(gtk.HILDON_SIZE_AUTO)
404 self.chain.set_text(config[2])
405 self.update_interval = hildon.NumberEditor(0, 9999)
406 self.update_interval.set_value(config[3])
407 self.regexp = hildon.Entry(gtk.HILDON_SIZE_AUTO)
408 self.regexp.set_text(config[4])
409 self.widthEdit = hildon.NumberEditor(0, 1000)
410 self.widthEdit.set_value(config[5])
411 self.retryEdit = hildon.Entry(gtk.HILDON_SIZE_AUTO)
414 #selector = hildon.hildon_touch_selector_new_text()
415 selector = hildon.TouchSelector(text=True)
416 for i in ussd_languages:
417 selector.append_text(i)
418 self.language = hildon.PickerButton(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
419 self.language.set_selector(selector)
420 self.language.set_active(config[10])
421 self.language.set_title(_("USSD reply language"))
422 self.language.set_size_request(-1, -1)
424 self.wname = hildon.Entry(gtk.HILDON_SIZE_AUTO)
425 self.wname.set_text(config[9][0])
426 self.show_name = gtk.CheckButton(_("Always show name"))
427 self.show_name.set_active(config[9][1])
434 self.retryEdit.set_text(text)
436 self.query_at_start = gtk.CheckButton(_("Execute query on start"))
437 self.query_at_start.set_active(config[6])
439 self.fontButton = gtk.Button(_("Font"))
440 self.fontButton.connect("clicked", self.on_show_font_selection)
442 self.colorButton = gtk.Button(_("Background color"))
443 self.colorButton.connect("clicked", self.on_show_color_selection)
444 self.textColorButton = gtk.Button(_("Text color"))
445 self.textColorButton.connect("clicked", self.on_show_text_color_selection)
447 phelp = gtk.Button("?")
448 phelp.connect("clicked", self.on_show_phelp)
450 chelp = gtk.Button("?")
451 chelp.connect("clicked", self.on_show_chelp)
453 reghelp = gtk.Button("?")
454 reghelp.connect("clicked", self.on_show_reghelp)
456 retryhelp = gtk.Button("?")
457 retryhelp.connect("clicked", self.on_show_retryhelp)
459 area = hildon.PannableArea()
462 area.add_with_viewport(vbox)
464 numberBox = gtk.HBox()
465 numberLabel = gtk.Label(_("USSD number"))
466 numberLabel.set_alignment(0,0)
467 numberLabel.set_size_request(100, -1)
468 self.ussdNumber.set_size_request(200, -1)
469 numberBox.add(numberLabel)
470 numberBox.add(self.ussdNumber)
473 vbox.add(self.language)
475 vbox.add(self.query_at_start)
478 self.show_name.set_size_request(100, -1)
479 self.wname.set_size_request(100, -1)
480 nameBox.add(self.show_name)
481 nameBox.add(self.wname)
484 parserBox = gtk.HBox()
485 parserLabel = gtk.Label(_("Parser"))
486 parserLabel.set_alignment(0,0)
487 parserLabel.set_size_request(200, -1)
488 phelp.set_size_request(10, -1)
489 parserBox.add(parserLabel)
492 vbox.add(self.parser)
494 chainBox = gtk.HBox()
495 chainLabel = gtk.Label(_("Chain"))
496 chainLabel.set_alignment(0,0)
497 chainLabel.set_size_request(200, -1)
498 chelp.set_size_request(10, -1)
499 chainBox.add(chainLabel)
504 regexpBox = gtk.HBox()
505 regexpLabel = gtk.Label(_("RegExp"))
506 regexpLabel.set_alignment(0,0)
507 regexpLabel.set_size_request(200, -1)
508 reghelp.set_size_request(10, -1)
509 regexpBox.add(regexpLabel)
510 regexpBox.add(reghelp)
512 vbox.add(self.regexp)
514 widthBox = gtk.HBox()
515 widthLabel = gtk.Label(_("Max. width"))
516 widthLabel.set_alignment(0,0)
517 symbolsLabel = gtk.Label(_("symbols"))
518 widthLabel.set_size_request(140, -1)
519 self.widthEdit.set_size_request(50, -1)
520 symbolsLabel.set_size_request(40,-1)
521 widthBox.add(widthLabel)
522 widthBox.add(self.widthEdit)
523 widthBox.add(symbolsLabel)
526 updateBox = gtk.HBox()
527 updateLabel = gtk.Label(_("Update every"))
528 updateLabel.set_alignment(0,0)
529 minutesLabel = gtk.Label(_("minutes"))
530 updateLabel.set_size_request(140, -1)
531 self.update_interval.set_size_request(50, -1)
532 minutesLabel.set_size_request(40, -1)
533 updateBox.add(updateLabel)
534 updateBox.add(self.update_interval)
535 updateBox.add(minutesLabel)
538 retryBox = gtk.HBox()
539 retryLabel = gtk.Label(_("Retry pattern"))
540 retryLabel.set_alignment(0,0)
541 retryLabel.set_size_request(200, -1)
542 retryhelp.set_size_request(10, -1)
543 retryBox.add(retryLabel)
544 retryBox.add(retryhelp)
546 vbox.add(self.retryEdit)
549 viewBox.add(self.fontButton)
550 viewBox.add(self.textColorButton)
551 viewBox.add(self.colorButton)
557 #============ Dialog helper functions =============
558 def on_show_phelp(self, widget):
559 dialog = pHelpDialog(_("Format help"), _("Reply would be passed to specified utility, output of utility would be shown to you.\n Format:\n% would be replaced by reply\n\\ invalidates special meaming of following symbol\n\" and ' work as usual\nspace delimits command line parameters of utility"))
563 def on_show_chelp(self, widget):
564 dialog = pHelpDialog(_("Format help"), _("Reply would be passed to specified utility after parser utility. May be used for logging, statistics etc.\n Format:\n% would be replaced by reply\n\\ invalidates special meaning of following symbol\n\" and ' work as usual\nspace delimits command line parameters of utility"))
568 def on_show_reghelp(self, widget):
569 dialog = pHelpDialog(_("Format help"), _("Standard python regexps. Use\n (.+?[\d\,\.]+)\n to delete everything after first number."))
573 def on_show_retryhelp(self, widget):
574 dialog = pHelpDialog(_("Format help"), _("Pauses between attemps (in seconds), delimited by -. For example 15-15-300 means \"In case of failure wait 15 seconds, try again, on failure wait 15 more secodns and try again, on failure make last attempt after 5 minutes\""))
578 def on_error_regexp(self, error):
579 dialog = pHelpDialog(_("Regexp syntax error"), error )
583 def on_error_ussd_number(self):
584 dialog = pHelpDialog(_("Incorrect USSD number"), _("USSD number should contain only digits, +, * or #") )
588 def on_error_retry_pattern(self):
589 dialog = pHelpDialog(_("Incorrect retry pattern"), _("Retry pattern should contain only numbers, delimited by -") )
593 def on_show_color_selection (self, event):
594 colorDialog = gtk.ColorSelectionDialog(_("Choose background color"))
595 colorDialog.colorsel.set_current_color(self.bg_color)
596 if colorDialog.run() == gtk.RESPONSE_OK :
597 self.bg_color = colorDialog.colorsel.get_current_color()
598 colorDialog.destroy()
600 def on_show_text_color_selection (self, event):
601 colorDialog = gtk.ColorSelectionDialog(_("Choose text color"))
602 colorDialog.colorsel.set_current_color(self.text_color)
603 if colorDialog.run() == gtk.RESPONSE_OK :
604 self.text_color = colorDialog.colorsel.get_current_color()
605 colorDialog.destroy()
607 def on_show_font_selection (self, event):
608 fontDialog = gtk.FontSelectionDialog(_("Choose a font"))
609 fontDialog.set_font_name(self.font.to_string())
611 if fontDialog.run() != gtk.RESPONSE_OK :
615 self.font = pango.FontDescription (fontDialog.get_font_name())
618 def smart_split_string (str, query) :
621 # Is simbol backslashed?
623 # Quotes: 1 - ", 2 - ', 0 - no quotes
625 for i in range(len(str)) :
626 if bs == 0 and (str[i] == '"' and qs == 1 or str[i] == "'" and qs == 2) :
628 elif bs == 0 and qs == 0 and (str[i] == '"' or str[i] == "'") :
633 elif bs == 0 and str[i] == '\\' :
635 elif bs == 0 and str[i] == '%' :
639 if bs == 1 and str[i] != '\\' and str[i] != '"' and str[i] != "'" :
641 if qs == 0 and (str[i] == " " or str[i] == "\t") :
652 def check_regexp(regexp):
659 def check_number(number):
661 if not (s in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "*", "#"]) :
665 #=============== The widget itself ================
667 def get_color(logicalcolorname):
668 settings = gtk.settings_get_default()
669 color_style = gtk.rc_get_style_by_paths(settings, 'GtkButton', 'osso-logical-colors', gtk.Button)
670 return color_style.lookup_color(logicalcolorname)
672 class UssdWidgetPlugin(hildondesktop.HomePluginItem):
674 hildondesktop.HomePluginItem.__init__(self)
677 self.bg_color=gtk.gdk.color_parse('#000000')
678 self.text_color=gtk.gdk.color_parse('#ffffff')
680 self.timeout_version = 0
682 colormap = self.get_screen().get_rgba_colormap()
683 self.set_colormap (colormap)
685 self.controller = USSD_Controller(self)
687 # TODO Click event would be better
688 self.connect("button-press-event", self.controller.ussd_renew)
690 self.vbox = gtk.HBox()
693 self.set_settings(True)
694 self.connect("show-settings", self.controller.on_show_settings)
695 self.label = gtk.Label("")
697 self.vbox.add(self.label)
698 self.vbox.set_child_packing(self.label, False, False, 0, gtk.PACK_START)
699 self.label.set_padding(15, 10)
700 self.label.set_size_request(-1,-1)
701 self.set_size_request(-1,-1)
702 self.label.set_line_wrap (True)
707 config = self.controller.read_config(self.get_applet_id())
708 self.set_width(config[5])
711 self.controller.ussd_renew(self, None)
713 self.controller.reset_timed_renew()
714 hildondesktop.HomePluginItem.do_show(self)
716 def error_return (self):
717 if self.error == 1 and self.processing == 0:
718 self.set_text(self.text)
722 # -1 - This is a permanent text message
723 # 0 - This is service message, but it shouldn't be hidden automatically
724 # >0 - This is service message, show permament message after showfor milliseconds
725 def set_text(self, text, showfor=-1):
727 # Show previous text after 5 seconds
728 gobject.timeout_add (showfor, self.error_return)
733 config = self.controller.get_config()
734 if config[9][1] or text == "":
735 text = config[9][0]+text
736 self.label.set_text(text)
741 def set_width(self, width):
743 self.label.set_width_chars (width)
745 self.label.set_width_chars(-1)
747 def set_bg_color(self, color):
748 self.bg_color = color
750 def get_bg_color(self):
753 def set_text_color(self, color):
754 self.label.modify_fg(gtk.STATE_NORMAL, color)
755 self.text_color = color
757 def get_text_color(self):
758 return self.text_color
760 def _expose(self, event):
761 cr = self.window.cairo_create()
764 width, height = self.label.allocation[2], self.label.allocation[3]
766 #/* a custom shape, that could be wrapped in a function */
767 x0 = 0 #/*< parameters like cairo_rectangle */
770 radius = min(15, width/2, height/2) #/*< and an approximate curvature radius */
775 cr.move_to (x0, y0 + radius)
776 cr.arc (x0 + radius, y0 + radius, radius, 3.14, 1.5 * 3.14)
777 cr.line_to (x1 - radius, y0)
778 cr.arc (x1 - radius, y0 + radius, radius, 1.5 * 3.14, 0.0)
779 cr.line_to (x1 , y1 - radius)
780 cr.arc (x1 - radius, y1 - radius, radius, 0.0, 0.5 * 3.14)
781 cr.line_to (x0 + radius, y1)
782 cr.arc (x0 + radius, y1 - radius, radius, 0.5 * 3.14, 3.14)
786 fg_color = get_color("ActiveTextColor")
791 bg_color=self.bg_color
793 cr.set_source_rgba (bg_color.red / 65535.0, bg_color.green/65535.0, bg_color.blue/65535.0, 0.7)
797 cr.set_source_rgba (1.0, 0.0, 0.0, 0.5)
799 cr.set_source_rgba (fg_color.red / 65535.0, fg_color.green / 65535.0, fg_color.blue / 65535.0, 0.7)
802 def do_expose_event(self, event):
805 self.vbox.do_expose_event (self, event)
807 hd_plugin_type = UssdWidgetPlugin
809 # The code below is just for testing purposes.
810 # It allows to run the widget as a standalone process.
811 if __name__ == "__main__":
813 gobject.type_register(hd_plugin_type)
814 obj = gobject.new(hd_plugin_type, plugin_id="plugin_id")