Changes in Singleton - do not run __init__() multiple times
[pedometerwidget] / src / usr / lib / hildon-desktop / pedometer_widget_home.py
1 #Pedometer Home Widget
2 #Author: Mirestean Andrei < andrei.mirestean at gmail.com >
3 #
4 #This program is free software: you can redistribute it and/or modify
5 #it under the terms of the GNU General Public License as published by
6 #the Free Software Foundation, either version 3 of the License, or
7 #(at your option) any later version.
8 #
9 #This program is distributed in the hope that it will be useful,
10 #but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #GNU General Public License for more details.
13 #
14 #You should have received a copy of the GNU General Public License
15 #along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import os
18 import time
19 import pickle
20 from datetime import date, timedelta
21 from xml.dom.minidom import getDOMImplementation, parseString
22
23 import gobject
24 import gconf
25 import gtk
26 import cairo
27
28 import pygst
29 pygst.require("0.10")
30 import gst
31
32 import hildondesktop
33 import hildon
34
35 PATH = "/apps/pedometerhomewidget"
36 MODE = PATH + "/mode"
37 HEIGHT = PATH + "/height"
38 STEP_LENGTH = PATH + "/step_length"
39 WEIGHT = PATH + "/weight"
40 UNIT = PATH + "/unit"
41 SENSITIVITY = PATH + "/sensitivity"
42 ASPECT = PATH + "/aspect"
43 SECONDVIEW = PATH + "/secondview"
44 GRAPHVIEW = PATH + "/graphview"
45 NOIDLETIME = PATH + "/noidletime"
46 LOGGING = PATH + "/logging"
47
48 ALARM_PATH = PATH + "/alarm"
49 ALARM_ENABLE = ALARM_PATH + "/enable"
50 ALARM_FNAME = ALARM_PATH + "/fname"
51 ALARM_TYPE = ALARM_PATH + "/type"
52 ALARM_INTERVAL = ALARM_PATH + "/interval"
53
54 ICONSPATH = "/opt/pedometerhomewidget/"
55
56 unit = 0
57
58 class Singleton(object):
59     _instance = None
60     _references = 0
61     def __new__(cls, *args, **kwargs):
62         cls._references+=1
63         if not cls._instance:
64             cls._instance = super(Singleton, cls).__new__(
65                                 cls, *args, **kwargs)
66         return cls._instance
67
68 class PedoIntervalCounter(Singleton):
69     MIN_THRESHOLD = 500
70     MIN_TIME_STEPS = 0.5
71     sensitivity = 100
72     mode = 0
73     x = []
74     y = []
75     z = []
76     t = []
77
78     #TODO: check if last detected step is at the end of the interval
79
80     def set_vals(self, coords, tval):
81         self.x = coords[0]
82         self.y = coords[1]
83         self.z = coords[2]
84         self.t = tval
85
86     def set_mode(self, mode):
87         #runnig, higher threshold to prevent fake steps
88         self.mode = mode
89         if mode == 1:
90             self.MIN_THRESHOLD = 650.0 * (200 - self.sensitivity) / 100
91             self.MIN_TIME_STEPS = 0.35
92         #walking
93         else:
94             self.MIN_THRESHOLD = 500.0 * (200 - self.sensitivity) / 100
95             self.MIN_TIME_STEPS = 0.5
96
97     def set_sensitivity(self, value):
98         self.sensitivity = value
99         self.set_mode(self.mode)
100
101     def calc_mean(self, vals):
102         sum = 0
103         for i in vals:
104             sum += i
105         if len(vals) > 0:
106             return sum / len(vals)
107         return 0
108
109     def calc_stdev(self, vals):
110         rez = 0
111         mean = self.calc_mean(vals)
112         for i in vals:
113             rez += pow(abs(mean - i), 2)
114         return math.sqrt(rez / len(vals))
115
116     def calc_threshold(self, vals):
117         vmax = max(vals)
118         vmin = min(vals)
119         mean = self.calc_mean(vals)
120         threshold = max (abs(mean - vmax), abs(mean - vmin))
121         return threshold
122
123     def count_steps(self, vals, t):
124         threshold = self.MIN_THRESHOLD
125         mean = self.calc_mean(vals)
126         cnt = 0
127         i = 0
128         while i < len(vals):
129             if abs(vals[i] - mean) > threshold:
130                 cnt += 1
131                 ntime = t[i] + self.MIN_TIME_STEPS
132                 while i < len(vals) and t[i] < ntime:
133                     i += 1
134             i += 1
135         return cnt
136
137     def get_best_values(self, x, y, z):
138         dev1 = self.calc_stdev(x)
139         dev2 = self.calc_stdev(y)
140         dev3 = self.calc_stdev(z)
141         dev_max = max(dev1, dev2, dev3)
142
143         if (abs(dev1 - dev_max) < 0.001):
144             logger.info("X chosen as best axis, stdev %f" % dev1)
145             return x
146         elif (abs(dev2 - dev_max) < 0.001):
147             logger.info("Y chosen as best axis, stdev %f" % dev2)
148             return y
149         else:
150             logger.info("Z chosen as best axis, stdev %f" % dev3)
151             return z
152
153     def number_steps(self):
154         vals = self.get_best_values(self.x, self.y, self.z)
155         return self.count_steps(vals, self.t)
156
157 class PedoValues():
158     def __init__(self, time=0, steps=0, dist=0, calories=0):
159         self.time = time
160         self.steps = steps
161         self.calories = calories
162         self.dist = dist
163
164     def __add__(self, other):
165         return PedoValues(self.time + other.time,
166                           self.steps + other.steps,
167                           self.dist + other.dist,
168                           self.calories + other.calories)
169
170     def __sub__(self, other):
171         return PedoValues(self.time - other.time,
172                           self.steps - other.steps,
173                           self.dist - other.dist,
174                           self.calories - other.calories)
175
176     def get_print_time(self):
177         tdelta = self.time
178         hours = int(tdelta / 3600)
179         tdelta -= 3600 * hours
180         mins = int(tdelta / 60)
181         tdelta -= 60 * mins
182         secs = int(tdelta)
183         strtime = "%.2d:%.2d:%.2d" % (hours, mins, secs)
184         return strtime
185
186     def get_print_distance(self):
187         global unit
188         if self.dist > 1000:
189             if unit == 0:
190                 return "%.2f km" % (self.dist / 1000)
191             else:
192                 return "%.2f mi" % (self.dist / 1609.344)
193         else:
194             if unit == 0:
195                 return "%d m" % self.dist
196             else:
197                 return "%d ft" % int(self.dist * 3.2808)
198
199     def get_avg_speed(self):
200         global unit
201         conv = 0
202         if unit:
203             conv = 2.23693629
204         else:
205             conv = 3.6
206
207         if self.time == 0:
208             return 0
209         speed = 1.0 * self.dist / self.time
210         return speed * conv
211
212     def get_print_avg_speed(self):
213         global unit
214         suffix = ""
215         conv = 0
216         if unit:
217             suffix = "mi/h"
218             conv = 2.23693629
219         else:
220             suffix = "km/h"
221             conv = 3.6
222
223         if self.time == 0:
224             return "N/A " + suffix
225         speed = 1.0 * self.dist / self.time
226         #convert from meters per second to km/h or mi/h
227         speed *= conv
228         return "%.2f %s" % (speed, suffix)
229
230     def get_print_steps(self):
231         return str(self.steps)
232
233     def get_print_calories(self):
234         return "%.2f" % self.calories
235
236 class PedoRepository(Singleton):
237     values = {}
238
239     def load(self):
240         raise NotImplementedError("Must be implemented by subclass")
241
242     def save(self):
243         raise NotImplementedError("Must be implemented by subclass")
244
245     def reset_values(self):
246         self.values = {}
247         self.save()
248
249     def get_history_count(self):
250         """return the number of days in the log"""
251         return len(values)
252
253     def get_values(self):
254         return self.values
255
256     def add_values(self, values, when=None):
257         if when is None:
258             when = date.today()
259         """add PedoValues values to repository """
260         try:
261             self.values[when] = self.values[when] + values
262         except KeyError:
263             self.values[when] = values
264
265     def get_last_7_days(self):
266         ret = []
267         day = date.today()
268         for i in range(7):
269             try:
270                 ret.append(self.values[day])
271             except KeyError:
272                 ret.append(PedoValues())
273             day = day - timedelta(days=1)
274         return ret
275
276     def get_last_weeks(self):
277         delta = timedelta(days=1)
278         day = date.today()
279         week = int(date.today().strftime("%W"))
280         val = PedoValues()
281         ret = []
282         for i in range(56):
283             try:
284                 val += self.values[day]
285             except KeyError:
286                 pass
287             w = int(day.strftime("%W"))
288             if w != week:
289                 ret.append(val)
290                 val = PedoValues()
291                 week = w
292                 if len(ret) == 7:
293                     break
294             day -= delta
295         return ret
296
297     def get_alltime_values(self):
298         ret = PedoValues()
299         for k, v in self.values.iteritems():
300             ret = ret + v
301         return ret
302
303     def get_today_values(self):
304         try:
305             return self.values[date.today()]
306         except KeyError:
307             return PedoValues()
308
309     def get_this_week_values(self):
310         day = date.today()
311         ret = PedoValues()
312         while True:
313             try:
314                 ret += self.values[day]
315             except:
316                 pass
317             if day.weekday() == 0:
318                 break
319             day = day - timedelta(days=1)
320
321         return ret
322
323 class PedoRepositoryXML(PedoRepository):
324     DIR = os.path.join(os.path.expanduser("~"), ".pedometer")
325     FILE = os.path.join(DIR, "data.xml")
326     FILE2 = os.path.join(DIR, "pickle.log")
327     def __init__(self):
328         if not os.path.exists(self.DIR):
329             os.makedirs(self.DIR)
330         PedoRepository.__init__(self)
331
332     def load(self):
333         try:
334             f = open(self.FILE, "r")
335             dom = parseString(f.read())
336             values = dom.getElementsByTagName("pedometer")[0]
337             for v in values.getElementsByTagName("date"):
338                 d = int(v.getAttribute("ordinal_day"))
339                 steps = int(v.getAttribute("steps"))
340                 calories = float(v.getAttribute("calories"))
341                 dist = float(v.getAttribute("dist"))
342                 time = float(v.getAttribute("time"))
343                 day = date.fromordinal(d)
344                 self.values[day] = PedoValues(time, steps, dist, calories)
345
346             f.close()
347         except Exception, e:
348             logger.error("Error while loading data from xml file: %s" % e)
349
350     def save(self):
351         try:
352             f = open(self.FILE, "w")
353
354             impl = getDOMImplementation()
355
356             newdoc = impl.createDocument(None, "pedometer", None)
357             top_element = newdoc.documentElement
358             for k, v in self.values.iteritems():
359                 d = newdoc.createElement('date')
360                 d.setAttribute("day", str(k.isoformat()))
361                 d.setAttribute("ordinal_day", str(k.toordinal()))
362                 d.setAttribute("steps", str(v.steps))
363                 d.setAttribute("time", str(v.time))
364                 d.setAttribute("dist", str(v.dist))
365                 d.setAttribute("calories", str(v.calories))
366                 top_element.appendChild(d)
367
368             newdoc.appendChild(top_element)
369             newdoc.writexml(f)
370             #f.write(newdoc.toprettyxml())
371             f.close()
372         except Exception, e:
373             logger.error("Error while saving data to xml file: %s" % e)
374
375 class PedoRepositoryPickle(PedoRepository):
376     DIR = os.path.join(os.path.expanduser("~"), ".pedometer")
377     FILE = os.path.join(DIR, "pickle.log")
378
379     def __init__(self):
380         if not os.path.exists(self.DIR):
381             os.makedirs(self.DIR)
382         PedoRepository.__init__(self)
383
384     def load(self):
385         try:
386             f = open(self.FILE, "rb")
387             self.values = pickle.load(f)
388             f.close()
389         except Exception, e:
390             logger.error("Error while loading pickle file: %s" % e)
391
392     def save(self):
393         try:
394             f = open(self.FILE, "wb")
395             pickle.dump(self.values, f)
396             f.close()
397         except Exception, e:
398             logger.error("Error while saving data to pickle: %s" % e)
399
400 class PedoController(Singleton):
401     mode = 0
402     unit = 0
403     weight = 70
404     height_interval = 0
405     sensitivity = 100
406     #what to display in second view - 0 - alltime, 1 - today, 2 - week
407     second_view = 0
408     callback_update_ui = None
409     no_idle_time = False
410
411     STEP_LENGTH = 0.7
412     #values for the two views in the widget ( current and day/week/alltime)
413     v = [PedoValues(), PedoValues()]
414
415     last_time = 0
416     is_running = False
417
418     observers = []
419
420     midnight_set = False
421     midnight_source_id = None
422
423     def __init__(self):
424
425         self.pedometer = PedoCounter(self.steps_detected)
426         self.pedometerInterval = PedoIntervalCounter()
427         self.pedometerInterval.set_mode(self.mode)
428         self.repository = PedoRepositoryXML()
429         self.repository.load()
430
431         self.load_values()
432
433         if not self.midnight_set:
434             self.update_at_midnight()
435             self.midnight_set = True
436
437         self.config = Config()
438         self.config.add_observer(self.load_config)
439
440     def update_at_midnight(self):
441         next_day = date.today() + timedelta(days=1)
442         diff = time.mktime(next_day.timetuple()) - time.time()
443         diff = int(diff+5)
444         self.midnight_source_id = gobject.timeout_add_seconds(diff, self.midnight_callback, True)
445
446     def stop_midnight_callback(self):
447         if self.midnight_source_id is not None:
448             gobject.source_remove(self.midnight_source_id)
449
450     def midnight_callback(self, first=False):
451         self.load_values()
452         self.notify()
453         if first:
454             self.midnight_source_id = gobject.timeout_add_seconds(24*3600, self.midnight_callback)
455             return False
456         else:
457             return True
458
459     def load_config(self):
460         self.set_height(self.config.get_height(), self.config.get_step_length())
461         self.set_mode(self.config.get_mode())
462         self.set_unit(self.config.get_unit())
463         self.set_weight(self.config.get_weight())
464         self.set_second_view(self.config.get_secondview())
465         self.set_no_idle_time(self.config.get_noidletime())
466         self.set_sensitivity(self.config.get_sensitivity())
467
468     def load_values(self):
469         if self.second_view == 0:
470             self.v[1] = self.repository.get_alltime_values()
471         elif self.second_view == 1:
472             self.v[1] = self.repository.get_today_values()
473         else:
474             self.v[1] = self.repository.get_this_week_values()
475
476     def save_values(self):
477         self.repository.add_values(self.v[0])
478         self.repository.save()
479         self.load_values()
480
481     def start_pedometer(self):
482         self.v[0] = PedoValues()
483         self.last_time = time.time()
484         self.is_running = True
485         self.pedometer.start()
486         self.notify(True)
487
488     def reset_all_values(self):
489         self.repository.reset_values()
490         self.v[0] = PedoValues()
491         self.v[1] = PedoValues()
492         self.notify()
493
494     def stop_pedometer(self):
495         self.is_running = False
496         self.pedometer.request_stop()
497
498     def get_first(self):
499         return self.v[0]
500
501     def get_second(self):
502         if self.is_running:
503             return self.v[0] + self.v[1]
504         else:
505             return self.v[1]
506
507     def update_current(self):
508         """
509         Update distance and calories for current values based on new height, mode values
510         """
511         self.v[0].dist = self.get_distance(self.v[0].steps)
512         self.v[0].calories = self.get_calories(self.v[0].steps)
513
514     def steps_detected(self, cnt, last_steps=False):
515         if not last_steps and cnt == 0 and self.no_idle_time:
516             logger.info("No steps detected, timer is paused")
517         else:
518             self.v[0].steps += cnt
519             self.v[0].dist += self.get_distance(cnt)
520             self.v[0].calories += self.get_calories(self.get_distance(cnt))
521             self.v[0].time += time.time() - self.last_time
522             if last_steps:
523                 self.save_values()
524                 self.notify()
525             else:
526                 self.notify(True)
527         self.last_time = time.time()
528
529     def get_calories(self, distance):
530         """calculate lost calories for the distance and weight given as parameters
531         """
532         #different coefficient for running and walking
533         if self.mode == 0:
534             coef = 0.53
535         else:
536             coef = 0.75
537
538         #convert distance from meters to miles
539         distance *= 0.000621371192
540
541         weight = self.weight
542         #convert weight from kg to pounds
543         if self.unit == 0:
544             weight *= 2.20462262
545         return weight * distance * coef
546
547     def set_mode(self, mode):
548         self.mode = mode
549         self.set_height(self.height_interval)
550         self.pedometerInterval.set_mode(self.mode)
551         self.notify()
552
553     def set_unit(self, new_unit):
554         self.unit = new_unit
555         global unit
556         unit = new_unit
557         self.notify()
558
559     def get_str_weight_unit(self, unit=None):
560         if unit is None:
561             unit = self.unit
562         if unit == 0:
563             return "kg"
564         else:
565             return "lb"
566
567     def set_weight(self, value):
568         self.weight = value
569         self.notify()
570
571     def get_weight(self):
572         return self.weight
573
574     def set_sensitivity(self, value):
575         self.sensitivity = value
576         self.pedometerInterval.set_sensitivity(value)
577
578     def get_sensitivity(self):
579         return self.sensitivity
580
581     def set_second_view(self, second_view):
582         self.second_view = second_view
583         self.load_values()
584         self.notify()
585
586     def set_callback_ui(self, func):
587         self.callback_update_ui = func
588
589     def set_height(self, height_interval, step_length=None):
590         self.height_interval = height_interval
591
592         if step_length is None:
593             step_length = self.STEP_LENGTH
594         #set height, will affect the distance
595         if height_interval == 0:
596             self.STEP_LENGTH = 0.59
597         elif height_interval == 1:
598             self.STEP_LENGTH = 0.64
599         elif height_interval == 2:
600             self.STEP_LENGTH = 0.71
601         elif height_interval == 3:
602             self.STEP_LENGTH = 0.77
603         elif height_interval == 4:
604             self.STEP_LENGTH = 0.83
605         elif height_interval == 5:
606             self.STEP_LENGTH = step_length
607         #increase step length if RUNNING
608         if self.mode == 1:
609             self.STEP_LENGTH *= 1.45
610         self.notify()
611
612     def set_no_idle_time(self, value):
613         self.no_idle_time = value
614
615     def get_distance(self, steps=None):
616         if steps == None:
617             steps = self.counter
618         return self.STEP_LENGTH * steps;
619
620     def add_observer(self, func):
621         try:
622             self.observers.index(func)
623         except:
624             self.observers.append(func)
625
626     def remove_observer(self, func):
627         self.observers.remove(func)
628
629     def notify(self, optional=False):
630         if self.callback_update_ui is not None:
631             self.callback_update_ui()
632
633         for func in self.observers:
634             func(optional)
635
636 class AlarmController(Singleton):
637     enable = False
638     fname = "/home/user/MyDocs/.sounds/Ringtones/Bicycle.aac"
639     interval = 5
640     type = 0
641
642     player = None
643     is_playing = False
644     pedo_controller = None
645
646     def __init__(self):
647         self.client = gconf.client_get_default()
648         self.config = Config()
649         self.config.add_observer(self.load_config)
650
651         self.pedo_controller = PedoController()
652         if self.enable:
653             self.init_player()
654             self.pedo_controller.add_observer(self.update)
655             self.start_value = self.pedo_controller.get_first()
656
657     def init_player(self):
658         self.player = gst.element_factory_make("playbin2", "player")
659         fakesink = gst.element_factory_make("fakesink", "fakesink")
660         self.player.set_property("video-sink", fakesink)
661
662         bus = self.player.get_bus()
663         bus.add_signal_watch()
664         bus.connect("message", self.on_message)
665
666     def on_message(self, bus, message):
667         t = message.type
668         if t == gst.MESSAGE_EOS:
669             self.player.set_state(gst.STATE_NULL)
670             self.is_playing = False
671         elif t == gst.MESSAGE_ERROR:
672             self.player.set_state(gst.STATE_NULL)
673             self.is_playing = False
674             err, debug = message.parse_error()
675             logger.error("ERROR: %s, %s" % (err, debug) )
676
677     def update(self, optional):
678         diff = self.pedo_controller.get_first() - self.start_value
679         if self.type == 0 and diff.time >= self.interval * 60 or \
680                    self.type == 1 and diff.steps >= self.interval or \
681                    self.type == 2 and diff.dist >= self.interval or \
682                    self.type == 3 and diff.calories >= self.interval:
683             self.play()
684             #get new instance of current values
685             self.start_value = PedoValues() + self.pedo_controller.get_first()
686             logger.info("Alarm!")
687
688     def play(self):
689         if self.player is None:
690             self.init_player()
691         if self.is_playing:
692             self.player.set_state(gst.STATE_NULL)
693             self.is_playing = False
694         else:
695             self.player.set_property("uri", "file://" + self.fname)
696             self.player.set_state(gst.STATE_PLAYING)
697             self.is_playing = True
698
699     def stop(self):
700         if self.player is not None:
701             self.player.set_state(gst.STATE_NULL)
702
703     def load_config(self):
704         self.enable  = self.config.get_alarm_enable()
705         self.set_alarm_file(self.config.get_alarm_fname())
706         self.set_interval(self.config.get_alarm_interval())
707         self.set_type(self.config.get_alarm_type())
708
709     def set_enable(self, value):
710        self.enable = value
711        if self.enable:
712            self.init_player()
713            self.pedo_controller.add_observer(self.update)
714            self.start_value = self.pedo_controller.get_first()
715        else:
716            self.stop()
717            self.player = None
718            self.pedo_controller.remove_observer(self.update)
719
720     def get_enable(self):
721         return self.enable
722
723     def set_alarm_file(self, fname):
724         self.fname = fname
725
726     def get_alarm_file(self):
727         if self.fname == None:
728             return ""
729         return self.fname
730
731     def set_interval(self, interval):
732         self.interval = interval
733
734     def get_interval(self):
735         return self.interval
736
737     def set_type(self, type):
738         self.type = type
739
740     def get_type(self):
741         return self.type
742
743 class PedoCounter(Singleton):
744     COORD_FNAME = "/sys/class/i2c-adapter/i2c-3/3-001d/coord"
745     COORD_FNAME_SDK = "/home/andrei/pedometer-widget-0.1/date.txt"
746     LOGFILE = "/home/user/log_pedometer"
747     #time in ms between two accelerometer data reads
748     COORD_GET_INTERVAL = 25
749
750     COUNT_INTERVAL = 5
751
752     interval_counter = None
753     stop_requested = False
754     update_function = None
755     logging = False
756     isRunning = False
757
758     def __init__(self, update_function=None):
759         if not os.path.exists(self.COORD_FNAME):
760             self.COORD_FNAME = self.COORD_FNAME_SDK
761
762         self.interval_counter = PedoIntervalCounter()
763         self.update_function = update_function
764
765     def set_logging(self, value):
766         self.logging = value
767
768     def get_rotation(self):
769         f = open(self.COORD_FNAME, 'r')
770         coords = [int(w) for w in f.readline().split()]
771         f.close()
772         return coords
773
774     def start(self):
775         logger.info("Counter started")
776         self.isRunning = True
777         self.stop_requested = False
778         if self.logging:
779             fname = "%d_%d_%d_%d_%d_%d" % time.localtime()[0:6]
780             self.file = open(self.LOGFILE + fname + ".txt", "w")
781         gobject.idle_add(self.run)
782
783     def run(self):
784         self.coords = [[], [], []]
785         self.stime = time.time()
786         self.t = []
787         gobject.timeout_add(self.COORD_GET_INTERVAL, self.read_coords)
788         return False
789
790     def read_coords(self):
791         x, y, z = self.get_rotation()
792         self.coords[0].append(int(x))
793         self.coords[1].append(int(y))
794         self.coords[2].append(int(z))
795         now = time.time() - self.stime
796         if self.logging:
797             self.file.write("%d %d %d %f\n" % (self.coords[0][-1], self.coords[1][-1], self.coords[2][-1], now))
798
799         self.t.append(now)
800         #call stop_interval
801         ret = True
802         if self.t[-1] > self.COUNT_INTERVAL or self.stop_requested:
803             ret = False
804             gobject.idle_add(self.stop_interval)
805         return ret
806
807     def stop_interval(self):
808         self.interval_counter.set_vals(self.coords, self.t)
809         cnt = self.interval_counter.number_steps()
810
811         logger.info("Number of steps detected for last interval %d, number of coords: %d" % (cnt, len(self.t)))
812
813         gobject.idle_add(self.update_function, cnt, self.stop_requested)
814
815         if self.stop_requested:
816             gobject.idle_add(self.stop)
817         else:
818             gobject.idle_add(self.run)
819         return False
820
821     def stop(self):
822         if self.logging:
823             self.file.close()
824         logger.info("Counter has finished")
825
826     def request_stop(self):
827         self.stop_requested = True
828         self.isRunning = False
829
830 class CustomButton(hildon.Button):
831     def __init__(self, icon):
832         hildon.Button.__init__(self, gtk.HILDON_SIZE_AUTO_WIDTH, hildon.BUTTON_ARRANGEMENT_VERTICAL)
833         self.icon = icon
834         self.set_size_request(int(32 * 1.4), int(30 * 1.0))
835         self.retval = self.connect("expose_event", self.expose)
836
837     def set_icon(self, icon):
838         self.icon = icon
839
840     def expose(self, widget, event):
841         self.context = widget.window.cairo_create()
842         self.context.rectangle(event.area.x, event.area.y,
843                             event.area.width, event.area.height)
844
845         self.context.clip()
846         rect = self.get_allocation()
847         self.context.rectangle(rect.x, rect.y, rect.width, rect.height)
848         self.context.set_source_rgba(1, 1, 1, 0)
849
850         style = self.rc_get_style()
851         color = style.lookup_color("DefaultBackgroundColor")
852         if self.state == gtk.STATE_ACTIVE:
853             style = self.rc_get_style()
854             color = style.lookup_color("SelectionColor")
855             self.context.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75);
856         self.context.fill()
857
858         #img = cairo.ImageSurface.create_from_png(self.icon)
859
860         #self.context.set_source_surface(img)
861         #self.context.set_source_surface(img, rect.width/2 - img.get_width() /2, 0)
862         img = gtk.Image()
863         img.set_from_file(self.icon)
864         buf = img.get_pixbuf()
865         buf = buf.scale_simple(int(32 * 1.5), int(30 * 1.5), gtk.gdk.INTERP_BILINEAR)
866
867         self.context.set_source_pixbuf(buf, rect.x + (event.area.width / 2 - 15) - 8, rect.y + 1)
868         self.context.scale(200, 200)
869         self.context.paint()
870
871         return self.retval
872
873 class CustomEventBox(gtk.EventBox):
874
875     def __init__(self):
876         gtk.EventBox.__init__(self)
877
878     def do_expose_event(self, event):
879         self.context = self.window.cairo_create()
880         self.context.rectangle(event.area.x, event.area.y,
881                             event.area.width, event.area.height)
882
883         self.context.clip()
884         rect = self.get_allocation()
885         self.context.rectangle(rect.x, rect.y, rect.width, rect.height)
886
887         if self.state == gtk.STATE_ACTIVE:
888             style = self.rc_get_style()
889             color = style.lookup_color("SelectionColor")
890             self.context.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75);
891         else:
892             self.context.set_source_rgba(1, 1, 1, 0)
893         self.context.fill()
894
895         gtk.EventBox.do_expose_event(self, event)
896
897 class GraphController(Singleton):
898     ytitles = ["Steps", "Average Speed", "Distance", "Calories"]
899     xtitles = ["Day", "Week"] # "Today"]
900     widget = None
901
902     config = None
903
904     def __init__(self):
905         self.repository = PedoRepositoryXML()
906         self.last_update = 0
907         PedoController().add_observer(self.update_ui)
908         self.config = Config()
909         self.config.add_observer(self.load_config)
910
911     def load_config(self):
912         self.set_current_view(self.config.get_graphview())
913
914     def set_graph(self, widget):
915         self.widget = widget
916         self.update_ui()
917
918     def set_current_view(self, view):
919         """
920         current_view % len(ytitles) - gives the ytitle
921         current_view / len(ytitles) - gives the xtitle
922         """
923         self.x_id = view / len(self.ytitles)
924         self.y_id = view % len(self.ytitles)
925         self.update_ui()
926
927     def next_view(self):
928         current_view = self.config.get_graphview() + 1
929         if current_view == len(self.ytitles) * len(self.xtitles):
930             current_view = 0
931         self.config.set_graphview(current_view)
932
933     def last_weeks_labels(self):
934         d = date.today()
935         delta = timedelta(days=7)
936         ret = []
937         for i in range(7):
938             ret.append(d.strftime("Week %W"))
939             d = d - delta
940         return ret
941
942     def compute_values(self):
943         labels = []
944         if self.x_id == 0:
945             values = self.repository.get_last_7_days()
946             d = date.today()
947             delta = timedelta(days=1)
948             for i in range(7):
949                 labels.append(d.ctime().split()[0])
950                 d = d - delta
951
952         elif self.x_id == 1:
953             values = self.repository.get_last_weeks()
954             d = date.today()
955             for i in range(7):
956                 labels.append(d.strftime("Week %W"))
957                 d = d - timedelta(days=7)
958         else:
959             values = self.repository.get_today()
960             #TODO get labels
961
962         if self.y_id == 0:
963             yvalues = [line.steps for line in values]
964         elif self.y_id == 1:
965             yvalues = [line.get_avg_speed() for line in values]
966         elif self.y_id == 2:
967             yvalues = [line.dist for line in values]
968         else:
969             yvalues = [line.calories for line in values]
970
971         #determine values for y lines in graph
972         diff = self.get_best_interval_value(max(yvalues))
973         ytext = []
974         for i in range(6):
975             ytext.append(str(int(i*diff)))
976
977         if self.widget is not None:
978             yvalues.reverse()
979             labels.reverse()
980             self.widget.values = yvalues
981             self.widget.ytext = ytext
982             self.widget.xtext = labels
983             self.widget.max_value = diff * 5
984             self.widget.text = self.xtitles[self.x_id] + " / " + self.ytitles[self.y_id]
985             self.widget.queue_draw()
986         else:
987             logger.error("Widget not set in GraphController")
988
989     def get_best_interval_value(self, max_value):
990         diff =  1.0 * max_value / 5
991         l = len(str(int(diff)))
992         d = math.pow(10, l/2)
993         val = int(math.ceil(1.0 * diff / d)) * d
994         if val == 0:
995             val = 1
996         return val
997
998     def update_ui(self, optional=False):
999         """update graph values every x seconds"""
1000         if optional and self.last_update - time.time() < 600:
1001             return
1002         if self.widget is None:
1003             return
1004
1005         self.compute_values()
1006         self.last_update = time.time()
1007
1008 class GraphWidget(gtk.DrawingArea):
1009
1010     def __init__(self):
1011         gtk.DrawingArea.__init__(self)
1012         self.set_size_request(-1, 150)
1013         self.yvalues = 5
1014
1015         """sample values"""
1016         self.ytext = ["   0", "1000", "2000", "3000", "4000", "5000"]
1017         self.xtext = ["Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday", "Sunday"]
1018         self.values = [1500, 3400, 4000, 3600, 3200, 0, 4500]
1019         self.max_value = 5000
1020         self.text = "All time steps"
1021
1022     def do_expose_event(self, event):
1023         context = self.window.cairo_create()
1024
1025         # set a clip region for the expose event
1026         context.rectangle(event.area.x, event.area.y,
1027                                event.area.width, event.area.height)
1028         context.clip()
1029
1030         context.save()
1031
1032         context.set_operator(cairo.OPERATOR_SOURCE)
1033         style = self.rc_get_style()
1034
1035         if self.state == gtk.STATE_ACTIVE:
1036             color = style.lookup_color("SelectionColor")
1037         else:
1038              color = style.lookup_color("DefaultBackgroundColor")
1039         context.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75)
1040
1041         context.paint()
1042         context.restore();
1043         self.draw(context)
1044
1045     def draw(self, cr):
1046         space_below = 20
1047         space_above = 10
1048         border_right = 10
1049         border_left = 30
1050
1051         rect = self.get_allocation()
1052         x = rect.width
1053         y = rect.height
1054
1055         cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
1056             cairo.FONT_WEIGHT_NORMAL)
1057         cr.set_font_size(13)
1058
1059         #check space needed to display ylabels
1060         te = cr.text_extents(self.ytext[-1])
1061         border_left = te[2] + 7
1062
1063         cr.set_source_rgb(1, 1, 1)
1064         cr.move_to(border_left, space_above)
1065         cr.line_to(border_left, y-space_below)
1066         cr.set_line_width(2)
1067         cr.stroke()
1068
1069         cr.move_to(border_left, y-space_below)
1070         cr.line_to(x-border_right, y-space_below)
1071         cr.set_line_width(2)
1072         cr.stroke()
1073
1074         ydiff = (y-space_above-space_below) / self.yvalues
1075         for i in range(self.yvalues):
1076             yy = y-space_below-ydiff*(i+1)
1077             cr.move_to(border_left, yy)
1078             cr.line_to(x-border_right, yy)
1079             cr.set_line_width(0.8)
1080             cr.stroke()
1081
1082
1083         for i in range(6):
1084             yy = y - space_below - ydiff*i + 5
1085             te = cr.text_extents(self.ytext[i])
1086
1087             cr.move_to(border_left-te[2]-2, yy)
1088             cr.show_text(self.ytext[i])
1089
1090         cr.set_font_size(15)
1091         te = cr.text_extents(self.text)
1092         cr.move_to((x-te[2])/2, y-5)
1093         cr.show_text(self.text)
1094
1095         graph_x_space = x - border_left - border_right
1096         graph_y_space = y - space_below - space_above
1097         bar_width = graph_x_space*0.75 / len(self.values)
1098         bar_distance = graph_x_space*0.25 / (1+len(self.values))
1099
1100         #set dummy max value to avoid exceptions
1101         if self.max_value == 0:
1102             self.max_value = 100
1103         for i in range(len(self.values)):
1104             xx = border_left + (i+1)*bar_distance + i * bar_width
1105             yy = y-space_below
1106             height = graph_y_space * (1.0 * self.values[i] / self.max_value)
1107             cr.set_source_rgba(1, 1, 1, 0.75)
1108             cr.rectangle(int(xx), int(yy-height), int(bar_width), int(height))
1109             cr.fill()
1110
1111         cr.set_source_rgba(1, 1, 1, 1)
1112         cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
1113                             cairo.FONT_WEIGHT_NORMAL)
1114         cr.set_font_size(13)
1115
1116         cr.rotate(2*math.pi * (-45) / 180)
1117         for i in range(len(self.values)):
1118             xx = y - space_below - 10
1119             yy = border_left + (i+1)*bar_distance + i * bar_width
1120             cr.move_to(-xx, yy + bar_width*1.25 / 2)
1121             cr.show_text(self.xtext[i])
1122
1123 class Config(Singleton):
1124     mode = 0
1125     height = 0
1126     step_length = 0.7
1127     weight = 70
1128     sensitivity = 100
1129     unit = 0
1130     aspect = 0
1131     sensitivity = 100
1132     second_view = 0
1133     graph_view = 0
1134     no_idle_time = False
1135     logging = False
1136
1137     alarm_enable = False
1138     alarm_fname = "/home/user/MyDocs/.sounds/Ringtones/Bicycle.aac"
1139     alarm_interval = 5
1140     alarm_type = 0
1141
1142     observers = []
1143
1144     def __init__(self):
1145         if self._references > 1:
1146             return
1147         self.client = gconf.client_get_default()
1148         self.client.add_dir('/apps/pedometerhomewidget', gconf.CLIENT_PRELOAD_NONE)
1149         self.notify_id = self.client.notify_add('/apps/pedometerhomewidget', self.gconf_changed)
1150
1151     def add_observer(self, func):
1152         try:
1153             self.observers.index(func)
1154         except:
1155             self.observers.append(func)
1156             func()
1157
1158     def remove_observer(self, func):
1159         self.observers.remove(func)
1160
1161     def gconf_changed(self, client, *args, **kargs):
1162         self.notify()
1163
1164     def notify(self):
1165         for func in self.observers:
1166             func()
1167
1168     def get_mode(self):
1169         return self.client.get_int(MODE)
1170
1171     def set_mode(self, value):
1172         self.client.set_int(MODE, value)
1173
1174     def get_height(self):
1175         return self.client.get_int(HEIGHT)
1176
1177     def set_height(self, value):
1178         self.client.set_int(HEIGHT, value)
1179
1180     def get_step_length(self):
1181         return self.client.get_float(STEP_LENGTH)
1182
1183     def set_step_length(self, value):
1184         self.client.set_float(STEP_LENGTH, value)
1185
1186     def get_weight(self):
1187         return self.client.get_int(WEIGHT)
1188
1189     def set_weight(self, value):
1190         self.client.set_int(WEIGHT, value)
1191
1192     def get_sensitivity(self):
1193         return self.client.get_int(SENSITIVITY)
1194
1195     def set_sensitivity(self, value):
1196         self.client.set_int(SENSITIVITY, value)
1197
1198     def get_unit(self):
1199         return self.client.get_int(UNIT)
1200
1201     def set_unit(self, value):
1202         self.client.set_int(UNIT, value)
1203
1204     def get_aspect(self):
1205         return self.client.get_int(ASPECT)
1206
1207     def set_aspect(self, value):
1208         self.client.set_int(ASPECT, value)
1209
1210     def get_secondview(self):
1211         value = self.client.get_int(SECONDVIEW)
1212         if value < 0 or value > 2:
1213             value = 0
1214             logger.error("Invalid secondview value read from Gconf. Using default value")
1215
1216         return value
1217
1218     def set_secondview(self, value):
1219         self.client.set_int(SECONDVIEW, value)
1220
1221     def get_graphview(self):
1222         return self.client.get_int(GRAPHVIEW)
1223
1224     def set_graphview(self, value):
1225         self.client.set_int(GRAPHVIEW, value)
1226
1227     def get_noidletime(self):
1228         return self.client.get_bool(NOIDLETIME)
1229
1230     def set_noidletime(self, value):
1231         self.client.set_bool(NOIDLETIME, value)
1232
1233     def get_logging(self):
1234         return self.client.get_bool(LOGGING)
1235
1236     def set_logging(self, value):
1237         self.client.set_bool(LOGGING, value)
1238
1239     def get_alarm_enable(self):
1240         return self.client.get_bool(ALARM_ENABLE)
1241
1242     def set_alarm_enable(self, value):
1243         self.client.set_bool(ALARM_ENABLE, value)
1244
1245     def get_alarm_fname(self):
1246         return self.client.get_string(ALARM_FNAME)
1247
1248     def set_alarm_fname(self, value):
1249         self.client.set_string(ALARM_FNAME, value)
1250
1251     def get_alarm_interval(self):
1252         return self.client.get_int(ALARM_INTERVAL)
1253
1254     def set_alarrm_interval(self, value):
1255         self.client.set_int(ALARM_INTERVAL, value)
1256
1257     def get_alarm_type(self):
1258         return self.client.get_int(ALARM_TYPE)
1259
1260     def set_alarm_type(self, value):
1261         self.client.set_int(ALARM_TYPE, value)
1262
1263 class PedometerHomePlugin(hildondesktop.HomePluginItem):
1264     button = None
1265
1266     #labels to display
1267     labels = ["timer", "count", "dist", "avgSpeed", "calories"]
1268
1269     #current view
1270     labelsC = {}
1271
1272     #second view ( day / week/ alltime)
1273     labelsT = {}
1274
1275     second_view_labels = ["All-time", "Today", "This week"]
1276
1277     controller = None
1278     graph_controller = None
1279
1280     config = None
1281
1282     def __init__(self):
1283         hildondesktop.HomePluginItem.__init__(self)
1284
1285         gobject.type_register(CustomEventBox)
1286         gobject.type_register(GraphWidget)
1287
1288         self.config = Config()
1289
1290         self.button = CustomButton(ICONSPATH + "play.png")
1291         self.button.connect("clicked", self.button_clicked)
1292
1293         self.create_labels(self.labelsC)
1294         self.create_labels(self.labelsT)
1295         self.label_second_view = self.new_label_heading(self.second_view_labels[self.config.get_secondview()])
1296
1297         self.controller = PedoController()
1298         self.controller.set_callback_ui(self.update_values)
1299
1300         self.graph_controller = GraphController()
1301         self.alarm_controller = AlarmController()
1302
1303         self.update_current()
1304         self.update_total()
1305
1306         mainHBox = gtk.HBox(spacing=1)
1307
1308         descVBox = gtk.VBox(spacing=1)
1309         descVBox.add(self.new_label_heading())
1310         descVBox.add(self.new_label_heading("Time:"))
1311         descVBox.add(self.new_label_heading("Steps:"))
1312         descVBox.add(self.new_label_heading("Calories:"))
1313         descVBox.add(self.new_label_heading("Distance:"))
1314         descVBox.add(self.new_label_heading("Avg Speed:"))
1315
1316         currentVBox = gtk.VBox(spacing=1)
1317         currentVBox.add(self.new_label_heading("Current"))
1318         currentVBox.add(self.labelsC["timer"])
1319         currentVBox.add(self.labelsC["count"])
1320         currentVBox.add(self.labelsC["calories"])
1321         currentVBox.add(self.labelsC["dist"])
1322         currentVBox.add(self.labelsC["avgSpeed"])
1323         self.currentBox = currentVBox
1324
1325         totalVBox = gtk.VBox(spacing=1)
1326         totalVBox.add(self.label_second_view)
1327         totalVBox.add(self.labelsT["timer"])
1328         totalVBox.add(self.labelsT["count"])
1329         totalVBox.add(self.labelsT["calories"])
1330         totalVBox.add(self.labelsT["dist"])
1331         totalVBox.add(self.labelsT["avgSpeed"])
1332         self.totalBox = totalVBox
1333
1334         buttonVBox = gtk.VBox(spacing=1)
1335         buttonVBox.add(self.new_label_heading(""))
1336         buttonVBox.add(self.button)
1337         buttonVBox.add(self.new_label_heading(""))
1338
1339         eventBox = CustomEventBox()
1340         eventBox.set_visible_window(False)
1341         eventBox.add(totalVBox)
1342         eventBox.connect("button-press-event", self.eventBox_clicked)
1343         eventBox.connect("button-release-event", self.eventBox_clicked_release)
1344
1345         mainHBox.add(buttonVBox)
1346         mainHBox.add(descVBox)
1347         mainHBox.add(currentVBox)
1348         mainHBox.add(eventBox)
1349         self.mainhbox = mainHBox
1350
1351         graph = GraphWidget()
1352         self.graph_controller.set_graph(graph)
1353
1354         eventBoxGraph = CustomEventBox()
1355         eventBoxGraph.set_visible_window(False)
1356         eventBoxGraph.add(graph)
1357         self.graph = graph
1358         eventBoxGraph.connect("button-press-event", self.eventBoxGraph_clicked)
1359         eventBoxGraph.connect("button-release-event", self.eventBoxGraph_clicked_release)
1360         self.graphBox = eventBoxGraph
1361
1362         self.mainvbox = gtk.VBox()
1363
1364         self.mainvbox.add(mainHBox)
1365         self.mainvbox.add(eventBoxGraph)
1366
1367         self.mainvbox.show_all()
1368         self.add(self.mainvbox)
1369         self.update_aspect()
1370
1371         self.connect("unrealize", self.close_requested)
1372         self.set_settings(True)
1373         self.connect("show-settings", self.show_settings)
1374
1375     def eventBoxGraph_clicked(self, widget, data=None):
1376         widget.set_state(gtk.STATE_ACTIVE)
1377
1378     def eventBoxGraph_clicked_release(self, widget, data=None):
1379         self.graph_controller.next_view()
1380         widget.set_state(gtk.STATE_NORMAL)
1381
1382     def eventBox_clicked(self, widget, data=None):
1383         widget.set_state(gtk.STATE_ACTIVE)
1384
1385     def eventBox_clicked_release(self, widget, data=None):
1386         widget.set_state(gtk.STATE_NORMAL)
1387
1388         second_view = self.config.get_secondview()
1389         second_view = (second_view + 1) % 3
1390         self.config.set_secondview(second_view)
1391
1392     def new_label_heading(self, title=""):
1393         l = gtk.Label(title)
1394         hildon.hildon_helper_set_logical_font(l, "SmallSystemFont")
1395         return l
1396
1397     def create_labels(self, new_labels):
1398         for label in self.labels:
1399             l = gtk.Label()
1400             hildon.hildon_helper_set_logical_font(l, "SmallSystemFont")
1401             hildon.hildon_helper_set_logical_color(l, gtk.RC_FG, gtk.STATE_NORMAL, "ActiveTextColor")
1402             new_labels[label] = l
1403
1404     def update_aspect(self):
1405         aspect = self.config.get_aspect()
1406         if aspect > 0:
1407             self.graphBox.hide_all()
1408         else:
1409             self.graphBox.show_all()
1410
1411         if aspect == 0 or aspect == 1:
1412             self.currentBox.show_all()
1413             self.totalBox.show_all()
1414         elif aspect == 2:
1415             self.currentBox.show_all()
1416             self.totalBox.hide_all()
1417         else:
1418             self.currentBox.hide_all()
1419             self.totalBox.show_all()
1420
1421         x,y = self.size_request()
1422         self.resize(x,y)
1423
1424     def update_ui_values(self, labels, values):
1425         labels["timer"].set_label(values.get_print_time())
1426         labels["count"].set_label(values.get_print_steps())
1427         labels["dist"].set_label(values.get_print_distance())
1428         labels["avgSpeed"].set_label(values.get_print_avg_speed())
1429         labels["calories"].set_label(values.get_print_calories())
1430
1431     def update_current(self):
1432         self.update_ui_values(self.labelsC, self.controller.get_first())
1433
1434     def update_total(self):
1435         self.update_ui_values(self.labelsT, self.controller.get_second())
1436
1437     def show_alarm_settings(self, main_button):
1438         def choose_file(widget):
1439             file = hildon.FileChooserDialog(self, gtk.FILE_CHOOSER_ACTION_OPEN, hildon.FileSystemModel() )
1440             file.show()
1441             if ( file.run() == gtk.RESPONSE_OK):
1442                 fname = file.get_filename()
1443                 widget.set_value(fname)
1444                 self.config.set_alarm_fname(fname)
1445             file.destroy()
1446
1447         def test_sound(button):
1448             try:
1449                 self.alarm_controller.play()
1450             except Exception, e:
1451                 logger.error("Could not play alarm sound: %s" % e)
1452                 hildon.hildon_banner_show_information(self, "None", "Could not play alarm sound")
1453
1454         def enableButton_changed(button):
1455             value = button.get_active()
1456             self.config.set_alarm_enable(value)
1457             if value:
1458                 main_button.set_value("Enabled")
1459             else:
1460                 main_button.set_value("Disabled")
1461
1462         def selectorType_changed(selector, data, labelEntry2):
1463             type = selector.get_active(0)
1464             self.config.set_alarm_type(type)
1465             labelEntry2.set_label(suffix[type])
1466
1467         dialog = gtk.Dialog()
1468         dialog.set_title("Alarm settings")
1469         dialog.add_button("OK", gtk.RESPONSE_OK)
1470
1471         enableButton = hildon.CheckButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
1472         enableButton.set_label("Enable alarm")
1473         enableButton.set_active(self.alarm_controller.get_enable())
1474         enableButton.connect("toggled", enableButton_changed)
1475
1476         testButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1477         testButton.set_alignment(0, 0.8, 1, 1)
1478         testButton.set_title("Test sound")
1479         testButton.connect("pressed", test_sound)
1480
1481         fileButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1482         fileButton.set_alignment(0, 0.8, 1, 1)
1483         fileButton.set_title("Alarm sound")
1484         fileButton.set_value(self.alarm_controller.get_alarm_file())
1485         fileButton.connect("pressed", choose_file)
1486
1487         labelEntry = gtk.Label("Notify every:")
1488         suffix = ["mins", "steps", "m/ft", "calories"]
1489         labelEntry2 = gtk.Label(suffix[self.alarm_controller.get_type()])
1490         intervalEntry = hildon.Entry(gtk.HILDON_SIZE_AUTO_WIDTH)
1491         intervalEntry.set_text(str(self.alarm_controller.get_interval()))
1492
1493         selectorType = hildon.TouchSelector(text=True)
1494         selectorType.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1495         selectorType.append_text("Time")
1496         selectorType.append_text("Steps")
1497         selectorType.append_text("Distance")
1498         selectorType.append_text("Calories")
1499         selectorType.connect("changed", selectorType_changed, labelEntry2)
1500
1501         typePicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1502         typePicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1503         typePicker.set_title("Alarm type")
1504         typePicker.set_selector(selectorType)
1505         typePicker.set_active(self.alarm_controller.get_type())
1506
1507         hbox = gtk.HBox()
1508         hbox.add(labelEntry)
1509         hbox.add(intervalEntry)
1510         hbox.add(labelEntry2)
1511
1512         dialog.vbox.add(enableButton)
1513         dialog.vbox.add(fileButton)
1514         dialog.vbox.add(testButton)
1515         dialog.vbox.add(typePicker)
1516         dialog.vbox.add(hbox)
1517         dialog.show_all()
1518         while 1:
1519             response = dialog.run()
1520             if response != gtk.RESPONSE_OK:
1521                 break
1522             try:
1523                 value = int(intervalEntry.get_text())
1524                 self.config.set_alarrm_interval(value)
1525                 break
1526             except:
1527                 hildon.hildon_banner_show_information(self, "None", "Invalid interval")
1528
1529         dialog.destroy()
1530
1531     def show_settings(self, widget):
1532         def reset_total_counter(arg):
1533             note = hildon.hildon_note_new_confirmation(self.dialog, "Are you sure you want to delete all your pedometer history?")
1534             ret = note.run()
1535             if ret == gtk.RESPONSE_OK:
1536                 self.controller.reset_all_values()
1537                 hildon.hildon_banner_show_information(self, "None", "All history was deleted")
1538             note.destroy()
1539
1540         def alarmButton_pressed(widget):
1541             self.show_alarm_settings(widget)
1542
1543         def selector_changed(selector, data):
1544             mode = selector.get_active(0)
1545             self.config.set_mode(mode)
1546
1547         def selectorUnit_changed(selector, data):
1548             unit = selector.get_active(0)
1549             self.config.set_unit(unit)
1550
1551             update_weight_button()
1552             stepLengthButton_value_update()
1553
1554         def selectorUI_changed(selector, data):
1555             aspect = selectorUI.get_active(0)
1556             widget.update_aspect()
1557
1558         def logButton_changed(checkButton):
1559             logging = checkButton.get_active()
1560             self.config.set_logging(logging)
1561
1562         def idleButton_changed(idleButton):
1563             no_idle_time = idleButton.get_active()
1564             self.config.set_noidletime(no_idle_time)
1565
1566         def update_weight_button():
1567             weightButton.set_value(str(self.config.get_weight()) + \
1568                                            " " + self.controller.get_str_weight_unit(self.config.get_unit()) )
1569
1570         def weight_dialog(button):
1571             dialog = gtk.Dialog("Weight", self.dialog)
1572             dialog.add_button("OK", gtk.RESPONSE_OK)
1573
1574             label = gtk.Label("Weight:")
1575             entry = gtk.Entry()
1576             entry.set_text(str(self.config.get_weight()))
1577
1578             suffixLabel = gtk.Label(self.controller.get_str_weight_unit(self.config.get_unit()))
1579
1580             hbox = gtk.HBox()
1581             hbox.add(label)
1582             hbox.add(entry)
1583             hbox.add(suffixLabel)
1584
1585             dialog.vbox.add(hbox)
1586             dialog.show_all()
1587             while 1:
1588                 response = dialog.run()
1589                 if response != gtk.RESPONSE_OK:
1590                     break
1591                 try:
1592                     value = int(entry.get_text())
1593                     if value <= 0:
1594                         raise ValueError
1595                     self.config.set_weight(value)
1596                     update_weight_button()
1597                     break
1598                 except:
1599                     hildon.hildon_banner_show_information(self, "None", "Invalid weight")
1600             dialog.destroy()
1601
1602         def sensitivity_dialog(button):
1603             def seekbar_changed(seekbar):
1604                 label.set_text(str(seekbar.get_position()) + " %")
1605
1606             dialog = gtk.Dialog("Sensitivity", self.dialog)
1607             dialog.add_button("OK", gtk.RESPONSE_OK)
1608             seekbar = hildon.Seekbar()
1609             seekbar.set_size_request(400, -1)
1610             seekbar.set_total_time(200)
1611             seekbar.set_position(self.config.get_sensitivity())
1612             seekbar.connect("value-changed", seekbar_changed)
1613
1614             hbox = gtk.HBox()
1615             hbox.add(seekbar)
1616             label = gtk.Label(str(self.config.get_sensitivity()) + " %")
1617             label.set_size_request(30, -1)
1618             hbox.add(label)
1619
1620             dialog.vbox.add(hbox)
1621             dialog.show_all()
1622
1623             if dialog.run() == gtk.RESPONSE_OK:
1624                 value = seekbar.get_position()
1625                 self.config.set_sensitivity(value)
1626                 button.set_value(str(value) + " %")
1627
1628             dialog.destroy()
1629
1630         def stepLengthButton_value_update():
1631             if self.config.get_height() == 5:
1632                 l_unit = ["m", "ft"]
1633                 stepLengthButton.set_value("Custom value: %.2f %s" % (self.config.get_step_length(), l_unit[self.config.get_unit()]))
1634             else:
1635                 h = [ ["< 1.50 m", "1.50 - 1.65 m", "1.66 - 1.80 m", "1.81 - 1.95 m", " > 1.95 m"],
1636                       ["< 5 ft", "5 - 5.5 ft", "5.5 - 6 ft", "6 - 6.5 ft", "> 6.5 ft"]]
1637                 str = "Using predefined value for height: %s" % h[self.config.get_unit()][self.config.get_height()]
1638                 stepLengthButton.set_value(str)
1639
1640         def stepLength_dialog(button):
1641             def selectorH_changed(selector, data, dialog):
1642                 height = selector.get_active(0)
1643                 self.config.set_height(height)
1644                 stepLengthButton_value_update()
1645
1646             def manualButton_clicked(button, dialog):
1647                 dlg = gtk.Dialog()
1648                 dlg.set_title("Custom step length")
1649                 dlg.add_button("OK", gtk.RESPONSE_OK)
1650
1651                 label = gtk.Label("Length")
1652
1653                 entry = hildon.Entry(gtk.HILDON_SIZE_AUTO_WIDTH)
1654                 if self.config.get_height() == 5:
1655                     entry.set_text(str(self.config.get_step_length()))
1656
1657                 labelSuffix = gtk.Label()
1658                 if self.config.get_unit() == 0:
1659                     labelSuffix.set_label("m")
1660                 else:
1661                     labelSuffix.set_label("ft")
1662                 hbox = gtk.HBox()
1663                 hbox.add(label)
1664                 hbox.add(entry)
1665                 hbox.add(labelSuffix)
1666                 dlg.vbox.add(hbox)
1667                 dlg.show_all()
1668
1669                 while 1:
1670                     response = dlg.run()
1671                     if response != gtk.RESPONSE_OK:
1672                         break
1673                     try:
1674                         value = float(entry.get_text())
1675                         if value <= 0:
1676                             raise ValueError
1677                         self.config.set_step_length(value)
1678                         self.config.set_height(5)
1679                         stepLengthButton_value_update()
1680                         break
1681                     except ValueError:
1682                         hildon.hildon_banner_show_information(self, "None", "Invalid length")
1683                 dlg.destroy()
1684                 dialog.destroy()
1685
1686             def heightButton_clicked(button, dialog):
1687                 dialog.destroy()
1688
1689             dialog = gtk.Dialog()
1690             dialog.set_title("Step length")
1691
1692             manualButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1693             manualButton.set_title("Enter custom value")
1694             manualButton.set_alignment(0, 0.8, 1, 1)
1695             manualButton.connect("clicked", manualButton_clicked, dialog)
1696
1697             selectorH = hildon.TouchSelector(text=True)
1698             selectorH.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1699             selectorH.append_text("< 1.50 m")
1700             selectorH.append_text("1.50 - 1.65 m")
1701             selectorH.append_text("1.66 - 1.80 m")
1702             selectorH.append_text("1.81 - 1.95 m")
1703             selectorH.append_text(" > 1.95 m")
1704
1705             selectorH_English = hildon.TouchSelector(text=True)
1706             selectorH_English.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1707             selectorH_English.append_text("< 5 ft")
1708             selectorH_English.append_text("5 - 5.5 ft")
1709             selectorH_English.append_text("5.5 - 6 ft")
1710             selectorH_English.append_text("6 - 6.5 ft")
1711             selectorH_English.append_text("> 6.5 ft")
1712
1713             heightPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1714             heightPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1715             heightPicker.set_title("Use predefined values for height")
1716
1717
1718             unit = self.config.get_unit()
1719             if unit == 0:
1720                 heightPicker.set_selector(selectorH)
1721             else:
1722                 heightPicker.set_selector(selectorH_English)
1723
1724             height = self.config.get_height()
1725             if height < 5:
1726                 heightPicker.set_active(height)
1727
1728             heightPicker.get_selector().connect("changed", selectorH_changed, dialog)
1729             heightPicker.connect("value-changed", heightButton_clicked, dialog)
1730
1731             dialog.vbox.add(heightPicker)
1732             dialog.vbox.add(manualButton)
1733             dialog.show_all()
1734
1735             if  dialog.run() == gtk.RESPONSE_DELETE_EVENT:
1736                 dialog.destroy()
1737
1738         def donateButton_clicked(button, dialog):
1739             url = "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=BKE6E9SLK7NP4&lc=RO&item_name=Pedometer%20Widget&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"
1740             command = "dbus-send --system --type=method_call --dest=\"com.nokia.osso_browser\"  --print-reply /com/nokia/osso_browser/request com.nokia.osso_browser.load_url string:\"%s\"" % url
1741             os.system(command)
1742
1743         dialog = gtk.Dialog()
1744         dialog.set_title("Settings")
1745         dialog.add_button("OK", gtk.RESPONSE_OK)
1746         self.dialog = dialog
1747
1748         stepLengthButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1749         stepLengthButton.set_title("Step length")
1750         stepLengthButton.set_alignment(0, 0.8, 1, 1)
1751         stepLengthButton.connect("clicked", stepLength_dialog)
1752         stepLengthButton_value_update()
1753
1754         resetButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1755         resetButton.set_title("Reset total counter")
1756         resetButton.set_alignment(0, 0.8, 1, 1)
1757         resetButton.connect("clicked", reset_total_counter)
1758
1759         alarmButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1760         alarmButton.set_title("Alarm")
1761         if self.config.get_alarm_enable():
1762             alarmButton.set_value("Enabled")
1763         else:
1764             alarmButton.set_value("Disabled")
1765         alarmButton.set_alignment(0, 0.8, 1, 1)
1766         alarmButton.connect("clicked", alarmButton_pressed)
1767
1768         selector = hildon.TouchSelector(text=True)
1769         selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1770         selector.append_text("Walk")
1771         selector.append_text("Run")
1772         selector.connect("changed", selector_changed)
1773
1774         modePicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1775         modePicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1776         modePicker.set_title("Mode")
1777         modePicker.set_selector(selector)
1778         modePicker.set_active(self.config.get_mode())
1779
1780         weightButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1781         weightButton.set_title("Weight")
1782         weightButton.set_alignment(0, 0.8, 1, 1)
1783         update_weight_button()
1784         weightButton.connect("clicked", weight_dialog)
1785
1786         selectorUnit = hildon.TouchSelector(text=True)
1787         selectorUnit.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1788         selectorUnit.append_text("Metric (km)")
1789         selectorUnit.append_text("English (mi)")
1790         selectorUnit.connect("changed", selectorUnit_changed)
1791
1792         unitPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1793         unitPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1794         unitPicker.set_title("Unit")
1795         unitPicker.set_selector(selectorUnit)
1796         unitPicker.set_active(self.config.get_unit())
1797
1798         selectorUI = hildon.TouchSelector(text=True)
1799         selectorUI = hildon.TouchSelector(text=True)
1800         selectorUI.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1801         selectorUI.append_text("Show current + total + graph")
1802         selectorUI.append_text("Show current + total")
1803         selectorUI.append_text("Show only current")
1804         selectorUI.append_text("Show only total")
1805         selectorUI.connect("changed", selectorUI_changed)
1806
1807         UIPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1808         UIPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1809         UIPicker.set_title("Widget aspect")
1810         UIPicker.set_selector(selectorUI)
1811         UIPicker.set_active(self.config.get_aspect())
1812
1813         sensitivityButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1814         sensitivityButton.set_title("Sensitivity")
1815         sensitivityButton.set_alignment(0, 0.8, 1, 1)
1816         sensitivityButton.set_value(str(self.config.get_sensitivity()) + " %")
1817         sensitivityButton.connect("clicked", sensitivity_dialog)
1818
1819         donateButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1820         donateButton.set_title("Donate")
1821         donateButton.set_alignment(0, 0.8, 1, 1)
1822         donateButton.connect("clicked", donateButton_clicked, dialog)
1823
1824         logButton = hildon.CheckButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
1825         logButton.set_label("Log data")
1826         logButton.set_active(self.config.get_logging())
1827         logButton.connect("toggled", logButton_changed)
1828
1829         idleButton = hildon.CheckButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
1830         idleButton.set_label("Pause time when not walking")
1831         idleButton.set_active(self.config.get_noidletime())
1832         idleButton.connect("toggled", idleButton_changed)
1833
1834         pan_area = hildon.PannableArea()
1835         vbox = gtk.VBox()
1836         vbox.add(alarmButton)
1837         vbox.add(modePicker)
1838         vbox.add(stepLengthButton)
1839         vbox.add(weightButton)
1840         vbox.add(unitPicker)
1841         vbox.add(sensitivityButton)
1842         vbox.add(UIPicker)
1843         vbox.add(idleButton)
1844         vbox.add(resetButton)
1845         vbox.add(donateButton)
1846         #vbox.add(logButton)
1847
1848         pan_area.add_with_viewport(vbox)
1849         pan_area.set_size_request(-1, 300)
1850
1851         dialog.vbox.add(pan_area)
1852         dialog.show_all()
1853
1854         response = dialog.run()
1855         dialog.destroy()
1856
1857     def close_requested(self, widget):
1858         if self.controller.is_running:
1859             self.controller.stop_pedometer()
1860         self.controller.stop_midnight_callback()
1861
1862     def update_values(self):
1863         #TODO: do not update if the widget is not on the active desktop
1864         self.label_second_view.set_label(self.second_view_labels[self.config.get_secondview()])
1865         self.update_current()
1866         self.update_total()
1867
1868     def button_clicked(self, button):
1869         if self.controller.is_running:
1870             self.controller.stop_pedometer()
1871             self.button.set_icon(ICONSPATH + "play.png")
1872         else:
1873             self.controller.start_pedometer()
1874             self.button.set_icon(ICONSPATH + "stop.png")
1875             hildon.hildon_banner_show_information(self, "None", "Keep the N900 in a pocket close to your hip for best results")
1876
1877     def do_expose_event(self, event):
1878         cr = self.window.cairo_create()
1879         cr.region(event.window.get_clip_region())
1880         cr.clip()
1881         #cr.set_source_rgba(0.4, 0.64, 0.564, 0.5)
1882         style = self.rc_get_style()
1883         color = style.lookup_color("DefaultBackgroundColor")
1884         cr.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75);
1885
1886         radius = 5
1887         width = self.allocation.width
1888         height = self.allocation.height
1889
1890         x = self.allocation.x
1891         y = self.allocation.y
1892
1893         cr.move_to(x + radius, y)
1894         cr.line_to(x + width - radius, y)
1895         cr.curve_to(x + width - radius, y, x + width, y, x + width, y + radius)
1896         cr.line_to(x + width, y + height - radius)
1897         cr.curve_to(x + width, y + height - radius, x + width, y + height, x + width - radius, y + height)
1898         cr.line_to(x + radius, y + height)
1899         cr.curve_to(x + radius, y + height, x, y + height, x, y + height - radius)
1900         cr.line_to(x, y + radius)
1901         cr.curve_to(x, y + radius, x, y, x + radius, y)
1902
1903         cr.set_operator(cairo.OPERATOR_SOURCE)
1904         cr.fill_preserve()
1905
1906         color = style.lookup_color("ActiveTextColor")
1907         cr.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.5);
1908         cr.set_line_width(1)
1909         cr.stroke()
1910
1911         hildondesktop.HomePluginItem.do_expose_event(self, event)
1912
1913     def do_realize(self):
1914         screen = self.get_screen()
1915         self.set_colormap(screen.get_rgba_colormap())
1916         self.set_app_paintable(True)
1917         hildondesktop.HomePluginItem.do_realize(self)
1918
1919 hd_plugin_type = PedometerHomePlugin
1920
1921 import math
1922 import logging
1923
1924 logger = logging.getLogger("pedometer")
1925 logger.setLevel(logging.INFO)
1926
1927 ch = logging.StreamHandler()
1928 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
1929 ch.setFormatter(formatter)
1930 logger.addHandler(ch)
1931
1932 # The code below is just for testing purposes.
1933 # It allows to run the widget as a standalone process.
1934 if __name__ == "__main__":
1935     import gobject
1936     gobject.type_register(hd_plugin_type)
1937     obj = gobject.new(hd_plugin_type, plugin_id="plugin_id")
1938     obj.show_all()
1939     gtk.main()