--- /dev/null
+
+/*
+ * generate alarm messages as required.
+ * Alarm information is stored in /data/alarms
+ * format is:
+ * date:recurrence:message
+ *
+ * date is yyyy-mm-dd-hh-mm-ss
+ * recurrence is currently nndays
+ * message is any text
+ *
+ * When the time comes, a txt message is generated
+ *
+ * We use dnotify to watch the file
+ *
+ * We never report any event that is before the timestamp
+ * on the file - that guards against replaying lots of
+ * events if the clock gets set backwards.
+ */
+
+#define _XOPEN_SOURCE
+#define _BSD_SOURCE
+#define _GNU_SOURCE
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <time.h>
+#include <string.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <linux/rtc.h>
+#include <sys/dir.h>
+#include <event.h>
+#include "libsus.h"
+
+struct alarm_ev {
+ struct alarm_ev *next;
+ time_t when;
+ long recur;
+ char *mesg;
+};
+
+void alarm_free(struct alarm_ev *ev)
+{
+ while (ev) {
+ struct alarm_ev *n = ev->next;
+ free(ev->mesg);
+ free(ev);
+ ev = n;
+ }
+}
+
+void update_event(struct alarm_ev *ev, time_t now)
+{
+ /* make sure 'when' is in the future if possible */
+ while (ev->when < now && ev->recur > 0)
+ ev->when += ev->recur;
+}
+
+struct alarm_ev *event_parse(char *line)
+{
+ struct alarm_ev *rv;
+ char *recur, *mesg;
+ struct tm tm;
+ long rsec;
+ recur = strchr(line, ':');
+ if (!recur)
+ return NULL;
+ *recur++ = 0;
+ mesg = strchr(recur, ':');
+ if (!mesg)
+ return NULL;
+ *mesg++ = 0;
+ line = strptime(line, "%Y-%m-%d-%H-%M-%S", &tm);
+ if (!line)
+ return NULL;
+ rsec = atoi(recur);
+ if (rsec < 0)
+ return NULL;
+ rv = malloc(sizeof(*rv));
+ rv->next = NULL;
+ tm.tm_isdst = -1;
+ rv->when = mktime(&tm);
+ rv->recur = rsec;
+ rv->mesg = strdup(mesg);
+ return rv;
+}
+
+struct alarm_ev *event_load_soonest(char *file, time_t now)
+{
+ /* load the file and return only the soonest event at or after now */
+ char line[2048];
+ struct stat stb;
+ struct alarm_ev *rv = NULL, *ev;
+ FILE *f;
+
+ f = fopen(file, "r");
+ if (!f)
+ return NULL;
+ if (fstat(fileno(f), &stb) == 0 &&
+ stb.st_mtime > now)
+ now = stb.st_mtime;
+
+ while (fgets(line, sizeof(line), f) != NULL) {
+ char *eol = line + strlen(line);
+ if (eol > line && eol[-1] == '\n')
+ eol[-1] = 0;
+
+ ev = event_parse(line);
+ if (!ev)
+ continue;
+ update_event(ev, now);
+ if (ev->when < now) {
+ alarm_free(ev);
+ continue;
+ }
+ if (rv == NULL) {
+ rv = ev;
+ continue;
+ }
+ if (rv->when < ev->when) {
+ alarm_free(ev);
+ continue;
+ }
+ alarm_free(rv);
+ rv = ev;
+ }
+ fclose(f);
+ return rv;
+}
+
+struct alarm_ev *event_load_soonest_dir(char *dir, time_t now)
+{
+ DIR *d = opendir(dir);
+ struct dirent *de;
+ struct alarm_ev *rv = NULL;
+
+ if (!d)
+ return NULL;
+ while ((de = readdir(d)) != NULL) {
+ char *p = NULL;
+ struct alarm_ev *e;
+ if (de->d_ino == 0)
+ continue;
+ if (de->d_name[0] == '.')
+ continue;
+ asprintf(&p, "%s/%s", dir, de->d_name);
+ e = event_load_soonest(p, now);
+ free(p);
+ if (e == NULL)
+ ;
+ else if (rv == NULL)
+ rv = e;
+ else if (rv->when < e->when)
+ alarm_free(e);
+ else {
+ alarm_free(rv);
+ rv = e;
+ }
+ }
+ closedir(d);
+ return rv;
+}
+
+void event_deliver(struct alarm_ev *ev)
+{
+ char line[2048];
+ char file[1024];
+ struct tm *tm;
+ char *z;
+ char *m;
+ int f;
+ static time_t last_ev;
+ time_t now;
+
+ if (!ev->mesg || !ev->mesg[0])
+ return;
+ time(&now);
+ if (now <= last_ev)
+ now = last_ev + 1;
+ last_ev = now;
+ tm = gmtime(&now);
+ strftime(line, 2048, "%Y%m%d-%H%M%S", tm);
+ tm = localtime(&ev->when);
+ strftime(line+strlen(line), 1024, " ALARM %Y%m%d-%H%M%S", tm);
+ z = line + strlen(line);
+ sprintf(z, "%+03d alarm ", tm->tm_gmtoff/60/15);
+
+ z = line + strlen(line);
+ m = ev->mesg;
+
+ while (*m) {
+ switch (*m) {
+ default:
+ *z++ = *m;
+ break;
+ case ' ':
+ case '\t':
+ case '\n':
+ sprintf(z, "%%%02x", *m);
+ z += 3;
+ break;
+ }
+ m++;
+ }
+ *z++ = '\n';
+ *z = 0;
+
+ sprintf(file, "/data/SMS/%.6s", line);
+ f = open(file, O_WRONLY | O_APPEND | O_CREAT, 0600);
+ if (f >= 0) {
+ write(f, line, strlen(line));
+ close(f);
+ f = open("/data/SMS/newmesg", O_WRONLY | O_APPEND | O_CREAT,
+ 0600);
+ if (f >= 0) {
+ line[16] = '\n';
+ write(f, line, 17);
+ close(f);
+ }
+ f = open("/var/run/alert/alarm", O_WRONLY | O_CREAT, 0600);
+ if (f) {
+ write(f, "new\n", 4);
+ close(f);
+ }
+ /* Abort any current suspend cycle */
+ suspend_abort(-1);
+ }
+}
+
+
+time_t now;
+struct event *wkev;
+int efd;
+static void check_alarms(int fd, short ev, void *vp)
+{
+ struct alarm_ev *evt = event_load_soonest_dir("/data/alarms", now);
+ if (wkev && !vp)
+ wakealarm_destroy(wkev);
+ wkev = NULL;
+ while ((evt = event_load_soonest_dir("/data/alarms", now)) != NULL
+ && evt->when <= time(0)) {
+ event_deliver(evt);
+ now = evt->when + 1;
+ alarm_free(evt);
+ }
+ if (!evt)
+ return;
+ wkev = wakealarm_set(evt->when, check_alarms, (void*)1);
+ alarm_free(evt);
+ fcntl(efd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_RENAME);
+}
+
+main(int argc, char *argv[])
+{
+ struct event ev;
+ sigset_t set;
+
+ efd = open("/data/alarms", O_DIRECTORY|O_RDONLY);
+ if (efd < 0)
+ exit(1);
+
+ event_init();
+ signal_set(&ev, SIGIO, check_alarms, NULL);
+ signal_add(&ev, NULL);
+
+ fcntl(efd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_RENAME);
+
+ time(&now);
+ check_alarms(0, 0, NULL);
+
+ event_loop(0);
+ exit(0);
+}
--- /dev/null
+#define _XOPEN_SOURCE
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <string.h>
+#include <malloc.h>
+#include <math.h>
+#include <time.h>
+
+#include <gtk/gtk.h>
+
+#include "listsel.h"
+
+struct event {
+ struct event *next;
+ time_t when, first;
+ long recur;
+ char *mesg;
+};
+
+
+char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+
+GtkWidget *calblist[42], *month, *date_display, *date_time_display;
+GtkWidget *window, *clockw, *cal, *reasonw, *timerw;
+GtkWidget *browse_buttons, *event_buttons, *move_buttons, *move_event;
+GtkWidget *timer_display, *time_display;
+GtkWidget *today_btn, *undelete_btn;
+GtkWidget *once_btn, *daily_btn, *weekly_btn, *freq_buttons;
+GtkWidget *timers_list;
+GtkTextBuffer *reason_buffer;
+time_t dlist[42], chosen_date;
+int prev_mday, hour, minute;
+int freq;
+
+struct event *evlist, *active_event;
+struct event *deleted_event;
+struct list_entry_text *alarm_entries;
+time_t alarm_date = 0;
+int evcnt = -1;
+int moving = 0;
+struct sellist *alarm_selector;
+
+char *filename = NULL;
+
+PangoLayout *layout;
+GdkGC *colour = NULL;
+
+int size(struct list_entry *i, int *width, int*height)
+{
+ PangoRectangle ink, log;
+ struct list_entry_text *item = (void*)i;
+
+ if (i->height) {
+ *width = item->true_width;
+ *height = i->height;
+ return 0;
+ }
+ pango_layout_set_text(layout, item->text, -1);
+ pango_layout_get_extents(layout, &ink, &log);
+ *width = log.width / PANGO_SCALE;
+ *height = log.height / PANGO_SCALE;
+ item->true_width = i->width = *width;
+ i->height = *height;
+ return 0;
+}
+
+int render(struct list_entry *i, int selected, GtkWidget *d)
+{
+ PangoRectangle ink, log;
+ struct list_entry_text *item = (void*)i;
+ int x;
+ GdkColor col;
+
+ pango_layout_set_text(layout, item->text, -1);
+
+ if (colour == NULL) {
+ colour = gdk_gc_new(gtk_widget_get_window(d));
+ gdk_color_parse("purple", &col);
+ gdk_gc_set_rgb_fg_color(colour, &col);
+ }
+ if (selected) {
+ gdk_color_parse("pink", &col);
+ gdk_gc_set_rgb_fg_color(colour, &col);
+ gdk_draw_rectangle(gtk_widget_get_window(d),
+ colour, TRUE,
+ item->head.x, item->head.y,
+ item->head.width, item->head.height);
+ gdk_color_parse("purple", &col);
+ gdk_gc_set_rgb_fg_color(colour, &col);
+ }
+#if 0
+ x = (i->width - item->true_width)/2;
+ if (x < 0)
+ x = 0;
+#else
+ x = 0;
+#endif
+ gdk_draw_layout(gtk_widget_get_window(d),
+ colour,
+ item->head.x+x, item->head.y, layout);
+ return 0;
+}
+
+
+
+void set_cal(time_t then);
+
+void set_date(GtkButton *button, int num)
+{
+ set_cal(dlist[num]);
+}
+
+void prev_year(GtkButton *button)
+{
+ set_cal(chosen_date - 365*24*3600);
+}
+void next_year(GtkButton *button)
+{
+ set_cal(chosen_date + 365*24*3600);
+}
+
+void finish(void)
+{
+ gtk_main_quit();
+}
+
+void set_time(GtkButton *button, int num)
+{
+ struct tm now;
+ time_t then;
+ char buf[100];
+ int hr24, hr12;
+ char m = 'A';
+ if (num >= 100)
+ hour = num/100;
+ else
+ minute = num;
+
+ hr24 = hour;
+ if (hour == 24)
+ hr24 = 0;
+ hr12 = hour;
+ if (hr12 > 12) {
+ hr12 -= 12;
+ m = 'P';
+ }
+ if (hr12 == 12)
+ m = 'P' + 'A' - m;
+
+ then = chosen_date + hour * 3600 + minute * 60;
+ localtime_r(&then, &now);
+ strftime(buf, sizeof(buf), "%a, %d %B %Y, %H:%M", &now);
+ gtk_label_set_text(GTK_LABEL(date_display), buf);
+ gtk_label_set_text(GTK_LABEL(date_time_display), buf);
+#if 0
+ sprintf(buf, "%02d:%02dhrs\n%2d:%02d%cM", hr24, minute,
+ hr12, minute, m);
+ gtk_label_set_text(GTK_LABEL(time_label), buf);
+#endif
+}
+
+GtkWidget *add_button(GtkWidget **blist, char *name,
+ PangoFontDescription *fd,
+ GCallback handle)
+{
+ GtkWidget *l, *b;
+ if (*blist == NULL) {
+ *blist = gtk_hbox_new(TRUE, 0);
+ gtk_widget_show(*blist);
+ gtk_widget_set_size_request(*blist, -1, 80);
+ }
+
+ l = *blist;
+
+ b = gtk_button_new_with_label(name);
+ gtk_widget_show(b);
+ pango_font_description_set_size(fd, 12 * PANGO_SCALE);
+ gtk_widget_modify_font(GTK_BIN(b)->child, fd);
+ gtk_container_add(GTK_CONTAINER(l), b);
+ g_signal_connect((gpointer)b, "clicked", handle, NULL);
+ return b;
+}
+int delay = 0;
+int show_time(void *d);
+int timer_tick = -1;
+void add_time(int mins);
+
+void load_timers(void);
+void select_timer(void)
+{
+ gtk_container_remove(GTK_CONTAINER(window), cal);
+ show_time(NULL);
+ delay = 0;
+ add_time(0);
+ load_timers();
+ gtk_container_add(GTK_CONTAINER(window), timerw);
+ timer_tick = g_timeout_add(1000, show_time, NULL);
+}
+
+void events_select(void)
+{
+ /* the selected event becomes current and can be moved */
+ if (!active_event)
+ return;
+ set_cal(active_event->when);
+}
+
+void move_confirm(void)
+{
+ struct tm now;
+ time_t then;
+ char buf[100];
+
+ if (active_event) {
+ then = active_event->when;
+ localtime_r(&then, &now);
+ hour = now.tm_hour;
+ minute = now.tm_min;
+ } else
+ hour = minute = 0;
+ then = chosen_date + hour * 3600 + minute * 60;
+ localtime_r(&then, &now);
+ strftime(buf, sizeof(buf), "%a, %d %B %Y, %H:%M", &now);
+ gtk_label_set_text(GTK_LABEL(date_display), buf);
+ gtk_label_set_text(GTK_LABEL(date_time_display), buf);
+
+ gtk_container_remove(GTK_CONTAINER(window), cal);
+ gtk_container_add(GTK_CONTAINER(window), clockw);
+}
+
+void update_freq(void);
+void events_move(void)
+{
+ if (!active_event)
+ return;
+
+ moving = 1;
+ freq = active_event->recur;
+ set_cal(active_event->when);
+
+ gtk_widget_hide(event_buttons);
+ gtk_widget_show(move_buttons);
+ update_freq();
+ gtk_widget_show(freq_buttons);
+
+ gtk_widget_hide(alarm_selector->drawing);
+ gtk_widget_show(move_event);
+
+ gtk_text_buffer_set_text(reason_buffer, active_event->mesg, -1);
+}
+
+void events_new(void)
+{
+ moving = 2;
+ freq = 0;
+ gtk_widget_hide(event_buttons);
+ gtk_widget_hide(browse_buttons);
+ gtk_widget_show(move_buttons);
+ update_freq();
+ gtk_widget_show(freq_buttons);
+
+ gtk_widget_hide(alarm_selector->drawing);
+ gtk_widget_show(move_event);
+ gtk_text_buffer_set_text(reason_buffer, "", -1);
+}
+
+void move_abort(void)
+{
+ gtk_widget_hide(move_buttons);
+ gtk_widget_hide(freq_buttons);
+ if (active_event) {
+ gtk_widget_hide(browse_buttons);
+ gtk_widget_show(event_buttons);
+ } else {
+ gtk_widget_hide(event_buttons);
+ gtk_widget_show(browse_buttons);
+ }
+
+ gtk_widget_show(alarm_selector->drawing);
+ gtk_widget_hide(move_event);
+ moving = 0;
+ if (active_event)
+ set_cal(active_event->when);
+ else
+ set_cal(chosen_date);
+
+}
+
+void cal_today(void)
+{
+ /* jump to today */
+ time_t now;
+ time(&now);
+ set_cal(now);
+ if (deleted_event) {
+ gtk_widget_hide(today_btn);
+ gtk_widget_show(undelete_btn);
+ }
+}
+
+void cal_restore(void)
+{
+ gtk_container_remove(GTK_CONTAINER(window), clockw);
+ gtk_container_add(GTK_CONTAINER(window), cal);
+}
+
+void reason_confirm(void);
+void clock_confirm(void)
+{
+ gtk_container_remove(GTK_CONTAINER(window), clockw);
+ gtk_container_add(GTK_CONTAINER(window), reasonw);
+ reason_confirm();
+}
+
+void update_freq(void)
+{
+ if (freq == 0)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(once_btn)->child), "<span background=\"pink\">Once</span>");
+ else
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(once_btn)->child), "(Once)");
+
+ if (freq == 1*24*3600)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "<span background=\"pink\">Daily</span>");
+ else if (freq == 2*24*3600)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "<span background=\"pink\">2-Daily</span>");
+ else if (freq == 3*24*3600)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "<span background=\"pink\">3-Daily</span>");
+ else
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "(Daily)");
+
+ if (freq == 7*24*3600)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "<span background=\"pink\">Weekly</span>");
+ else if (freq == 14*24*3600)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "<span background=\"pink\">Fortnightly</span>");
+ else
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "(Weekly)");
+}
+void set_once(void)
+{
+ freq = 0;
+ update_freq();
+}
+void set_weekly(void)
+{
+ if (freq == 7 * 24 * 3600)
+ freq = 14 *24 * 3600;
+ else
+ freq = 7 * 24 * 3600;
+ update_freq();
+}
+void set_daily(void)
+{
+ if (freq == 1 * 24 * 3600)
+ freq = 2 * 24 * 3600;
+ else if (freq == 2 *24 * 3600)
+ freq = 3 * 24 * 3600;
+ else
+ freq = 1 * 24 * 3600;
+ update_freq();
+}
+
+/********************************************************************/
+
+
+void event_free(struct event *ev)
+{
+ while (ev) {
+ struct event *n = ev->next;
+ free(ev->mesg);
+ free(ev);
+ ev = n;
+ }
+}
+
+void update_event(struct event *ev, time_t now)
+{
+ /* make sure 'when' is in the future if possible */
+ ev->when = ev->first;
+ while (ev->when < now && ev->recur > 0)
+ ev->when += ev->recur;
+}
+
+void update_events(struct event *ev, time_t now)
+{
+ while (ev) {
+ update_event(ev, now);
+ ev = ev->next;
+ }
+}
+
+void sort_events(struct event **evtp)
+{
+ /* sort events list in *evtp by ->when
+ * use merge sort
+ */
+ int cnt=0;
+
+ struct event *ev[2];
+ ev[0] = *evtp;
+ ev[1] = NULL;
+
+ do {
+ struct event **evp[2], *e[2];
+ int current = 0;
+ time_t prev = 0;
+ int next = 0;
+ cnt++;
+ evp[0] = &ev[0];
+ evp[1] = &ev[1];
+ e[0] = ev[0];
+ e[1] = ev[1];
+
+ /* take least of e[0] and e[1]
+ * if it is larger than prev, add to
+ * evp[current], else swap current then add
+ */
+ while (e[0] || e[1]) {
+ if (e[next] == NULL ||
+ (e[1-next] != NULL &&
+ !((prev <= e[1-next]->when)
+ ^(e[1-next]->when <= e[next]->when)
+ ^(e[next]->when <= prev)))
+ )
+ next = 1 - next;
+
+ if (e[next]->when < prev)
+ current = 1 - current;
+ prev = e[next]->when;
+ *evp[current] = e[next];
+ evp[current] = &e[next]->next;
+ e[next] = e[next]->next;
+ }
+ *evp[0] = NULL;
+ *evp[1] = NULL;
+ } while (ev[0] && ev[1]);
+ if (ev[0])
+ *evtp = ev[0];
+ else
+ *evtp = ev[1];
+}
+
+struct event *event_parse(char *line)
+{
+ struct event *rv;
+ char *recur, *mesg;
+ struct tm tm;
+ long rsec;
+ recur = strchr(line, ':');
+ if (!recur)
+ return NULL;
+ *recur++ = 0;
+ mesg = strchr(recur, ':');
+ if (!mesg)
+ return NULL;
+ *mesg++ = 0;
+ line = strptime(line, "%Y-%m-%d-%H-%M-%S", &tm);
+ if (!line)
+ return NULL;
+ rsec = atoi(recur);
+ if (rsec < 0)
+ return NULL;
+ rv = malloc(sizeof(*rv));
+ rv->next = NULL;
+ tm.tm_isdst = -1;
+ rv->when = mktime(&tm);
+ rv->first = rv->when;
+ rv->recur = rsec;
+ rv->mesg = strdup(mesg);
+ return rv;
+}
+
+struct event *event_load_all(char *file)
+{
+ /* load the file and return a linked list */
+ char line[2048];
+ struct event *rv = NULL, *ev;
+ FILE *f;
+
+ f = fopen(file, "r");
+ if (!f)
+ return NULL;
+ while (fgets(line, sizeof(line), f) != NULL) {
+ char *eol = line + strlen(line);
+ if (eol > line && eol[-1] == '\n')
+ eol[-1] = 0;
+
+ ev = event_parse(line);
+ if (!ev)
+ continue;
+ ev->next = rv;
+ rv = ev;
+ }
+ return rv;
+}
+
+void event_save_all(char *file, struct event *list)
+{
+ FILE *f;
+ char *tmp = strdup(file);
+ char *c;
+
+ c = tmp + strlen(tmp);
+ while (c > tmp && c[-1] != '/')
+ c--;
+ if (*c) *c = '.';
+
+ f = fopen(tmp, "w");
+ while (list) {
+ struct event *ev = list;
+ struct tm *tm;
+ char buf[100];
+ list = ev->next;
+
+ tm = localtime(&ev->first);
+ strftime(buf, sizeof(buf), "%Y-%m-%d-%H-%M-%S", tm);
+ fprintf(f, "%s:%d:%s\n", buf, ev->recur, ev->mesg);
+ }
+ fflush(f);
+ fsync(fileno(f));
+ fclose(f);
+ rename(tmp, file);
+}
+
+
+void load_timers(void)
+{
+ time_t now = time(0);
+ struct event *timers = event_load_all("/data/alarms/timer");
+ struct event *t;
+ char buf[1024];
+ int len = 0;
+
+ update_events(timers, now);
+ sort_events(&timers);
+ strcpy(buf,"");
+ for (t = timers ; t; t = t->next) {
+ struct tm *tm;
+ if (t->when < now)
+ continue;
+ tm = localtime(&t->when);
+ strftime(buf+len, sizeof(buf)-len, "%H:%M\n", tm);
+ len += strlen(buf+len);
+ }
+ event_free(timers);
+ gtk_label_set_text(GTK_LABEL(timers_list), buf);
+}
+
+
+struct list_entry *alarms_item(void *list, int n)
+{
+ struct event *ev;
+
+ if (alarm_date != chosen_date) {
+ int i;
+ struct tm *tm, today;
+
+ if (evlist == NULL) {
+ filename = "/home/neilb/ALARMS";
+ evlist = event_load_all(filename);
+ if (evlist == NULL) {
+ filename = "/data/alarms/cal";
+ evlist = event_load_all(filename);
+ }
+ }
+ update_events(evlist, chosen_date);
+ sort_events(&evlist);
+ evcnt = 0;
+ ev = evlist;
+ while (ev && ev->when < chosen_date)
+ ev = ev->next;
+ for ( ; ev ; ev = ev->next)
+ evcnt++;
+ free(alarm_entries);
+ alarm_entries = calloc(evcnt, sizeof(alarm_entries[0]));
+ ev = evlist;
+ while (ev && ev->when < chosen_date)
+ ev = ev->next;
+ tm = localtime(&chosen_date);
+ today = *tm;
+ for (i=0; ev ; i++, ev = ev->next) {
+ char buf[50], o, c, r;
+ struct tm *tm = localtime(&ev->when);
+ if (tm->tm_mday == today.tm_mday &&
+ tm->tm_mon == today.tm_mon &&
+ tm->tm_year == today.tm_year)
+ strftime(buf, 50, "%H:%M\t", tm);
+ else if (tm->tm_year == today.tm_year ||
+ (tm->tm_year == today.tm_year + 1 &&
+ tm->tm_mon < today.tm_mon)
+ )
+ strftime(buf, 50, "%b%d\t", tm);
+ else
+ strftime(buf, 50, "%Y\t", tm);
+
+ o = ' ';c=' ';
+ if (ev->when < time(0)) {
+ o = '(';
+ c = ')';
+ }
+ if (ev->recur == 0)
+ r = ' ';
+ else if (ev->recur == 3600*24*1)
+ r = '1'; /* daily */
+ else if (ev->recur == 3600*24*2)
+ r = '2'; /* 2-daily */
+ else if (ev->recur == 3600*24*3)
+ r = '3'; /* 3-daily */
+ else if (ev->recur == 3600*24*7)
+ r = '+'; /* weekly */
+ else if (ev->recur == 3600*24*14)
+ r = '*'; /* fortnightly */
+ else
+ r = '#'; /* other period */
+ asprintf(&alarm_entries[i].text, "%c%c %s %s%c",
+ o, r,
+ buf, ev->mesg, c);
+ alarm_entries[i].bg = "white";
+ alarm_entries[i].fg = "blue";
+ alarm_entries[i].underline = 0;
+ }
+ alarm_date = chosen_date;
+ }
+ if (n >= evcnt)
+ return NULL;
+
+ return &alarm_entries[n].head;
+}
+
+
+void events_delete(void)
+{
+ struct event **evp;
+ if (!active_event)
+ return;
+
+ for (evp = &evlist; *evp; evp = &(*evp)->next) {
+ if (*evp != active_event)
+ continue;
+ *evp = active_event->next;
+ break;
+ }
+ if (deleted_event) {
+ free(deleted_event->mesg);
+ free(deleted_event);
+ }
+ deleted_event = active_event;
+ active_event = NULL;
+ event_save_all(filename, evlist);
+ alarm_date = 0;
+ move_abort();
+}
+
+void events_undelete(void)
+{
+ if (!deleted_event)
+ return;
+ active_event = deleted_event;
+ deleted_event = NULL;
+ active_event->next = evlist;
+ evlist = active_event;
+ event_save_all(filename, evlist);
+ alarm_date = 0;
+ move_abort();
+}
+
+void alarms_selected(void *list, int s)
+{
+ struct event *e;
+
+
+ if (s < 0 || s >= evcnt)
+ return;
+ e = evlist;
+ while (e && e->when < chosen_date)
+ e = e->next;
+ while (s && e) {
+ s--;
+ e = e->next;
+ }
+ active_event = e;
+
+ gtk_widget_hide(browse_buttons);
+ gtk_widget_show(event_buttons);
+}
+
+struct list_handlers alarms_han = {
+ .getitem = alarms_item,
+ .get_size = size,
+ .render = render,
+ .selected = alarms_selected,
+};
+
+
+GtkWidget *create_cal_window(void)
+{
+ GtkWidget *v, *t, *l;
+ GtkWidget *h;
+ GtkWidget *blist = NULL;
+ int i,r,c;
+ PangoFontDescription *desc;
+ struct sellist *sl;
+
+ desc = pango_font_description_new();
+ pango_font_description_set_size(desc, 11 * PANGO_SCALE);
+
+ v = gtk_vbox_new(FALSE, 0);
+ gtk_widget_show(v);
+
+ h = gtk_hbox_new(FALSE, 0);
+ gtk_container_add_with_properties(GTK_CONTAINER(v), h, "expand", 0, NULL);
+ gtk_widget_show(h);
+
+ l = gtk_button_new_with_label(" << ");
+ gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+ gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE);
+ g_signal_connect((gpointer)l, "clicked",
+ G_CALLBACK(prev_year), NULL);
+ gtk_container_add(GTK_CONTAINER(h), l);
+ gtk_widget_show(l);
+ gtk_widget_set(l, "can-focus", 0, NULL);
+
+ l = gtk_label_new("Month 9999");
+ gtk_widget_modify_font(l, desc);
+ gtk_container_add(GTK_CONTAINER(h), l);
+ gtk_widget_show(l);
+ month = l;
+
+
+ l = gtk_button_new_with_label(" >> ");
+ gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+ gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE);
+ g_signal_connect((gpointer)l, "clicked",
+ G_CALLBACK(next_year), NULL);
+ gtk_container_add(GTK_CONTAINER(h), l);
+ gtk_widget_show(l);
+ gtk_widget_set(l, "can-focus", 0, NULL);
+
+
+ t = gtk_table_new(7, 7, FALSE);
+ gtk_widget_show(t);
+
+ gtk_container_add_with_properties(GTK_CONTAINER(v), t, "expand", 0, NULL);
+
+ for (i=0; i<7; i++) {
+ l = gtk_label_new(days[i]);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+ gtk_table_attach(GTK_TABLE(t), l, i, i+1, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);
+ }
+
+ for (r=1; r<7; r++)
+ for (c=0; c<7; c++) {
+ int p = (r-1)*7+c;
+ l = gtk_button_new_with_label(" 99 ");
+ gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+ gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE);
+ gtk_table_attach(GTK_TABLE(t), l, c,c+1, r,r+1, GTK_FILL, GTK_FILL, 0, 0);
+ gtk_widget_show(l);
+ g_signal_connect((gpointer)l, "clicked",
+ G_CALLBACK(set_date), (void*)(long)p);
+ calblist[p] = l;
+ dlist[p] = 0;
+ }
+
+ /* list of alarms from this date */
+ sl = listsel_new(NULL, &alarms_han);
+ pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+ gtk_widget_modify_font(sl->drawing, desc);
+ gtk_container_add(GTK_CONTAINER(v), sl->drawing);
+ gtk_widget_show(sl->drawing);
+ alarm_selector = sl;
+
+ l = gtk_text_view_new_with_buffer(reason_buffer);
+ gtk_widget_modify_font(l, desc);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(l), GTK_WRAP_WORD_CHAR);
+ gtk_widget_hide(l);
+ gtk_container_add(GTK_CONTAINER(v), l);
+ move_event = l;
+
+
+ add_button(&blist, "timer", desc, select_timer);
+ add_button(&blist, "new", desc, events_new);
+ today_btn = add_button(&blist, "today", desc, cal_today);
+ undelete_btn = add_button(&blist, "undelete", desc, events_undelete);
+ gtk_widget_hide(undelete_btn);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+ browse_buttons = blist;
+ blist = NULL;
+
+ add_button(&blist, "go to", desc, events_select);
+ add_button(&blist, "change", desc, events_move);
+ add_button(&blist, "delete", desc, events_delete);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+ event_buttons = blist;
+ blist = NULL;
+
+ add_button(&blist, "confirm", desc, move_confirm);
+ add_button(&blist, "abort", desc, move_abort);
+ gtk_widget_hide(blist);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+ move_buttons = blist;
+
+ blist = NULL;
+ once_btn = add_button(&blist, "Once", desc, set_once);
+ daily_btn = add_button(&blist, "(Daily)", desc, set_daily);
+ weekly_btn = add_button(&blist, "(Weekly)", desc, set_weekly);
+ gtk_widget_hide(blist);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+ freq_buttons = blist;
+
+ return v;
+}
+
+void set_cal(time_t then)
+{
+ struct tm now, first, today, *tm;
+ int d, x;
+ time_t today_s;
+ char buf[400];
+
+ time(&today_s);
+ localtime_r(&today_s, &today);
+ localtime_r(&then, &now);
+
+ then -= now.tm_sec;
+ then -= now.tm_min*60;
+ then -= now.tm_hour*3600;
+ chosen_date = then;
+
+ localtime_r(&then, &now);
+
+ tm = localtime(&then);
+
+ strftime(buf, sizeof(buf), "%a, %d %B %Y", &now);
+ gtk_label_set_text(GTK_LABEL(date_display), buf);
+
+ /* previous month */
+ while (tm->tm_mon == now.tm_mon) {
+ then -= 22*3600;
+ tm = localtime(&then);
+ }
+ /* Start of week */
+ while (tm->tm_wday != 0) {
+ then -= 22*3600;
+ tm = localtime(&then);
+ }
+ first = *tm;
+
+ if (abs(dlist[0] - then) > 48*3600) {
+ strftime(buf, 40, "%B %Y", &now);
+ gtk_label_set_text(GTK_LABEL(month), buf);
+ }
+
+ for (d=0; d<42; d++) {
+ char *bg = "", *fg = "black", *today_fg = "blue";
+
+ if (tm->tm_mon != now.tm_mon) {
+ fg = "grey"; today_fg = "pink";
+ } else if (tm->tm_mday == now.tm_mday)
+ bg = "background=\"green\"";
+
+ if (tm->tm_year == today.tm_year &&
+ tm->tm_mon == today.tm_mon &&
+ tm->tm_mday == today.tm_mday)
+ fg = today_fg;
+
+ sprintf(buf, "<span %s foreground=\"%s\"> %02d </span>",
+ bg, fg, tm->tm_mday);
+ if (abs(dlist[d] - then) > 48*3600 ||
+ tm->tm_mday == now.tm_mday ||
+ tm->tm_mday == prev_mday)
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(calblist[d])->child), buf);
+ dlist[d] = then;
+ x = tm->tm_mday;
+ while (x == tm->tm_mday) {
+ then += 22*3600;
+ tm = localtime(&then);
+ }
+ }
+ prev_mday = now.tm_mday;
+
+ alarm_selector->selected = -1;
+ if (!moving) {
+ active_event = NULL;
+ gtk_widget_hide(event_buttons);
+ gtk_widget_show(browse_buttons);
+ }
+ g_signal_emit_by_name(alarm_selector->drawing, "configure-event",
+ NULL, alarm_selector);
+
+
+ gtk_widget_show(today_btn);
+ gtk_widget_hide(undelete_btn);
+}
+
+void center_me(GtkWidget *label, void *xx, int pos)
+{
+ /* I have just been realised. Find size and
+ * adjust position
+ */
+ GtkWidget *button = gtk_widget_get_parent(label);
+ GtkWidget *parent = gtk_widget_get_parent(button);
+ GtkAllocation alloc;
+ int x = pos / 10000;
+ int y = pos % 10000;
+
+ gtk_widget_get_allocation(button, &alloc);
+ printf("move %d %d/%d by %d/%d from %d/%d\n", pos, x, y,
+ alloc.width/2, alloc.height/2, alloc.x, alloc.y);
+ x -= alloc.width / 2;
+ y -= alloc.height / 2;
+ if (x != alloc.x || y != alloc.y) {
+ printf("move %d %d/%d by %d/%d from %d/%d\n", pos, x, y,
+ alloc.width/2, alloc.height/2, alloc.x, alloc.y);
+ gtk_fixed_move(GTK_FIXED(parent), button, x, y);
+ }
+}
+
+
+void add_nums(GtkWidget *f, int radius, int start, int end, int step,
+ int div, int scale,
+ char *colour, PangoFontDescription *desc)
+{
+ int i;
+ for (i=start; i<end; i+= step) {
+ char buf[400];
+ double a, s, c, r;
+ int x, y;
+ GtkWidget *l;
+ long scaled;
+ sprintf(buf, "<span background=\"%s\">%02d</span>", colour, i);
+ l = gtk_button_new_with_label(" 99 ");
+ gtk_widget_modify_font(GTK_BIN(l)->child, desc);
+ gtk_label_set_markup(GTK_LABEL(GTK_BIN(l)->child),
+ buf);
+ gtk_widget_show(l);
+ scaled = i * scale;
+ g_signal_connect((gpointer)l, "clicked",
+ G_CALLBACK(set_time), (void*)scaled);
+ a = i * 2.0 * M_PI / div;
+ s = sin(a); c= cos(a);
+ r = (double)radius;
+ if (fabs(s) < fabs(c))
+ r = r / fabs(c);
+ else
+ r = r / fabs(s);
+ x = 210 + (int)(r * s) - r/18;
+ y = 210 - (int)(r * c) - r/18;
+ gtk_fixed_put(GTK_FIXED(f), l, x, y);
+ }
+}
+
+GtkWidget *create_clock_window(void)
+{
+ PangoFontDescription *desc;
+ GtkWidget *f, *l, *v;
+ GtkWidget *blist = NULL;
+
+ desc = pango_font_description_new();
+
+ v = gtk_vbox_new(FALSE, 0);
+ gtk_widget_show(v);
+
+ l = gtk_label_new(" Date "); /* Date for which time is being chosen */
+ pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+ gtk_widget_show(l);
+ date_display = l;
+
+ f = gtk_fixed_new();
+ gtk_widget_show(f);
+ gtk_container_add(GTK_CONTAINER(v), f);
+
+ pango_font_description_set_size(desc, 17 * PANGO_SCALE);
+ add_nums(f, 200, 6, 18, 1, 12, 100, "light green", desc);
+ pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+ add_nums(f, 140, 1, 6, 1, 12, 100, "light blue", desc);
+ add_nums(f, 140,18,25, 1, 12, 100, "light blue", desc);
+ pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+ add_nums(f, 95, 0, 60, 5, 60, 1, "pink", desc);
+
+#if 0
+ l = gtk_label_new("09:00\n9:00AM");
+ hour = 9;
+ minute = 0;
+ pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+ gtk_fixed_put(GTK_FIXED(f), l, 170,180);
+ time_label = l;
+#endif
+
+ add_button(&blist, "confirm", desc, clock_confirm);
+ add_button(&blist, "back", desc, cal_restore);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+ return v;
+}
+
+char *reasons[] = {"awake", "go", "meet", "buy", "call", "doctor", "dentist", "ok", "thanks", "coming"};
+struct list_entry_text *reason_entries;
+int selected_reason = -1;
+int num_reasons;
+
+struct list_entry *item(void *list, int n)
+{
+ if (n < num_reasons)
+ return &reason_entries[n].head;
+ else
+ return NULL;
+}
+void selected(void *list, int n)
+{
+ selected_reason = n;
+}
+
+struct list_handlers reason_han = {
+ .getitem = item,
+ .get_size = size,
+ .render = render,
+ .selected = selected,
+};
+
+void reason_back(void)
+{
+ gtk_container_remove(GTK_CONTAINER(window), reasonw);
+ gtk_container_add(GTK_CONTAINER(window), clockw);
+}
+
+void reason_confirm(void)
+{
+ struct event *ev;
+ GtkTextIter start, finish;
+
+ if (!active_event) {
+ ev = malloc(sizeof(*ev));
+ ev->next = evlist;
+ evlist = ev;
+ } else {
+ ev = active_event;
+ free(ev->mesg);
+ }
+ ev->recur = freq;
+ ev->when = ev->first = chosen_date + hour*3600 + minute*60;
+ gtk_text_buffer_get_bounds(reason_buffer, &start, &finish);
+ ev->mesg = gtk_text_buffer_get_text(reason_buffer,
+ &start, &finish, FALSE);
+ gtk_container_remove(GTK_CONTAINER(window), reasonw);
+ gtk_container_add(GTK_CONTAINER(window), cal);
+
+ event_save_all(filename, evlist);
+
+ active_event = ev;
+ alarm_date = 0; /* force refresh */
+ move_abort();
+}
+
+void reason_select(void)
+{
+ if (selected_reason < 0 ||
+ selected_reason >= num_reasons)
+ return;
+ gtk_text_buffer_delete_selection(reason_buffer, TRUE, TRUE);
+ gtk_text_buffer_insert_at_cursor(reason_buffer, " ", 1);
+ gtk_text_buffer_insert_at_cursor(reason_buffer, reasons[selected_reason],
+ strlen(reasons[selected_reason]));
+}
+
+GtkWidget *create_reason_window(void)
+{
+ /* The "reason" window allows the reason for the alarm
+ * to be set.
+ * It has:
+ * a label to show date and time
+ * a text editing widget containing the reason
+ * a selectable list of canned reasons
+ * buttons for: confirm select clear
+ *
+ * confirm adds the alarm, or not if the reason is clear
+ * select adds the selected canned reason to the editor
+ * clear clears the reason
+ */
+
+ PangoFontDescription *desc;
+ GtkWidget *v, *blist=NULL, *l;
+ struct sellist *sl;
+ int i, num;
+
+ desc = pango_font_description_new();
+ v = gtk_vbox_new(FALSE, 0);
+ gtk_widget_show(v);
+
+ l = gtk_label_new(" Date and Time ");
+ pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+ gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+ date_time_display = l;
+
+ l = gtk_text_view_new_with_buffer(reason_buffer);
+ gtk_widget_modify_font(l, desc);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(l), GTK_WRAP_WORD_CHAR);
+ gtk_widget_show(l);
+ gtk_container_add(GTK_CONTAINER(v), l);
+ gtk_text_buffer_set_text(reason_buffer, "Sample Reason", -1);
+
+ num = sizeof(reasons)/sizeof(reasons[0]);
+ reason_entries = calloc(num+1, sizeof(reason_entries[0]));
+ for (i=0; i<num; i++) {
+ reason_entries[i].text = reasons[i];
+ reason_entries[i].bg = "white";
+ reason_entries[i].fg = "blue";
+ reason_entries[i].underline = 0;
+ }
+ num_reasons = num;
+
+ sl = listsel_new(NULL, &reason_han);
+ pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+ gtk_widget_modify_font(sl->drawing, desc);
+ gtk_container_add(GTK_CONTAINER(v), sl->drawing);
+ gtk_widget_show(sl->drawing);
+
+ add_button(&blist, "confirm", desc, reason_confirm);
+ add_button(&blist, "select", desc, reason_select);
+ add_button(&blist, "back", desc, reason_back);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+ return v;
+}
+
+void add_time(int mins)
+{
+ time_t now;
+ char buf[30];
+ struct tm *tm;
+ delay += mins;
+ if (delay <= 0)
+ delay = 0;
+ now = time(0) + (delay?:1)*60;
+ tm = localtime(&now);
+ sprintf(buf, "+%d:%02d - ",
+ delay/60, delay%60);
+ strftime(buf+strlen(buf),
+ sizeof(buf)-strlen(buf),
+ "%H:%M", tm);
+ gtk_label_set_text(GTK_LABEL(timer_display), buf);
+
+}
+void add_time_60(void) { add_time(60); }
+void add_time_10(void) { add_time(10); }
+void add_time_1(void) { add_time(1); }
+void del_time_60(void) { add_time(-60); }
+void del_time_10(void) { add_time(-10); }
+void del_time_1(void) { add_time(-1); }
+
+int show_time(void *d)
+{
+ time_t now;
+ char buf[30];
+ struct tm *tm;
+ now = time(0);
+ tm = localtime(&now);
+ strftime(buf, sizeof(buf), "%H:%M:%S", tm);
+ gtk_label_set_text(GTK_LABEL(time_display), buf);
+}
+
+void to_cal(void)
+{
+ gtk_container_remove(GTK_CONTAINER(window), timerw);
+ g_source_remove(timer_tick);
+ gtk_container_add(GTK_CONTAINER(window), cal);
+}
+void set_timer(void)
+{
+ FILE *f = fopen("/data/alarms/timer", "a");
+ char buf[100];
+ time_t now = time(0) + delay*60;
+ struct tm *tm = localtime(&now);
+
+ strftime(buf, sizeof(buf), "%Y-%m-%d-%H-%M-%S::TIMER\n", tm);
+ if (f) {
+ fputs(buf, f);
+ fclose(f);
+ }
+ to_cal();
+}
+void clear_timers(void)
+{
+ unlink("/data/alarms/timer");
+ to_cal();
+}
+
+GtkWidget *create_timer_window(void)
+{
+ /* The timer window lets you set a one-shot alarm
+ * some number of minutes in the future.
+ * There are buttons for +1hr +10 +1 and - all those
+ * The timer window also shows a large digital clock with seconds
+ */
+
+ PangoFontDescription *desc;
+ GtkWidget *v, *blist, *l;
+
+ desc = pango_font_description_new();
+ v = gtk_vbox_new(FALSE, 0);
+ gtk_widget_show(v);
+
+ l = gtk_label_new(" Timer ");
+ pango_font_description_set_size(desc, 25 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+ gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+
+ l = gtk_label_new(" 99:99:99 ");
+ pango_font_description_set_size(desc, 40 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+ gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL);
+ time_display = l;
+
+ l = gtk_label_new(" ");
+ pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+ gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 1, NULL);
+ timers_list = l;
+
+
+ /* now from the bottom up */
+ blist = NULL;
+ pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+ add_button(&blist, "Return", desc, to_cal);
+ add_button(&blist, "Set", desc, set_timer);
+ add_button(&blist, "Clear All", desc, clear_timers);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+ blist = NULL;
+ pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+ add_button(&blist, "-1hr", desc, del_time_60);
+ add_button(&blist, "-10m", desc, del_time_10);
+ add_button(&blist, "-1m", desc, del_time_1);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+
+ l = gtk_label_new("+1:34 - 99:99 ");
+ pango_font_description_set_size(desc, 15 * PANGO_SCALE);
+ gtk_widget_modify_font(l, desc);
+ gtk_widget_show(l);
+
+ gtk_box_pack_end(GTK_BOX(v), l, FALSE, FALSE, 0);
+ timer_display = l;
+
+ blist = NULL;
+ pango_font_description_set_size(desc, 10 * PANGO_SCALE);
+ add_button(&blist, "+1hr", desc, add_time_60);
+ add_button(&blist, "+10m", desc, add_time_10);
+ add_button(&blist, "+1m", desc, add_time_1);
+ gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0);
+
+ return v;
+}
+
+main(int argc, char *argv[])
+{
+ GtkSettings *set;
+
+ gtk_set_locale();
+ gtk_init_check(&argc, &argv);
+
+ set = gtk_settings_get_default();
+ gtk_settings_set_long_property(set, "gtk-xft-dpi", 151 * PANGO_SCALE, "code");
+
+
+ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_default_size(GTK_WINDOW(window), 480, 640);
+ {
+ PangoContext *context;
+ PangoFontDescription *desc;
+ desc = pango_font_description_new();
+
+ pango_font_description_set_size(desc, 12 * PANGO_SCALE);
+ gtk_widget_modify_font(window, desc);
+ context = gtk_widget_get_pango_context(window);
+ layout = pango_layout_new(context);
+ }
+
+ reason_buffer = gtk_text_buffer_new(NULL);
+ cal = create_cal_window();
+ clockw = create_clock_window();
+ reasonw = create_reason_window();
+ timerw = create_timer_window();
+ gtk_container_add(GTK_CONTAINER(window), cal);
+ g_signal_connect((gpointer)window, "destroy",
+ G_CALLBACK(finish), NULL);
+ gtk_widget_show(window);
+ gtk_widget_ref(cal);
+ gtk_widget_ref(clockw);
+ gtk_widget_ref(reasonw);
+ gtk_widget_ref(timerw);
+ set_cal(time(0));
+
+
+
+ gtk_main();
+}
--- /dev/null
+/*
+ * selectable, auto-scrolling list widget
+ *
+ * We display a list of texts and allow one to be selected,
+ * normally with a tap.
+ * The selected text is highlighted and the list auto-scrolls to
+ * ensure it is not too close to the edge, so no other scroll
+ * function is needed.
+ *
+ * The list is defined by a function that takes a number
+ * and returns the element.
+ * This element will point to a table of functions that
+ * measure the size of the entry and render it. Some standard
+ * functions are provided to help with this.
+ * Each element has space to store current size/location for
+ * tap lookups.
+ */
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <string.h>
+#include <malloc.h>
+
+#include <gtk/gtk.h>
+
+#include "listsel.h"
+
+void calc_layout_no(struct sellist *list)
+{
+ /* choose an appropriate 'top', 'last' and cols
+ * set x,y,width,height on each entry in that range.
+ *
+ * Starting at 'sel' (or zero) we walk backwards
+ * towards 'top' assessing size.
+ * If we hit 'top' while in first 1/3 of display
+ * we target being just above bottom third
+ * If we don't reach 'top' while above bottom
+ * 1/3, then we target just below the top 1/3,
+ * having recorded where 'top' would be (When we first
+ * reached 1/3 from 'sel')
+ * Once we have chosen top, we move to end to find
+ * bottom. If we hit end before bottom,
+ * we need to move top backwards if possible.
+ *
+ * So:
+ * - walk back from 'sel' until find
+ * 'top' or distance above sel exceeds 2/3
+ * record location where distance was last < 1/3
+ * - if found top and distance < 1/3 and top not start,
+ * step back while that is still < 1/3
+ * - elif distance exceeds 2/3, set top et al to the
+ * found 1/3 position
+ * - move down from 'sel' until total distance is
+ * height, or hit end.
+ * - if hit end, move top backwards while total distance
+ * still less than height.
+ *
+ * columns add a complication as we don't know how
+ * many columns to use until we know the max width of
+ * the display choices.
+ * I think we assume cols based on selected entry,
+ * and as soon as we find something that decreases
+ * col count, start again from top.
+ *
+ * That doesn't work so well, particularly with multiple
+ * columns - we don't see the col-breaks properly.
+ * To see them, we really need to chose a firm starting
+ * place.
+ * So:
+ * Start with 'top' at beginning and walk along.
+ * If we hit end of list before bottom and top!=0,
+ * walk backwards from end above bottom to set top.
+ * If we find 'sel' after first 1/3 and before last - good.
+ * If before first third, set 'sel' above last third
+ * and walk back until find beginning or start, set top there.
+ * If after last third, set 'sel' past first third,
+ * walk back to top, set top there.
+ *
+ */
+ int top, last;
+ int sel;
+ int i;
+ int cols = 1, col, row;
+ struct list_entry *e;
+ int w, h;
+ int dist, pos, colwidth;
+ int altdist, alttop;
+
+ sel = list->selected;
+ if (sel < 0)
+ sel = 0;
+ e = list->han->getitem(list->list, sel);
+ if (e == NULL && sel > 0) {
+ sel = 0;
+ e = list->han->getitem(list->list, sel);
+ }
+ if (e == NULL)
+ /* list is empty */
+ return;
+
+ top = list->top;
+ if (top < 0)
+ top = 0;
+ if (top > sel)
+ top = sel;
+
+ retry_cols:
+ // did_scroll = 0;
+ retry_scroll:
+ /* lay things out from 'top' */
+ i = top; col = 0; row = 0;
+ while (col < cols) {
+ e = list->han->getitem(list->list, i);
+ if (e == NULL)
+ break;
+ list->han->get_size(e, &w, &h);
+ e->x = col * list->width / cols;
+ e->y = row;
+ e->width = list->width / cols;
+ row += h;
+ if (row > list->height) {
+ col++;
+ e->x = col * list->width / cols;
+ e->y = 0;
+ row = h;
+ }
+ e->need_draw = 1;
+ }
+
+ list->han->get_size(e, &w, &h);
+ cols = list->width / w;
+ if (cols == 0)
+ cols = 1;
+ col_retry:
+ dist = 0;
+ i = sel;
+ e = list->han->getitem(list->list, i);
+ list->han->get_size(e, &w, &h);
+ dist += h;
+
+ while (i > top && dist < (cols-1) * list->height + 2 * list->height/3) {
+ //printf("X i=%d dist=%d cols=%d lh=%d at=%d\n", i, dist, cols, list->height, alttop);
+ i--;
+ e = list->han->getitem(list->list, i);
+ list->han->get_size(e, &w, &h);
+ dist += h;
+ if (cols > 1 && w*cols > list->width) {
+ cols --;
+ goto col_retry;
+ }
+ if (dist * 3 < list->height) {
+ alttop = i;
+ altdist = dist;
+ }
+ }
+ if (i == top) {
+ if (dist*3 < list->height) {
+ /* try to move to other end */
+ while (i > 0 &&
+ dist < (cols-1)*list->height * list->height*2/3) {
+ i--;
+ e = list->han->getitem(list->list, i);
+ list->han->get_size(e, &w, &h);
+ dist += h;
+ if (cols > 1 && w*cols > list->width) {
+ cols--;
+ goto col_retry;
+ }
+ top = i;
+ }
+ }
+ } else {
+ /* too near bottom */
+ top = alttop;
+ dist = altdist;
+ }
+
+ i = sel;
+ e = list->han->getitem(list->list, i);
+ list->han->get_size(e, &w, &h);
+ while (dist + h <= list->height * cols) {
+ dist += h;
+ i++;
+ //printf("Y i=%d dist=%d cols=%d\n", i, dist, cols);
+ e = list->han->getitem(list->list, i);
+ if (e == NULL)
+ break;
+ list->han->get_size(e, &w, &h);
+ if (cols > 1 && w*cols > list->width) {
+ cols --;
+ goto col_retry;
+ }
+ }
+ if (e == NULL) {
+ /* near end, maybe move top up */
+ i = top;
+ while (i > 0) {
+ i--;
+ e = list->han->getitem(list->list, i);
+ list->han->get_size(e, &w, &h);
+ if (dist + h >= list->height * cols)
+ break;
+ dist += h;
+ if (cols > 1 && w * cols > list->width) {
+ cols--;
+ goto col_retry;
+ }
+ top = i;
+ }
+ }
+
+ /* OK, we are going with 'top' and 'cols'. */
+ list->cols = cols;
+ list->top = top;
+ list->selected = sel;
+ /* set x,y,width,height for each
+ * width and height are set - adjust width and set x,y
+ */
+ col = 0;
+ pos = 0;
+ i = top;
+ colwidth = list->width / cols;
+ last = top;
+ while (col < cols) {
+ e = list->han->getitem(list->list, i);
+ if (e == NULL)
+ break;
+ e->x = col * colwidth;
+ e->y = pos;
+ //printf("Set %d,%d %d,%d\n", e->x, e->y, e->height, list->height);
+ pos += e->height;
+ if (pos > list->height) {
+ pos = 0;
+ col++;
+ continue;
+ }
+ e->width = colwidth;
+ e->need_draw = 1;
+ last = i;
+ i++;
+ }
+ list->last = last;
+}
+
+void calc_layout(struct sellist *list)
+{
+ /* Choose 'top', and set 'last' and 'cols'
+ * so that 'selected' is visible in width/height,
+ * and is not in a 'bad' position.
+ * If 'sel' is in first column and ->y < height/3
+ * and 'top' is not zero, that is bad, we set top to zero.
+ * If 'sel' is in last column and ->Y > height/3
+ * and 'last' is not end of list, that is bad, we set
+ * top to 'last'
+ * If 'sel' is before 'top', that is bad, set 'top' to 'sel'.
+ * If 'sel' is after 'last', that is bad, set 'top' to 'sel'.
+ */
+ int top = list->top;
+ int sel = list->selected;
+ int cols = 10000;
+ int col, row, pos, last;
+ struct list_entry *sele;
+ int did_jump = 0;
+
+ retry_cols:
+ /* make sure 'top' and 'sel' are valid */
+ top = list->top;
+ sel = list->selected;
+ did_jump = 0;
+ if (top < 0 || list->han->getitem(list->list, top) == NULL)
+ top = 0;
+ if (sel < 0 || list->han->getitem(list->list, sel) == NULL)
+ sel = 0;
+
+
+ retry:
+ //printf("try with top=%d cols=%d\n", top, cols);
+ pos = top; col=0; row=0; last= -1;
+ sele = NULL;
+ while(1) {
+ int w, h;
+ struct list_entry *e;
+
+ e = list->han->getitem(list->list, pos);
+ if (e == NULL)
+ break;
+ if (pos == sel)
+ sele = e;
+ list->han->get_size(e, &w, &h);
+ if (cols > 1 && w * cols > list->width) {
+ /* too many columns */
+ cols = list->width / w;
+ if (cols <= 0)
+ cols = 1;
+ goto retry_cols;
+ }
+ e->x = col * list->width / cols;
+ e->y = row;
+ e->width = list->width / cols;
+ e->need_draw = 1;
+ row += h;
+ if (row > list->height && e->y > 0) {
+ col ++;
+ if (col == cols)
+ break;
+ e->x = col * list->width / cols;
+ e->y = 0;
+ row = h;
+ }
+ last = pos;
+ pos++;
+ }
+ /* Check if this is OK */
+ if (sele == NULL) {
+ //printf("no sel: %d %d\n", sel, top);
+ if (last <= top)
+ goto out;
+ if (sel < top)
+ top--;
+ else
+ top++;
+ goto retry;
+ }
+ //printf("x=%d y=%d hi=%d lh=%d top=%d lw=%d cols=%d\n",
+ // sele->x, sele->y, sele->height, list->height, top, list->width, cols);
+ if (sele->x == 0 && sele->y + sele->height < list->height / 3
+ && top > 0) {
+ if (did_jump)
+ top --;
+ else {
+ top = 0;
+ did_jump = 1;
+ }
+ goto retry;
+ }
+ if (sele->x == (cols-1) * list->width / cols &&
+ sele->y + sele->height > list->height * 2 / 3 &&
+ col == cols) {
+ if (did_jump)
+ top ++;
+ else {
+ top = last;
+ did_jump = 1;
+ }
+ goto retry;
+ }
+ if (col < cols && top > 0 &&
+ (col < cols - 1 || row < list->height / 2)) {
+ top --;
+ goto retry;
+ }
+ out:
+ list->top = top;
+ list->last = last;
+ list->cols = cols;
+ //printf("top=%d last=%d cols=%d\n", top, last, cols);
+}
+
+
+void configure(GtkWidget *draw, void *event, struct sellist *list)
+{
+ GtkAllocation alloc;
+
+ gtk_widget_get_allocation(draw, &alloc);
+#if 0
+ if (list->width == alloc.width &&
+ list->height == alloc.height)
+ return;
+#endif
+ list->width = alloc.width;
+ list->height = alloc.height;
+ calc_layout(list);
+ gtk_widget_queue_draw(draw);
+}
+
+void expose(GtkWidget *draw, GdkEventExpose *event, struct sellist *list)
+{
+ int i;
+ /* Draw all fields in the event->area */
+
+ for (i = list->top; i <= list->last; i++) {
+ struct list_entry *e = list->han->getitem(list->list, i);
+ if (e->x > event->area.x + event->area.width)
+ /* to the right */
+ continue;
+ if (e->x + e->width < event->area.x)
+ /* to the left */
+ continue;
+ if (e->y > event->area.y + event->area.height)
+ /* below */
+ continue;
+ if (e->y + e->height < event->area.y)
+ /* above */
+ continue;
+
+ e->need_draw = 1;
+ if (event->count == 0 && e->need_draw == 1) {
+ e->need_draw = 0;
+ list->han->render(e, i == list->selected, list->drawing);
+ }
+ }
+}
+
+void tap(GtkWidget *draw, GdkEventButton *event, struct sellist *list)
+{
+ int i;
+ for (i=list->top; i <= list->last; i++) {
+ struct list_entry *e = list->han->getitem(list->list, i);
+ if (event->x >= e->x &&
+ event->x < e->x + e->width &&
+ event->y >= e->y &&
+ event->y < e->y + e->height)
+ {
+ //printf("found item %d\n", i);
+ list->selected = i;
+ calc_layout(list);
+ gtk_widget_queue_draw(list->drawing);
+ list->han->selected(list, i);
+ break;
+ }
+ }
+}
+
+void *listsel_new(void *list, struct list_handlers *han)
+{
+ struct sellist *rv;
+
+ rv = malloc(sizeof(*rv));
+ rv->list = list;
+ rv->han = han;
+ rv->drawing = gtk_drawing_area_new();
+ rv->top = 0;
+ rv->selected = -1;
+ rv->width = rv->height = 0;
+ rv->cols = 1;
+
+ g_signal_connect((gpointer)rv->drawing, "expose-event",
+ G_CALLBACK(expose), rv);
+ g_signal_connect((gpointer)rv->drawing, "configure-event",
+ G_CALLBACK(configure), rv);
+
+ gtk_widget_add_events(rv->drawing,
+ GDK_BUTTON_PRESS_MASK|
+ GDK_BUTTON_RELEASE_MASK);
+ g_signal_connect((gpointer)rv->drawing, "button_release_event",
+ G_CALLBACK(tap), rv);
+
+ return rv;
+}
+
+
+#ifdef MAIN
+
+struct list_entry_text *entries;
+int entcnt;
+PangoFontDescription *fd;
+PangoLayout *layout;
+GdkGC *colour;
+
+/*
+ * gdk_color_parse("green", GdkColor *col);
+ * gdk_colormap_alloc_color(GdkColormap, GdkColor, writeable?, bestmatch?)
+ * gdk_colormap_get_system
+ *
+ * gdk_gc_new(drawable)
+ * gdk_gc_set_foreground(gc, color)
+ * gdk_gc_set_background(gc, color)
+ * gdk_gc_set_rgb_fg_color(gc, color)
+ */
+struct list_entry *item(void *list, int n)
+{
+ if (n < entcnt)
+ return &entries[n].head;
+ else
+ return NULL;
+}
+int size(struct list_entry *i, int *width, int*height)
+{
+ PangoRectangle ink, log;
+ struct list_entry_text *item = (void*)i;
+
+ if (i->height) {
+ *width = item->true_width;
+ *height = i->height;
+ return 0;
+ }
+ //printf("calc for %s\n", item->text);
+ pango_layout_set_text(layout, item->text, -1);
+ pango_layout_get_extents(layout, &ink, &log);
+ //printf("%s %d %d\n", item->text, log.width, log.height);
+ *width = log.width / PANGO_SCALE;
+ *height = log.height / PANGO_SCALE;
+ item->true_width = i->width = *width;
+ i->height = *height;
+ return 0;
+}
+
+int render(struct list_entry *i, int selected, GtkWidget *d)
+{
+ PangoRectangle ink, log;
+ struct list_entry_text *item = (void*)i;
+ int x;
+ GdkColor col;
+
+ pango_layout_set_text(layout, item->text, -1);
+
+ if (colour == NULL) {
+ colour = gdk_gc_new(gtk_widget_get_window(d));
+ gdk_color_parse("purple", &col);
+ gdk_gc_set_rgb_fg_color(colour, &col);
+ }
+ if (selected) {
+ gdk_color_parse("pink", &col);
+ gdk_gc_set_rgb_fg_color(colour, &col);
+ gdk_draw_rectangle(gtk_widget_get_window(d),
+ colour, TRUE,
+ item->head.x, item->head.y,
+ item->head.width, item->head.height);
+ gdk_color_parse("purple", &col);
+ gdk_gc_set_rgb_fg_color(colour, &col);
+ }
+ x = (i->width - item->true_width)/2;
+ if (x < 0)
+ x = 0;
+ gdk_draw_layout(gtk_widget_get_window(d),
+ colour,
+ item->head.x+x, item->head.y, layout);
+ return 0;
+}
+void selected(void *list, int n)
+{
+ printf("got %d\n", n);
+}
+
+struct list_handlers myhan = {
+ .getitem = item,
+ .get_size = size,
+ .render = render,
+ .selected = selected,
+};
+
+main(int argc, char *argv[])
+{
+ int i;
+ GtkWidget *w;
+ struct sellist *l;
+ PangoContext *context;
+
+ entries = malloc(sizeof(*entries) * argc);
+ memset(entries, 0, sizeof(*entries)*argc);
+ for (i=1; i<argc; i++) {
+ entries[i-1].text = argv[i];
+ entries[i-1].bg = "white";
+ entries[i-1].fg = "blue";
+ entries[i-1].underline = 0;
+ }
+ entcnt = i-1;
+
+ gtk_set_locale();
+ gtk_init_check(&argc, &argv);
+
+ fd = pango_font_description_new();
+ pango_font_description_set_size(fd, 35 * PANGO_SCALE);
+
+ w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_default_size(GTK_WINDOW(w), 480, 640);
+
+ gtk_widget_modify_font(w, fd);
+ context = gtk_widget_get_pango_context(w);
+ layout = pango_layout_new(context);
+
+ l = listsel_new(NULL, &myhan);
+ gtk_container_add(GTK_CONTAINER(w), l->drawing);
+ gtk_widget_show(l->drawing);
+ gtk_widget_show(w);
+
+ gtk_main();
+}
+#endif
--- /dev/null
+
+struct sellist {
+ void *list;
+ struct list_handlers *han;
+ GtkWidget *drawing;
+
+ int top; /* Index of first element displayed */
+ int selected; /* Index of currently selected element */
+ int last; /* Index of last displayed element */
+
+ int width, height; /* Pixel size of widget */
+ int cols; /* Columns */
+};
+
+struct list_handlers {
+ struct list_entry *(*getitem)(void *list, int n);
+ int (*get_size)(struct list_entry *item, int *width, int *height);
+ int (*render)(struct list_entry *item, int selected,
+ GtkWidget *d);
+ void (*selected)(void *list, int element);
+};
+
+struct list_entry {
+ int x, y, width, height;
+ int need_draw;
+};
+
+struct list_entry_text {
+ struct list_entry head;
+ char *text;
+
+ int true_width;
+ char *bg, *fg;
+ int underline;
+};
+
+extern void *listsel_new(void *list, struct list_handlers *han);
+
+
--- /dev/null
+/*
+ * wkalrm.c - Use the RTC alarm to wake us up
+ *
+ * Copyright (C) 2008 by OpenMoko, Inc.
+ * Written by Werner Almesberger <werner@openmoko.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <linux/rtc.h>
+
+
+#define DEFAULT_RTC "/dev/rtc0"
+
+
+static const char *device = DEFAULT_RTC;
+static int fd;
+
+
+/* ----- Low-level wrappers ------------------------------------------------ */
+
+
+static void read_alarm(struct rtc_wkalrm *alarm)
+{
+ int res;
+
+ res = ioctl(fd, RTC_WKALM_RD, alarm);
+ if (res < 0) {
+ perror("ioctl(RTC_WKALM_RD)");
+ exit(1);
+ }
+}
+
+
+static void read_time(struct rtc_time *tm)
+{
+ int res;
+
+ res = ioctl(fd, RTC_RD_TIME, tm);
+ if (res < 0) {
+ perror("ioctl(RTC_RD_TIME)");
+ exit(1);
+ }
+}
+
+
+static void write_alarm(const struct rtc_wkalrm *alarm)
+{
+ int res;
+
+ res = ioctl(fd, RTC_WKALM_SET, alarm);
+ if (res < 0) {
+ perror("ioctl(RTC_WKALM_SET)");
+ exit(1);
+ }
+}
+
+
+/* ----- Date conversions -------------------------------------------------- */
+
+
+static void show_alarm(void)
+{
+ struct rtc_wkalrm alarm;
+ struct rtc_time tm;
+
+ read_time(&tm);
+ printf("time is %02d:%02d:%02d %04d-%02d-%02d\n",
+ tm.tm_hour, tm.tm_min, tm.tm_sec,
+ tm.tm_year+1900, tm.tm_mon+1,
+ tm.tm_mday);
+
+
+ read_alarm(&alarm);
+ if (!alarm.enabled)
+ printf("alarm disabled%s\n",
+ alarm.pending ? " (pending)" : "");
+ else
+ printf("%02d:%02d:%02d %04d-%02d-%02d%s\n",
+ alarm.time.tm_hour, alarm.time.tm_min, alarm.time.tm_sec,
+ alarm.time.tm_year+1900, alarm.time.tm_mon+1,
+ alarm.time.tm_mday,
+ alarm.pending ? " (pending)" : "");
+}
+
+
+static void set_alarm_abs(const char *t, const char *day)
+{
+ fprintf(stderr, "not yet implemented :-)\n");
+ exit(1);
+}
+
+
+static void set_alarm_delta(time_t delta)
+{
+ struct rtc_wkalrm alarm;
+ struct tm tm, *tmp;
+ time_t t;
+
+ read_time(&alarm.time);
+ memset(&tm, 0, sizeof(tm));
+ tm.tm_sec = alarm.time.tm_sec;
+ tm.tm_min = alarm.time.tm_min;
+ tm.tm_hour = alarm.time.tm_hour;
+ tm.tm_mday = alarm.time.tm_mday;
+ tm.tm_mon = alarm.time.tm_mon;
+ tm.tm_year = alarm.time.tm_year;
+ tm.tm_isdst = -1;
+ t = mktime(&tm);
+ if (t == (time_t) -1) {
+ fprintf(stderr, "mktime: error\n");
+ exit(1);
+ }
+ t += delta;
+ tmp = localtime(&t);
+ if (!tmp) {
+ fprintf(stderr, "localtime_r: error\n");
+ exit(1);
+ }
+ alarm.time.tm_sec = tmp->tm_sec;
+ alarm.time.tm_min = tmp->tm_min;
+ alarm.time.tm_hour = tmp->tm_hour;
+ alarm.time.tm_mday = tmp->tm_mday;
+ alarm.time.tm_mon = tmp->tm_mon;
+ alarm.time.tm_year = tmp->tm_year;
+ alarm.enabled = 1;
+ write_alarm(&alarm);
+}
+
+
+static void set_alarm_rel(const char *delta)
+{
+ unsigned long n;
+ char *end;
+
+ n = strtoul(delta, &end, 10);
+ if (!strcmp(end, "d") || !strcmp(end, "day") || !strcmp(end, "days"))
+ n *= 24*3600;
+ else if (!strcmp(end, "h") || !strcmp(end, "hour") ||
+ !strcmp(end, "hours"))
+ n *= 3600;
+ else if (!strcmp(end, "m") || !strcmp(end, "min") ||
+ !strcmp(end, "mins"))
+ n *= 60;
+ else if (strcmp(end, "s") && strcmp(end, "sec") &&
+ strcmp(end, "secs")) {
+ fprintf(stderr, "invalid delta time \"%s\"\n", delta);
+ exit(1);
+ }
+ set_alarm_delta(n);
+}
+
+
+static void disable_alarm(void)
+{
+ struct rtc_wkalrm alarm;
+
+ read_alarm(&alarm);
+ alarm.enabled = 0;
+ write_alarm(&alarm);
+}
+
+
+static void set_alarm_24h(const char *t)
+{
+ fprintf(stderr, "not yet implemented :-)\n");
+ exit(1);
+}
+
+
+static void set_alarm(const char *when)
+{
+ if (*when == '+')
+ set_alarm_rel(when+1);
+ else
+ set_alarm_24h(when);
+}
+
+
+/* ----- Command line parsing ---------------------------------------------- */
+
+
+static void usage(const char *name)
+{
+ fprintf(stderr,
+"usage: %s [-d device]\n"
+" %s [-d device] hh:mm[:ss] [[yyyy-]mm-dd]\n"
+" %s [-d device] +Nunit\n\n"
+" unit is d[ay[s]], h[our[s]] m[in[s]], or s[ec[s]]\n\n"
+" -d device open the specified RTC device (default: %s)\n"
+ , name, name, name, DEFAULT_RTC);
+ exit(1);
+}
+
+
+int main(int argc, char **argv)
+{
+ int c;
+
+ while ((c = getopt(argc, argv, "d:")) != EOF)
+ switch (c) {
+ case 'd':
+ device = optarg;
+ break;
+ default:
+ usage(*argv);
+ }
+
+ fd = open(device, O_RDWR);
+ if (fd < 0) {
+ perror(device);
+ exit(1);
+ }
+
+ switch (argc-optind) {
+ case 0:
+ show_alarm();
+ break;
+ case 1:
+ if (!strcmp(argv[optind], "off"))
+ disable_alarm();
+ else
+ set_alarm(argv[optind]);
+ break;
+ case 2:
+ set_alarm_abs(argv[optind], argv[optind+1]);
+ break;
+ default:
+ usage(*argv);
+ }
+ return 0;
+}
--- /dev/null
+
+#
+# Contacts DB interface for various programs
+# Currently only understand name/number and speed-dials
+# Separately we also provide access to log of incoming and
+# outgoing calls, mapped to names through the contacts DB.
+#
+# Need:
+# list of "speed-dials". These are ordered
+# look up name/number from speed-dial
+# map 'name' to entry - first match, with '.'s
+# map 'number' to probable name
+# load/save address book
+# load /var/log/incoming and /var/log/outgoing
+#
+
+import os, time
+
+class entry:
+ def __init__(self, name, num = None):
+ # if 'num' is None, then 'name' contains "name;num;..."
+ if num == None:
+ a = name.strip().split(';')
+ if len(a) < 2:
+ raise ValueError;
+ name = a[0]
+ num = a[1]
+ self.name = name
+ self.num = num
+ self.speed = None
+
+ def is_speed(self):
+ return len(self.name) == 1
+
+ def is_deleted(self):
+ return self.name[:8] == '!deleted'
+
+ def match_type(self, patn):
+ # patn might match:
+ # 1: start of name
+ # 2: start of word in name
+ # 3: somewhere in name
+ # 4: somewhere in num
+ p = patn.lower()
+ n = name.lower()
+ l = len(p)
+ if n[0:l] == p:
+ return 1
+ if n.find(' '+p) >= 0:
+ return 2
+ if n.find(p) >= 0:
+ return 3
+ if self.num.find(p):
+ return 4
+ return -1
+
+ def same_num(self, num):
+ l = len(num)
+ if l < 4:
+ # too short to be at all useful
+ return False
+ # 8 is enough to identify
+ if l > 8:
+ l = 8
+ return len(self.num) >= l and self.num[-l:] == num[-l:]
+
+ def __cmp__(self, other):
+ if self.speed and not other.speed:
+ return -1
+ if other.speed and not self.speed:
+ return 1
+ return cmp(self.name, other.name)
+
+class contacts:
+ def __init__(self):
+ try:
+ self.file = '/data/address-book'
+ self.load()
+ except:
+ self.file = '/home/neilb/home/mobile-numbers-jan-08'
+ self.load()
+
+ def load(self):
+ self.list = []
+ self.deleted = []
+ self.speed = {}
+ speed = {}
+ f = open(self.file)
+ for l in f:
+ e = entry(l)
+ if e.is_speed():
+ speed[e.num] = e.name
+ elif e.is_deleted():
+ self.deleted.append(e)
+ else:
+ self.list.append(e)
+ if speed:
+ for i in range(len(self.list)):
+ if self.list[i].name in speed:
+ self.speed[speed[self.list[i].name]] = self.list[i]
+ self.list[i].speed = speed[self.list[i].name]
+ self.resort()
+
+ def resort(self):
+ self.list.sort()
+ self.deleted.sort()
+
+ def save(self):
+ f = open(self.file + '.new', 'w')
+ for e in self.list:
+ f.write(e.name+';'+e.num+';\n')
+ if e.speed:
+ f.write(e.speed+';'+e.name+';\n')
+ for e in self.deleted:
+ f.write(e.name+';'+e.num+';\n')
+ f.close()
+ os.rename(self.file+'.new', self.file)
+
+ def add(self, name, num, speed = None):
+ c = entry(name, num)
+ self.list.append(c)
+ if speed:
+ c.speed = speed
+ self.speed[speed] = c
+ self.resort()
+
+ def undelete(self, ind):
+ e = self.deleted[ind]
+ del self.deleted[ind]
+ s = e.name
+ if s[:8] == '!deleted':
+ p = s.find('-')
+ if p <= 0:
+ p = 7
+ e.name = s[p+1:]
+ self.list.append(e)
+ self.resort()
+ if e.speed:
+ self.speed[e.speed] = e
+
+ def delete(self, ind):
+ e = self.list[ind]
+ del self.list[ind]
+ n = time.strftime('!deleted.%Y.%m.%d-') + e.name
+ e.name = n
+ self.deleted.append(e)
+ self.resort()
+
+ def find_num(self, num):
+ for e in self.list:
+ if e.same_num(num):
+ return e
+ return None
+
+if __name__ == "__main__":
+ c = contacts()
+ print c.speed.keys()
--- /dev/null
+#!/usr/bin/env python
+
+#
+# Contacts manager
+# Currently a 'contact' is a name, a number, and a speed-dial letter
+#
+# We have a content pane and a row of buttons at the bottom.
+# The content is either:
+# - list of contact names - highlighted minipane at bottom with number
+# - detailed contact info that can be editted (name, number, speed-pos)
+#
+# When list of contacts are displayed, typing a char adds that char to
+# a substring and only contacts containing that substring are listed.
+# An extra entry at the start is given which matches exactly the substring
+# and can be used to create a new entry.
+# Alternately, the list can show only 'deleted' entries, still with substring
+# matching. Colour is different in this case.
+#
+# Buttons for contact list are:
+# When nothing selected (list_none):
+# New ALL Undelete
+# When contact selected (list_sel):
+# Call SMS Open ALL
+# When new-entry selected (list_new):
+# Create ALL
+# When viewing deleted entries and nothing or first is selected (del_none):
+# Return
+# When viewing deleted entries and one is selected (del_sel):
+# Undelete Open Return
+#
+# When the detailed contact info is displayed all fields can be
+# edited and change background colour if they differ from stored value
+# Fields are: Name Number
+# Button for details are:
+# When nothing is changed (edit_nil):
+# Call SMS Close Delete
+# When something is changed on new entry (edit_new)
+# Discard Create
+# When something is changed on old entry (edit old)
+# Restore Save Create
+#
+# 'delete' doesn't actually delete, but adds '!delete$DATE-' to the name which
+# causes most lookups to ignore the entry.
+#
+# TODO
+# - find way to properly reset 'current' pos after edit
+# - have a global 'state' object which other things refer to
+# It has an 'updated' state which other objects can connect to
+# - save file after an update
+
+import gtk, pango, time, gobject, os
+from scrawl import Scrawl
+from listselect import ListSelect
+from contactdb import contacts
+
+def strip_prefix(num):
+ if num[:2] == "02":
+ return num[2:]
+ if num[:2] == "04":
+ return num[1:]
+ if num[:4] == "+612":
+ return num[4:]
+ if num[:4] == "+614":
+ return num[3:]
+ return num
+
+def match_num(num, str):
+ """ 'num' is a phone number, and 'str' is a search string
+ We want to allow matches that ignore the country/local prefix
+ which might be "02" or "+612" or something else in other countries.
+ But I'm not in other countries, so just strip those if present.
+ """
+ if num.find(str) >= 0:
+ return True
+ if "+612".find(str) == 0:
+ return True
+ if "02".find(str) == 0:
+ return True
+ return strip_prefix(num).find(strip_prefix(str)) == 0
+
+class Contacts(gtk.Window):
+ def __init__(self):
+ gtk.Window.__init__(self)
+ self.set_default_size(480,640)
+ self.set_title("Contacts")
+ self.connect('destroy', self.close_win)
+
+ self.current = None
+ self.timer = None
+ self.make_ui()
+ self.load_book()
+ self.show()
+ self.voice_cb = None
+ self.sms_cb = None
+ self.undeleting = False
+ self.watch_clip('contact-find')
+
+ def make_ui(self):
+ # UI consists of:
+ # list of contacts -or-
+ # editable field
+ # -and-
+ # variable list of buttons.
+ #
+ ctx = self.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(35*pango.SCALE)
+ self.fd = fd
+
+ v = gtk.VBox(); v.show()
+ self.add(v)
+
+ s = self.listui()
+ self.lst = s
+ v.pack_start(s, expand=True)
+ s.show()
+ self.show()
+ self.sc.set_colour('red')
+
+ s = self.editui()
+ v.pack_start(s, expand = True)
+ s.hide()
+ self.ed = s
+
+ bv = gtk.VBox(); bv.show(); v.pack_start(bv, expand=False)
+ def hide_some(w):
+ for c in w.get_children():
+ c.hide()
+ bv.hide_some = lambda : hide_some(bv)
+ self.buttons = bv
+
+ b = self.buttonlist(bv)
+ self.button(b, 'New', self.open)
+ self.button(b, 'Undelete', self.undelete)
+ self.button(b, 'ALL', self.all)
+ self.list_none = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Call', self.call)
+ self.button(b, 'SMS', self.sms)
+ self.button(b, 'Open', self.open)
+ self.button(b, 'Delete', self.delete)
+ self.list_sel = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Create', self.open)
+ self.button(b, 'ALL', self.all)
+ self.list_new = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Return', self.all)
+ self.del_none = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Undelete', self.delete)
+ self.button(b, 'Open', self.open)
+ self.button(b, 'Return', self.all)
+ self.del_sel = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Call', self.call)
+ self.button(b, 'SMS', self.sms)
+ self.button(b, 'Close', self.close)
+ self.button(b, 'Delete', self.delete)
+ self.edit_nil = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Discard', self.close)
+ self.button(b, 'Create', self.create)
+ self.edit_new = b
+
+ b = self.buttonlist(bv)
+ self.button(b, 'Restore', self.open)
+ self.button(b, 'Save', self.save)
+ self.button(b, 'Create', self.create)
+ self.edit_old = b
+
+ self.list_none.show()
+
+ def listui(self):
+ s = ListSelect(markup=True); s.show()
+ s.set_format("normal","black", background="grey", selected="white")
+ s.set_format("deleted","red", background="grey", selected="white")
+ s.set_format("virtual","blue", background="grey", selected="white")
+ s.connect('selected', self.selected)
+ s.set_zoom(37)
+ self.clist = contact_list()
+ s.list = self.clist
+ s.list_changed()
+ self.sel = s
+ def gottap(p):
+ x,y = p
+ s.tap(x,y)
+ self.sc = Scrawl(s, self.gotsym, gottap, None, None)
+
+ s.set_events(s.get_events() | gtk.gdk.KEY_PRESS_MASK)
+ def key(list, ev):
+ if len(ev.string) == 1:
+ self.gotsym(ev.string)
+ elif ev.keyval == 65288:
+ self.gotsym('<BS>')
+ else:
+ #print ev.keyval, len(ev.string), ev.string
+ pass
+ s.connect('key_press_event', key)
+ s.set_flags(gtk.CAN_FOCUS)
+ s.grab_focus()
+
+ v = gtk.VBox(); v.show()
+ v.pack_start(s, expand=True)
+ l = gtk.Label('')
+ l.modify_font(self.fd)
+ self.number_label = l
+ v.pack_start(l, expand=False)
+ return v
+
+ def editui(self):
+ ui = gtk.VBox()
+
+ self.fields = {}
+ self.field(ui, 'Name')
+ self.field(ui, 'Number')
+ self.field(ui, 'Speed Number')
+ return ui
+
+ def field(self, v, lbl):
+ h = gtk.HBox(); h.show()
+ l = gtk.Label(lbl); l.show()
+ l.modify_font(self.fd)
+ h.pack_start(l, expand=False)
+ e = gtk.Entry(); e.show()
+ e.modify_font(self.fd)
+ h.pack_start(e, expand=True)
+ e.connect('changed', self.field_update, lbl)
+ v.pack_start(h, expand=True, fill=True)
+ self.fields[lbl] = e
+ return e
+
+ def buttonlist(self, v):
+ b = gtk.HBox()
+ b.set_homogeneous(True)
+ b.hide()
+ v.pack_end(b, expand=False)
+ return b
+
+ def button(self, bl, label, func):
+ b = gtk.Button()
+ if label[0:3] == "gtk" :
+ img = gtk.image_new_from_stock(label, self.isize)
+ img.show()
+ b.add(img)
+ else:
+ b.set_label(label)
+ b.child.modify_font(self.fd)
+ b.show()
+ b.connect('clicked', func)
+ b.set_focus_on_click(False)
+ bl.pack_start(b, expand = True)
+
+ def close_win(self, widget):
+ gtk.main_quit()
+
+ def selected(self, list, item):
+ self.buttons.hide_some()
+
+ if item == None:
+ item = -1
+ if self.undeleting:
+ if item < 0 or (item == 0 and self.clist.str != ''):
+ self.del_none.show()
+ else:
+ self.del_sel.show()
+ self.current = self.clist.getitem(item)
+ elif item < 0:
+ self.list_none.show()
+ self.number_label.hide()
+ self.current = None
+ elif item == 0 and self.clist.str != '':
+ self.list_new.show()
+ self.number_label.hide()
+ self.current = None
+ else:
+ self.list_sel.show()
+ i = self.clist[item]
+ self.current = self.clist.getitem(item)
+ if i == None:
+ self.number_label.hide()
+ else:
+ self.number_label.set_text(self.clist.list[self.current].num)
+ self.number_label.show()
+
+ def field_update(self, ev, fld):
+ if self.flds[fld] == self.fields[fld].get_text():
+ self.fields[fld].modify_base(gtk.STATE_NORMAL,None)
+ else:
+ self.fields[fld].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('yellow'))
+
+ same = True
+ for i in ['Name', 'Number', 'Speed Number']:
+ if self.fields[i].get_text() != self.flds[i]:
+ same = False
+ self.buttons.hide_some()
+ if self.current == None:
+ self.edit_new.show()
+ elif same:
+ self.edit_nil.show()
+ else:
+ self.edit_old.show()
+ pass
+
+ def load_book(self):
+ self.book = contacts()
+ self.clist.set_list(self.book.list)
+
+ def queue_save(self):
+ if self.timer:
+ gobject.source_remove(self.timer)
+ self.timer = gobject.timeout_add(15*1000, self.do_save)
+ def do_save(self):
+ if self.timer:
+ gobject.source_remove(self.timer)
+ self.timer = None
+ self.book.save()
+
+ def gotsym(self,sym):
+ if sym == '<BS>':
+ s = self.clist.str[:-1]
+ elif len(sym) > 1:
+ s = self.clist.str
+ elif ord(sym) >= 32:
+ s = self.clist.str + sym
+ else:
+ return
+ self.clist.set_str(s)
+ self.sel.list_changed()
+ self.sel.select(None)
+
+ def watch_clip(self, board):
+ self.cb = gtk.Clipboard(selection=board)
+ self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+ self.got_clip(self.cb, None)
+ self.cb.set_with_data(self.targets,
+ self.get_clip, self.got_clip, None)
+
+
+ def got_clip(self, clipb, data):
+ s = clipb.wait_for_text()
+ if not s:
+ print 'not s'
+ return
+ print 'got', s
+ self.clist.set_str(s)
+ self.lst.show()
+ self.sel.grab_focus()
+ self.ed.hide()
+ self.sel.list_changed()
+ self.sel.select(None)
+ self.cb.set_with_data(self.targets, self.get_clip, self.got_clip, None)
+ self.present()
+
+ def get_clip(self, sel, seldata, info, data):
+ sel.set_text("Contact Please")
+
+ def open(self, ev):
+ self.lst.hide()
+ self.ed.show()
+
+ self.buttons.hide_some()
+ if self.current == None:
+ self.edit_new.show()
+ else:
+ self.edit_nil.show()
+ self.flds = {}
+ self.flds['Name'] = ''
+ self.flds['Number'] = ''
+ self.flds['Speed Number'] = ''
+ if self.current != None:
+ current = self.clist.list[self.current]
+ self.flds['Name'] = current.name
+ self.flds['Number'] = current.num
+ self.flds['Speed Number'] = current.speed
+ if current.speed == None:
+ self.flds['Speed Number'] = ""
+ elif self.clist.str:
+ num = True
+ for d in self.clist.str:
+ if not d.isdigit() and d != '+':
+ num = False
+ if num:
+ self.flds['Number'] = self.clist.str
+ else:
+ self.flds['Name'] = self.clist.str
+
+ for f in ['Name', 'Number', 'Speed Number'] :
+ c = self.flds[f]
+ if not c:
+ c = ""
+ self.fields[f].set_text(c)
+ self.fields[f].modify_base(gtk.STATE_NORMAL,None)
+
+ def all(self, ev):
+ self.clist.set_str('')
+ self.undeleting = False
+ self.current = None
+ self.clist.set_list(self.book.list)
+ self.sel.select(None)
+ self.sel.list_changed()
+ pass
+ def create(self, ev):
+ self.save(None)
+ def save(self,ev):
+ name = self.fields['Name'].get_text()
+ num = self.fields['Number'].get_text()
+ speeds = self.fields['Speed Number'].get_text()
+ speed = speeds
+ if speed == "":
+ speed = None
+ if name == '' or name.find(';') >= 0:
+ self.fields['Name'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
+ return
+ if num == '' or num.find(';') >= 0:
+ self.fields['Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
+ return
+ if len(speeds) > 1 or speeds.find(';') >= 0:
+ self.fields['Speed Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink'))
+ return
+ if self.current == None or ev == None:
+ self.book.add(name, num, speed)
+ else:
+ current = self.book.list[self.current]
+ current.name = name
+ current.num = num
+ current.speed = speed
+ self.flds['Name'] = name
+ self.flds['Number'] = num
+ self.flds['Speed Number'] = speeds
+ self.book.resort()
+ self.clist.set_list(self.book.list)
+ self.sel.list_changed()
+ self.close(ev)
+ self.queue_save()
+
+ def delete(self, ev):
+ if self.current != None:
+ if self.undeleting:
+ self.book.undelete(self.current)
+ self.clist.set_list(self.book.deleted)
+ else:
+ self.book.delete(self.current)
+ self.clist.set_list(self.book.list)
+ self.sel.list_changed()
+ self.close(ev)
+ self.queue_save()
+ pass
+
+ def undelete(self, ev):
+ self.undeleting = True
+ self.clist.set_str('')
+ self.clist.set_list(self.book.deleted)
+ self.current = None
+ self.sel.list_changed()
+ self.sel.select(None)
+ pass
+ def call(self, ev):
+ if not self.voice_cb:
+ self.voice_cb = gtk.Clipboard(selection='voice-dial')
+ self.call_str = self.clist.list[self.current].num
+ self.voice_cb.set_with_data([ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ],
+ self.call_getdata, self.call_cleardata, None)
+ def call_getdata(self, clipb, sel, info, data):
+ sel.set_text(self.call_str)
+ def call_cleardata(self, clipb, data):
+ self.call_str = ""
+
+ def sms(self, ev):
+ if not self.sms_cb:
+ self.sms_cb = gtk.Clipboard(selection='sms-new')
+ self.sms_str = self.clist.list[self.current].num
+ self.sms_cb.set_with_data([ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
+ self.sms_get_data, self.sms_cleardata, None)
+ def sms_get_data(self, clipb, sel, info, data):
+ sel.set_text(self.sms_str)
+ def sms_cleardata(self, clipb, data):
+ self.sms_str = ""
+
+
+ def close(self, ev):
+ self.lst.show()
+ self.sel.grab_focus()
+ self.ed.hide()
+ if self.current != None and self.current >= len(self.clist):
+ self.current = None
+ self.selected(None, self.sel.selected)
+
+class contact_list:
+ def __init__(self):
+ self.set_list([])
+ def set_list(self, list):
+ self.list = list
+ self.match_start = []
+ self.match_word = []
+ self.match_any = []
+ self.match_str = ''
+ self.total = 0
+ self.str = ''
+
+ def set_str(self,str):
+ self.str = str
+
+ def recalc(self):
+ if self.str == self.match_str:
+ return
+ if self.match_str != '' and self.str[:len(self.match_str)] == self.match_str:
+ # str is a bit longer
+ self.recalc_quick()
+ return
+ self.match_start = []
+ self.match_word = []
+ self.match_any = []
+ self.match_str = self.str
+ s = self.str.lower()
+ l = len(self.str)
+ for i in range(len(self.list)):
+ name = self.list[i].name.lower()
+ if name[0:l] == s:
+ self.match_start.append(i)
+ elif name.find(' '+s) >= 0:
+ self.match_word.append(i)
+ elif name.find(s) >= 0 or match_num(self.list[i].num, s):
+ self.match_any.append(i)
+ self.total = len(self.match_start) + len(self.match_word) + len(self.match_any)
+
+ def recalc_quick(self):
+ # The string has been extended so we only look through what we already have
+ self.match_str = self.str
+ s = self.str.lower()
+ l = len(self.str)
+
+ lst = self.match_start
+ self.match_start = []
+ for i in lst:
+ name = self.list[i].name.lower()
+ if name[0:l] == s:
+ self.match_start.append(i)
+ else:
+ self.match_word.append(i)
+ self.match_word.sort()
+ lst = self.match_word
+ self.match_word = []
+ for i in lst:
+ name = self.list[i].name.lower()
+ if name.find(' '+s) >= 0:
+ self.match_word.append(i)
+ else:
+ self.match_any.append(i)
+ self.match_any.sort()
+ lst = self.match_any
+ self.match_any = []
+ for i in lst:
+ name = self.list[i].name.lower()
+ if name.find(s) >= 0 or match_num(self.list[i].num, s):
+ self.match_any.append(i)
+ self.total = len(self.match_start) + len(self.match_word) + len(self.match_any)
+
+ def __len__(self):
+ self.recalc()
+ if self.str == '':
+ return len(self.list)
+ else:
+ return self.total + 1
+ def getitem(self, ind):
+ if ind < 0:
+ print '!!!!', ind
+ if self.str == '':
+ return ind
+
+ if ind == 0:
+ return -1
+ ind -= 1
+ if ind < len(self.match_start):
+ return self.match_start[ind]
+ ind -= len(self.match_start)
+ if ind < len(self.match_word):
+ return self.match_word[ind]
+ ind -= len(self.match_word)
+ if ind < len(self.match_any):
+ return self.match_any[ind]
+ return None
+
+ def __getitem__(self, ind):
+
+ i = self.getitem(ind)
+ if i < 0:
+ return (protect(self.str), 'virtual')
+ if i == None:
+ return None
+ s = self.list[i].name
+ n = self.list[i].num
+ s = protect(s)
+ n = protect(n)
+ if s[:8] == '!deleted':
+ p = s.find('-')
+ if p <= 0:
+ p = 7
+ return (s[p+1:],'deleted')
+ else:
+ return (s + '<span color="blue" size="15000">\n '+ n + '</span>', 'normal')
+
+def protect(txt):
+ txt = txt.replace('&', '&')
+ txt = txt.replace('<', '<')
+ txt = txt.replace('>', '>')
+ return txt
+
+if __name__ == "__main__":
+
+ c = Contacts()
+ gtk.main()
+
--- /dev/null
+#!/bin/bash
+
+
+gpspipe -r -n 30 | {
+sum=0
+cnt=0
+while IFS=, read x t d m y r;
+ do [ $x = '$GPZDA' ] || continue
+ [ -z "$t" ] && continue
+ now=`date +%s`
+ hr=${t:0:2}
+ mn=${t:2:2}
+ sc=${t:4:2}
+
+ then=`date --utc --date="$y-$m-$d $hr:$mn:$sc" +%s`
+ sum=$[sum+then-now]
+ cnt=$[cnt+1]
+done
+if [ $cnt -gt 0 ]
+then
+ diff=$[sum/cnt]
+ if [ $diff -lt -4 -o $diff -gt 4 ]
+ then echo "Change by $diff seconds"
+ date --set "+$diff seconds"
+ hwclock -w
+ else
+ echo "Diff is $diff - no change"
+ fi
+else
+ echo "No time found"
+fi
+}
+
--- /dev/null
+#!/bin/bash
+
+case $1 in
+ */* )
+ if cmp -s /etc/localtime /usr/share/zoneinfo/$1
+ then : localtime is OK
+ else : echo Copying to localtime
+ cp /usr/share/zoneinfo/$1 /etc/localtime
+ fi
+ if [ `cat /etc/timezone` != $1 ]
+ then : echo Setting /etc/timezone
+ echo $1 > /etc/timezone
+ fi
+ exit 0
+ ;;
+ --list ) ;;
+ * ) echo >&2 Usage: gpstz [--list] zone/name
+ exit 1
+esac
+
+gpspipe -r -n 20 | grep GPGGA | while IFS=, read a tm lat NS long EW etc
+ do
+ long=${long%.*} lat=${lat%.*}
+ case $NS in
+ N) lat=+$lat;;
+ S) lat=-$lat;;
+ esac
+ case $EW in
+ E) long=+$long ;;
+ W) long=-$long ;;
+ esac
+ # echo $lat $long
+ mind=9999999999
+ while read country loc tz desc
+ do
+ case $country in
+ \#* ) continue;;
+ esac
+ case $loc in
+ [-+][0-9][0-9][0-9][0-9][-+][0-9][0-9][0-9][0-9][0-9] )
+ tlat=${loc%??????}
+ tlat=${tlat#?}
+ tlat=${tlat#0}
+ tlat=${tlat#0}
+ tlat=${tlat#0}
+ tlat=${loc%??????????}$tlat
+ tlong=${loc#?????}
+ slong=${tlong%?????}
+ tlong=${tlong#?}
+ tlong=${tlong#0}
+ tlong=${tlong#0}
+ tlong=$slong${tlong#0}
+ ;;
+ * ) continue
+ esac
+ # echo $tz at $tlat $tlong
+ x=$[long-tlong] y=$[lat-tlat]
+ d=$[x*x+y*y]
+ echo $d $tz
+ done < /usr/share/zoneinfo/zone.tab
+ break
+ done | sort -n | sed 10q
--- /dev/null
+#!/usr/bin/python
+#
+# ubx packet generator
+#
+# v0.2
+#
+# Wilfried Klaebe <wk-openmoko@chaos.in-kiel.de>
+# NeilBrown <neilb@suse.de>
+#
+# Usage:
+#
+# ubxgen.py 06 13 04 00 01 00 00 00 > packet.ubx
+#
+# prepends 0xb5 0x62 header,
+# appends checksum,
+# outputs binary packet to stdout
+#
+# Numbers can be given in decimal with a suffix 'dN' where
+# 'N' is the number of bytes. These are converted in little-endian
+# The value 'L' can be given which is th 2-byte length
+# of the rest of the message
+#
+# you can send the packet to GPS chip like this:
+#
+# cat packet.ubx > /dev/ttySAC1
+
+import sys
+import binascii
+
+cs0=0
+cs1=0
+
+sys.stdout.write("\xb5\x62")
+
+outbuf = []
+leng = None
+
+for d in sys.argv[1:]:
+ if d == 'L':
+ leng = len(outbuf)
+ outbuf.append(0)
+ outbuf.append(0)
+ elif 'd' in d:
+ p = d.index('d')
+ bytes = int(d[p+1:])
+ d = int(d[0:p])
+ while bytes > 0:
+ b = d % 256
+ d = int(d/256)
+ outbuf.append(b)
+ bytes -= 1
+ else:
+ c = binascii.unhexlify(d)
+ outbuf.append(ord(c))
+
+if leng != None:
+ l = len(outbuf) - (leng + 2)
+ outbuf[leng] = l % 256
+ outbuf[leng+1] = int(l/256)
+
+for c in outbuf:
+ sys.stdout.write(chr(c))
+ cs0 += c
+ cs0 &= 255
+ cs1 += cs0
+ cs1 &= 255
+
+sys.stdout.write(chr(cs0)+chr(cs1))
--- /dev/null
+Thu Aug 30 22:30:11 2012 state becomes flight
+Thu Aug 30 22:30:11 2012 check flightmode got 1
+Thu Aug 30 22:30:11 2012 advance flight chooses 0, 0
+Thu Aug 30 22:30:11 2012 send AT command +CFUN=0 2000
+Traceback (most recent call last):
+ File "/usr/local/bin/gsmd", line 859, in <module>
+ a = GsmD('/dev/ttyHS_Application')
+ File "/usr/local/bin/gsmd", line 648, in __init__
+ self.advance()
+ File "/usr/local/bin/gsmd", line 746, in advance
+ control[self.gstate][t].start(self)
+ File "/usr/local/bin/gsmd", line 106, in start
+ self.advance(channel)
+ File "/usr/local/bin/gsmd", line 170, in advance
+ channel.atcmd(at)
+ File "/usr/local/bin/atchan.py", line 114, in atcmd
+ self.sock.write('AT' + cmd + '\r')
+AttributeError: 'NoneType' object has no attribute 'write'
--- /dev/null
+Sun Dec 23 10:36:08 2012 send AT command +CPAS 2000
+Sun Dec 23 10:36:08 2012 received AT response +CPAS: 4
+Sun Dec 23 10:36:08 2012 call_status got +CPAS: 4
+Sun Dec 23 10:36:08 2012 s = 4
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:08 2012 call_status got OK
+Sun Dec 23 10:36:08 2012 advance on-call chooses 0, 2000
+Sun Dec 23 10:36:08 2012 Sleeping for 2.000000 seconds
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:08 2012 received AT response +CPAS: 4
+Sun Dec 23 10:36:08 2012 received AT response OK
+Sun Dec 23 10:36:09 2012 Check call got 0415836820
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 5
+Sun Dec 23 10:36:10 2012 Sleeping for 0.005000 seconds
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 5
+Sun Dec 23 10:36:10 2012 Sleeping for 0.005000 seconds
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 4
+Sun Dec 23 10:36:10 2012 Sleeping for 0.004000 seconds
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 advance on-call chooses 2, 0
+Sun Dec 23 10:36:10 2012 send AT command +CPAS 2000
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 send AT command 2000
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 send AT command 2000
+Sun Dec 23 10:36:10 2012 Timer Fired
+Sun Dec 23 10:36:10 2012 send AT command 2000
+Sun Dec 23 10:36:10 2012 received AT response +CPAS: 4
+Sun Dec 23 10:36:10 2012 received AT response OK
+Sun Dec 23 10:36:10 2012 call_status got OK
+Sun Dec 23 10:36:10 2012 send AT command +CPAS 2000
+:
--- /dev/null
+
+#
+# Handle a connection to an AT device via /dev/ttyXX
+#
+# We directly support high level commands (reset_modem
+# etc) but don't know anything about AT commands - we just send them
+# through and hand back reply. Replies also go via a callback
+# We also provide timeout support, but someone else needs to tell us
+# when to set a timeout, and when to clear it.
+#
+# This is usually subclassed by code with an agenda.
+
+import gobject, sys, os, time
+import termios
+from tracing import log
+from socket import *
+
+class AtChannel:
+ def __init__(self, path):
+ self.path = path
+ self.connected = False
+ self.watcher = None
+ self.sock = None
+ self.buf = ""
+ self.linelist = []
+
+ self.pending = False
+ self.timer = None
+
+ def disconnect(self):
+ if self.watcher:
+ gobject.source_remove(self.watcher)
+ self.watcher = None
+ if self.sock:
+ self.sock.close()
+ self.sock = None
+ self.connected = False
+
+ def connect(self):
+ if self.sock != None:
+ return
+ log("connect to", self.path)
+ s = open(self.path,"r+")
+ #s.setblocking(0)
+ fd = s.fileno()
+ attr = termios.tcgetattr(fd)
+ attr[3] = attr[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
+ attr[6][termios.VMIN] = 0
+ attr[6][termios.VTIME] = 0
+ termios.tcsetattr(fd, termios.TCSANOW, attr)
+
+ self.watcher = gobject.io_add_watch(s, gobject.IO_IN, self.readdata)
+ self.sock = s
+ self.connected = True
+
+ def readdata(self, io, arg):
+ try:
+ r = self.sock.read(1000)
+ except IOError:
+ # no data there really.
+ return True
+ if not r:
+ # pipe closed
+ if io != None:
+ self.getline(None)
+ return False
+ r = self.buf + r
+ ra = r.split('\n')
+ self.buf = ra[-1];
+ del ra[-1]
+ for ln in ra:
+ ln = ln.strip('\r')
+ self.getline(ln)
+ # FIXME this should be configurable
+ if self.buf == '> ':
+ self.getline(self.buf)
+ self.buf = ''
+ return True
+
+ def getline(self, line):
+ if line == None:
+ log("received EOF")
+ self.takeline(line)
+ if self.pending:
+ self.pending = False
+ gobject.source_remove(self.timer)
+ self.timer = None
+ return
+ if len(line):
+ log("received AT response", line)
+ if self.takeline(line):
+ if self.pending:
+ self.pending = False
+ gobject.source_remove(self.timer)
+ self.timer = None
+
+ def atcmd(self, cmd, timeout = 2000):
+ """
+ Send the command, preceeded by 'AT' and set a timeout.
+ self.takeline() should return True when the command
+ has been responded to, otherwise we will call
+ self.timedout() after the time.
+ """
+ self.set_timeout(timeout)
+ log("send AT command", cmd, timeout)
+ try:
+ self.sock.write('AT' + cmd + '\r')
+ self.sock.flush()
+ except IOError:
+ self.cancel_timeout()
+ self.set_timeout(10)
+
+ def timer_fired(self):
+ log("Timer Fired")
+ self.pending = False
+ self.timer = None
+ self.timedout()
+ return False
+
+ def set_timeout(self, delay):
+ if self.pending:
+ raise ValueError
+ self.timer = gobject.timeout_add(delay, self.timer_fired)
+ self.pending = True
+
+ def cancel_timeout(self):
+ if self.pending:
+ gobject.source_remove(self.timer)
+ self.pending = False
+
+ def abort_timeout(self):
+ if self.pending:
+ self.cancel_timeout()
+ self.set_timeout(0)
+
+ # these are likely to be over-ridden by a child class
+ def takeline(self, line):
+ self.linelist.append(line)
+
+ def wait_line(self, timeout):
+ self.cancel_timeout()
+ self.set_timeout(timeout)
+ if len(self.linelist) == 0:
+ self.readdata(None, None)
+ c = gobject.main_context_default()
+ while not self.linelist and self.pending:
+ c.iteration()
+ if self.linelist:
+ self.cancel_timeout()
+ l = self.linelist[0]
+ del self.linelist[0]
+ return l
+ else:
+ return None
+ def timedout(self):
+ pass
+
+
+ def chat(self, mesg, resp, timeout = 1000):
+ """
+ Send the message (if not 'None') and wait up to
+ 'timeout' for one of the responses (regexp)
+ Return None on timeout, or number of response.
+ combined with an array of the messages received.
+ """
+ if mesg:
+ log("send command", mesg)
+ try:
+ self.sock.write(mesg + '\r\n')
+ self.sock.flush()
+ except error:
+ timeout = 10
+
+ conv = []
+ while True:
+ l = self.wait_line(timeout)
+ if l == None:
+ return (None, conv)
+ conv.append(l)
+ for i in range(len(resp)):
+ ptn = resp[i]
+ if type(ptn) == str:
+ if ptn == l.strip():
+ return (i, conv)
+ else:
+ if resp[i].match(l):
+ return (i, conv)
+
+ def chat1(self, mesg, resp, timeout=1000):
+ n,c = self.chat(mesg, resp, timeout = timeout)
+ return n
+
+
+def found(list, patn):
+ """
+ see if patn can be found in the list of strings
+ """
+ for l in list:
+ l = l.strip()
+ if type(patn) == str:
+ if l == patn:
+ return True
+ else:
+ p = patn.match(l)
+ if p:
+ return p
+ return False
+
--- /dev/null
+#!/usr/bin/env python
+
+# get list of currently available carriers from Option 3G
+#
+from atchan import AtChannel, found
+import sys, re
+
+chan = AtChannel(path='/dev/ttyHS_Control')
+chan.connect()
+
+if chan.chat1('ATE0', ['OK','ERROR']) != 0:
+ sys.exit(1)
+
+n,c = chan.chat('AT+COPS=?', ['OK','ERROR'], timeout=45000)
+if n == None and len(c) == 0:
+ # need to poke to get a response
+ n,c = chan.chat('', ['OK','ERROR'], timeout=10000)
+if n == None and len(c) == 0:
+ # need to poke to get a response
+ n,c = chan.chat('', ['OK','ERROR'], timeout=10000)
+
+if n != 0:
+ sys.exit(1)
+
+
+m = found(c, re.compile('^\+COPS: (.*)'))
+if m:
+ clist = re.findall('\((\d+),"([^"]*)","([^"]*)","([^"]*)",(\d+)\)', m.group(1))
+ for (mode, long, short, num, type) in clist:
+ print num, type, '"%s" "%s"' % (short, long)
+
--- /dev/null
+#!/usr/bin/env python
+
+# establish data connection on Option 3G module.
+#
+# AT+CGDCONT=1,"IP","${APN}"
+# OK
+# AT_OWANCALL=1,1,1
+# OK
+# _OWANCALL: 1, 1
+# AT_OWANDATA?
+# _OWANDATA: 1, 10.6.177.182, 0.0.0.0, 211.29.132.12, 61.88.88.88, 0.0.0.0, 0.0.0.0,144000
+# ifconfig hso0 10.6.177.182
+# route add default dev hso0
+# echo nameserver 211.29.132.12 > /etc/resolv.conf
+# echo nameserver 61.88.88.88 >> /etc/resolv.conf
+#
+# ...but....
+# we might already be connected, and we might not know, and just
+# stopping/starting doesn't seem to do much - no notification.
+# so:
+# - first check status. If up, configure and done
+# - if not, enable and wait.
+# - on timeout, check status again.
+
+#APN="exetel1"
+APN="INTERNET"
+import atchan, sys, re, os
+
+def check_status(chan):
+ n,c = chan.chat('AT_OWANDATA?', ['OK','ERROR'])
+ want = re.compile('_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$')
+ if n == 0:
+ m = atchan.found(c, want)
+ if n != 0 or not m:
+ return False
+ return m
+
+def configure(m):
+ g = m.groups()
+ ip = g[0]
+ ns1= g[1]
+ ns2= g[2]
+ print ip, ns1, ns2
+ os.system("/sbin/ifconfig hso0 %s" % ip)
+ os.system('route add default dev hso0')
+ f = open("/etc/resolv.conf", "w")
+ f.write("nameserver %s\n" % ns1)
+ f.write("nameserver %s\n" % ns2)
+ f.close()
+
+def disconnect(chan):
+ n,c = chan.chat('AT_OWANCALL=1,0,0', ['OK','ERROR'])
+
+chan = atchan.AtChannel(path="/dev/ttyHS_Control")
+chan.connect()
+
+chan.chat1('ATE0', ['OK','ERROR'])
+if chan.chat1('AT+CGDCONT=1,"IP","%s"' % APN, ['OK','ERROR']) != 0:
+ print 'Could not set APN'
+ sys.exit(1)
+
+m = check_status(chan)
+
+if sys.argv[1] == 'status':
+ if m:
+ print "Active: ", m.groups()[0]
+ else:
+ print "inactive"
+ sys.exit(0)
+if sys.argv[1] == "off":
+ if m:
+ disconnect(chan)
+ else:
+ print 'DATA already disconnected'
+ os.system('route delete default dev hso0')
+ os.system('/sbin/ifconfig hso0 down')
+ sys.exit(0)
+
+if m:
+ print 'already active'
+ configure(m)
+ sys.exit(0)
+
+want = re.compile('^_OWANCALL: *\d+, *(\d*)$')
+
+if chan.chat1('AT_OWANCALL=1,1,1', ['OK','ERROR']) != 0:
+ print 'Could not start data connection'
+ sys.exit(1)
+l = chan.wait_line(10000)
+if l == None:
+ print 'No response for DATA connect'
+else:
+ m = want.match(l)
+ if m and m.groups()[0] == '1':
+ print 'Connected'
+ else:
+ print 'Connect failed'
+ sys.exit(1)
+
+m = check_status(chan)
+if m:
+ configure(m)
+else:
+ print 'Sorry, could not connect'
+ #disconnect(chan)
--- /dev/null
+#!/usr/bin/env python
+# coding=UTF-8
+
+# Collect SMS messages from the GSM device.
+# We store a list of messages that are thought to be
+# in the SIM card: number from date
+# e.g. 17 61403xxxxxx 09/02/17,20:28:36+44
+# As we read messages, if we find one that is not in that list,
+# we record it in the SMS store, then update the list
+#
+# An option can specify either 'new' or 'all.
+# 'new' will only ask for 'REC UNREAD' and so will be faster and so
+# is appropriate when we know that a new message has arrived.
+# 'all' reads all messages and so is appropriate for an occasional
+# 'sync' like when first turning the phone on.
+#
+# If we discover that the SMS card is more than half full, we
+# deleted the oldest messages.
+# We discover this by 'all' finding lots of messages, or 'new'
+# finding a message with a high index.
+# For now, we "know" that the SIM card can hold 30 messages.
+#
+# We need to be careful about long messages. A multi-part message
+# looks like e.g.
+#+CMGL: 19,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:46+44",145,140
+#0500031C0201A8E8F41C949E83C2207B599E07B1DFEE33A85D9ECFC3E732888E0ED34165FCB85C26CF41747419344FBBCFEC32A85D9ECFC3E732889D6EA7E9A0F91B444787E9A024681C7683E6E532E88E0ED341E939485E1E97D3F6321914A683E8E832E84D4797E5A0B29B0C7ABB41ED3CC8282F9741F2BADB5D96BB40D7329B0D9AD3D36C36887E2FBBE9
+#+CMGL: 20,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:47+44",145,30
+#0500031C0202F2A0B71C347F83D8653ABD2C9F83E86FD0F90D72B95C2E17
+
+# what about:
+# 0281F001000081000019C9531BF4AED3E769721944479741F4F7DD0D4287D96C
+# 02 81F0 ??
+# 010000810000
+# 19 (25 bytes)
+# C9531BF4AED3E769721944479741F4F7DD0D4287D96C
+# I'm outside the town hall
+# This is a saved message.
+#
+# If that was just hex you could use
+# perl -e 'print pack("H*","050....")'
+# to print it.. but no...
+# Looks like it decodes as:
+# 05 - length of header, not including this byte
+# 00 - concatentated SMS with 8 bit ref number (08 means 16 bit ref number)
+# 03 - length of rest of header
+# 1C - ref number for this concat-SMS
+# 02 - number of parts in this SMS
+# 01 - number of this part - counting starts from 1
+# A8E8F41C949E83C22.... message, 7 bits per char. so:
+# A8 - 54 *2 + 0 54 == T 1010100 0 1010100
+# 0E8 - 68 *2 + 0 68 == h 1 1101000 1101000
+# F4 - 69 == i 11 110100 1101001 1
+# 1C 73 == s 000 11100 1110011 11
+# 94 20 == space 1001 0100 0100000 000
+# 9E 69 == i 10011 110 1101001 1001
+# 83 73 == s 100000 11 1110011 10011
+# 20 == space 0100000 0100000
+
+# 153 characters in first message. 19*8 + 1
+# that uses 19*7+1 == 134 octets
+# There are 6 in the header so a total of 140
+# second message has 27 letters - 3*8+3
+# That requires 3*7+3 == 24 octets. 30 with the 6 octet header.
+
+# then there are VCARD messages that look lie e.g.
+#+CMGL: 2,"REC READ","61403xxxxxx",,"09/01/29,13:01:26+44",145,137
+#06050423F40000424547494E3A56434152440D0A56455253494F4E3A322E310D0A4E3A....0D0A454E443A56434152440D0A
+#which is
+#06050423F40000
+#then
+#BEGIN:VCARD
+#VERSION:2.1
+#N: ...
+#...
+#END:VCARD
+# The 06050423f40000
+# might decode like:
+# 06 - length of rest of header
+# 05 - magic code meaning 'user data'
+# 04 - length of rest of header...
+# 23 -
+# f4 - destination port '23f4' means 'vcard'
+# 00 -
+# 00 - 0000 is the origin port.
+#
+#in hex/ascii
+#
+# For now, ignore anything longer than the specified length.
+
+import os
+import suspend
+#os.environ['PYTRACE'] = '1'
+
+import atchan, sys, re
+from storesms import SMSmesg, SMSstore
+
+
+def load_mirror(filename):
+ # load an array of index address date
+ # from the file and store in a hash
+ rv = {}
+ try:
+ f = file(filename)
+ except IOError:
+ return rv
+ l = f.readline()
+ while l:
+ fields = l.strip().split(None, 1)
+ rv[fields[0]] = fields[1]
+ l = f.readline()
+ return rv
+
+def save_mirror(filename, hash):
+ n = filename + '.new'
+ f = open(n, 'w')
+ for i in hash:
+ f.write(i + ' ' + hash[i] + '\n')
+ f.close()
+ os.rename(n, filename)
+
+# GSM uses a 7-bit code that is not the same as ASCII...
+# -*- coding: utf8 -*-
+gsm = (u"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?"
+ u"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà ")
+ext = (u"````````````````````^```````````````````{}`````\\````````````[~]`"
+ u"|````````````````````````````````````€``````````````````````````")
+
+# Take a unicode string and produce a byte string for GSM
+def gsm_encode(plaintext):
+ res = ""
+ for c in plaintext:
+ idx = gsm.find(c);
+ if idx != -1:
+ res += chr(idx)
+ continue
+ idx = ext.find(c)
+ if idx != -1:
+ res += chr(27)
+ res += chr(idx)
+ return res
+# take a GSM byte string (7-in-8 conversion already done) and produce unicode
+def gsm_decode(code):
+ uni = u''
+ esc = False
+ for c in code:
+ n = ord(c)
+ if esc:
+ uni += ext[n]
+ esc = False
+ elif n == 27:
+ esc = True
+ else:
+ uni += gsm[n]
+ return uni
+
+
+def sms_decode(msg,pos = 1):
+ #msg is a 7-in-8 encoding of a longer message.
+ carry = 0
+ str = ''
+ while msg:
+ c = msg[0:2]
+ msg = msg[2:]
+ b = int(c, 16)
+ if pos == 0:
+ if carry:
+ str += chr(carry + (b&1)*64)
+ carry = 0
+ b /= 2
+ else:
+ b = (b << (pos-1)) | carry
+ carry = (b & 0xff80) >> 7
+ b &= 0x7f
+ if (b & 0x7f) != 0:
+ str += chr(b&0x7f)
+ pos = (pos+1) % 7
+ return gsm_decode(str)
+
+def sms_unicode_decode(msg):
+ # 2bytes unicode numbers - 4 syms each
+ m = u''
+ for i in range(len(msg)/4):
+ c = int(msg[i*4:i*4+4],16)
+ m += unichr(c)
+ return m
+
+def cvt_telnum(type, msg, len):
+ if type == '81' or type == '91':
+ n = ''
+ for i in range(len):
+ n += msg[i + 1 - (i%2)*2]
+ if type == '91':
+ return '+' + n
+ else:
+ return n
+ if type == 'D0':
+ return sms_decode(msg)
+ return "?" + type + msg
+
+def cvt_date(msg):
+ #YYMMDDHHMMSSZZ -> 20YY/MM/DD HH:MM:SS+ZZ swapping letters
+ sep='0//,::+'
+ dt = '2'
+ for i in range(len(msg)/2):
+ dt += sep[i] + msg[i*2+1] + msg[i*2]
+ return dt
+
+
+def main():
+ mode = 'all'
+ for a in sys.argv[1:]:
+ if a == '-n':
+ mode = 'new'
+ else:
+ print "Unknown option:", a
+ sys.exit(1)
+
+ pth = None
+ for p in ['/data','/media/card','/var/tmp']:
+ if os.path.exists(os.path.join(p,'SMS')):
+ pth = p
+ break
+
+ if not pth:
+ print "Cannot find SMS directory"
+ sys.exit(1)
+
+ dir = os.path.join(pth, 'SMS')
+ print "Using ", dir
+ store = SMSstore(dir)
+
+ chan = atchan.AtChannel(path = '/dev/ttyHS_Control')
+ chan.connect()
+
+ # consume any extra 'OK' that might be present
+ chan.chat1('', ['OK', 'ERROR']);
+ if chan.chat1('ATE0', ['OK', 'ERROR']) != 0:
+ sys.exit(1)
+
+ # get ID of SIM card
+ n,c = chan.chat('AT+CIMI', ['OK', 'ERROR'])
+ CIMI='unknown'
+ for l in c:
+ l = l.strip()
+ if re.match('^\d+$', l):
+ CIMI = l
+
+ mfile = os.path.join(dir, '.sim-mirror-'+CIMI)
+ #FIXME lock mirror file
+ mirror = load_mirror(mfile)
+
+ chan.chat('AT+CMGF=0', ['OK','ERROR'])
+ if mode == 'new':
+ chan.atcmd('+CMGL=0')
+ else:
+ chan.atcmd('+CMGL=4')
+
+ # reading the msg list might fail for some reason, so
+ # we always prime the mirror list with the previous version
+ # and only replace things, never assume they aren't there
+ # because we cannot see them
+ newmirror = mirror
+ mirror_seen = {}
+
+ l = ''
+ state = 'waiting'
+ msg = ''
+ # text format
+ # indx state from name date type len
+ #+CMGL: 40,"REC READ","+61406022084",,"12/03/14,18:00:40+44"
+ # PDU MODE
+ #+CMGL: index, 0-4, unread read unsent sent all, ?? , byte len after header
+ #+CMGL: 3,1,,40
+ #07911614786007F0 040B911654999946F100002120217035534417CE729ACD02BDD7203A3AEC5ECF5D2062D9ED4ECF01
+ # 61450000641F000012021207533544
+ # 07 is length (octets) of header (911614786007F0)
+ # 91 is SMSC number type; 81 is local? 91 is international D0 is textual D0456C915A6402 == EXETEL
+ # 1614786007F0 is the SMSC number: 61418706700
+ #
+ # 04 is "SMS-DELIVER" and some other details 44 == ?? 24??
+ # 0B is length of sende number (11)
+ # 91 is type as above
+ # 1654999946F1 is number: 61459999641 (F is padding)
+ # 00 is protocol id - always 0 ??
+ # 00 is Data coding scheme. 00 is 7-bit default
+ # 21202170355344 is stime stamp: 12/02/12 07:35:53+44
+ # 17 is length of body (23)
+ # CE729ACD02BDD7203A3AEC5ECF5D2062D9ED4ECF01 is 7-in-8 message
+ #
+ # other coding schemes:
+ # 08 is 16 bit unicode
+ # 11 is VCARD: 06050400E20080 C2E231E9D459874129B1A170EA886176B9A1A016993A182D26B3E164B919AEA1283A893AEB3028253614
+ # looks like 7-in-8 with a 7/8 header
+ # or can be just a message.
+ # or ??? (message from AUST POST)
+ # 01 is much like 00 ??
+
+
+ want = re.compile('^\+CMGL: (\d+),(\d+),("[^"]*")?,(\d+)$')
+
+ found_one = False
+ while state == 'reading' or not (l[0:2] == 'OK' or l[0:5] == 'ERROR' or
+ l[0:10] == '+CMS ERROR'):
+ l = chan.wait_line(1000)
+ if l == None:
+ sys.exit(1)
+ print "Got (",state,")", l
+ if state == 'reading':
+ msg = l
+ if designation != '0' and designation != '1':
+ #send, not recv
+ state = 'waiting'
+ continue
+ if len(msg) >= msg_len:
+ state = 'waiting'
+ hlen = int(msg[0:2], 16)
+ hdr = msg[2:(2+hlen*2)]
+ # hdr is the sending number - don't care
+ msg = msg[2+hlen*2:]
+ # typ == 04 - SMS-DELIVER
+ typ = int(msg[0:2],16)
+ nlen = int(msg[2:4], 16)
+ ntype = msg[4:6]
+ numlen = (nlen + nlen % 2)
+ sender = cvt_telnum(ntype, msg[6:6+numlen], nlen)
+ msg = msg[6+numlen:]
+ proto = msg[0:2]
+ coding = msg[2:4]
+ date = cvt_date(msg[4:18])
+ bdy_len = int(msg[18:20], 16)
+ body = msg[20:]
+ ref = None; part = None
+
+ if body[0:6] == '050003':
+ # concatenated message with 8bit ref number
+ ref = body[6:8]
+ part = ( int(body[10:12],16), int(body[8:10], 16))
+ if coding == '08':
+ txt = sms_unicode_decode(body)
+ else:
+ txt = sms_decode(body[12:],0)
+ elif body[0:6] == '060504':
+ # VCARD ??
+ txt = sms_decode(body[14:])
+ elif coding == '00':
+ txt = sms_decode(body)
+ elif coding == '11':
+ txt = sms_decode(body)
+ elif coding == '01':
+ txt = sms_decode(body)
+ elif coding == '08':
+ txt = sms_unicode_decode(body)
+ else:
+ print "ignoring", index, sender, date, body
+ continue
+ if ref == None:
+ print "found", index, sender, date, txt.encode('utf-8')
+ else:
+ print "found", index, ref, part, sender, date, repr(txt)
+
+ if index in mirror and mirror[index] == date[2:] + ' ' + sender:
+ print "Already have that one"
+ else:
+ sms = SMSmesg(source='GSM', time=date[2:], sender=sender,
+ text = txt.encode('utf-8'), state = 'NEW',
+ ref= ref, part = part)
+ store.store(sms)
+ found_one = True
+ newmirror[index] = date[2:] + ' ' + sender
+ mirror_seen[index] = date[2:] + ' ' + sender
+ else:
+ m = want.match(l)
+ if m:
+ g = m.groups()
+ index = g[0]
+ designation = g[1]
+ msg_len = int(g[3], 10)
+ msg = ''
+ state = 'reading'
+
+ mirror = newmirror
+
+ if len(mirror) > 10:
+ rev = {}
+ dlist = []
+ for i in mirror:
+ rev[mirror[i]+' '+str(i)] = i
+ dlist.append(mirror[i]+' '+str(i))
+ dlist.sort()
+ for i in range(len(mirror) - 10):
+ dt=dlist[i]
+ ind = rev[dt]
+ print 'del', i, dt, ind
+ resp = chan.chat1('AT+CMGD=%s' % ind, ['OK', 'ERROR', '+CMS ERROR'],
+ timeout=3000)
+ if resp == 0 or ind not in mirror_seen:
+ del mirror[ind]
+
+ save_mirror(mfile, mirror)
+ if found_one:
+ try:
+ f = open("/var/run/alert/sms", "w")
+ f.write("new")
+ f.close()
+ suspend.abort_cycle()
+ except:
+ pass
+ if mode == 'new' and not found_one:
+ sys.exit(1)
+ sys.exit(0)
+
+main()
--- /dev/null
+#!/usr/bin/env python
+
+# Send an SMS message using GSM.
+# Args are:
+# sender - ignored
+# recipient - phone number
+# message - no newlines
+#
+# We simply connect to the GSM module,
+# check for a registration
+# and
+# AT+CMGS="recipient"
+# > message
+# > control-Z
+#
+# Sending multipart sms messages:
+# ref: http://www.developershome.com/sms/cmgsCommand4.asp
+# 1/ set PDU mode with AT+CMGF=0
+# 2/ split message into 153-char bundles
+# 3/ create messages as follows:
+#
+# 00 - this says we aren't providing an SMSC number
+# 41 - TPDU header - type is SMS-SUBMIT, user-data header present
+# 00 - please assign a message reference number
+# xx - length in digits of phone number
+# 91 for IDD, 81 for "don't know what sort of number this is"
+# 164030... BCD phone number, nibble-swapped, pad with F at end if needed
+# 00 - protocol identifier??
+# 00 - encoding - 7 bit ascii
+# XX - length of message in septets
+# Then the message which starts with 7 septets of header that looks like 6 octets.
+# 05 - length of rest of header
+# 00 - multi-path with 1 byte id number
+# 03 - length of rest
+# idnumber - random id number
+# parts - number of parts
+# this part - this part, starts from '1'
+#
+# then AT+CMGS=len-of-TPDU in octets
+#
+# TPDU header byte:
+# Structure: (n) = bits
+# +--------+----------+---------+-------+-------+--------+
+# | RP (1) | UDHI (1) | SRI (1) | X (1) | X (1) | MTI(2) |
+# +--------+----------+---------+-------+-------+--------+
+# RP:
+# Reply path
+# UDHI:
+# User Data Header Indicator = Does the UD contains a header
+# 0 : Only the Short Message
+# 1 : Beginning of UD containsheader information
+# SRI:
+# Status Report Indication.
+# The SME (Short Message Entity) has requested a status report.
+# MTI:
+# 00 for SMS-Deliver
+# 01 for SMS-SUBMIT
+
+
+import atchan, sys, re, random
+
+def encode_number(recipient):
+ # encoded number is
+ # number of digits
+ # 91 for international, 81 for local interpretation
+ # BCD digits, nibble-swapped, F padded.
+ if recipient[0] == '+':
+ type = '91'
+ recipient = recipient[1:]
+ else:
+ type = '81'
+ leng = '%02X' % len(recipient)
+ if len(recipient) % 2 == 1:
+ recipient += 'F'
+ swap = ''
+ while recipient:
+ swap += recipient[1] + recipient[0]
+ recipient = recipient[2:]
+ return leng + type + swap
+
+def code7(pad, mesg):
+ # Encode the message as 8 chars in 7 bytes.
+ # pad with 'pad' 0 bits at the start (low in the byte)
+ carry = 0
+ # we have 'pad' low bits stored low in 'carry'
+ code = ''
+ while mesg:
+ c = ord(mesg[0])
+ mesg = mesg[1:]
+ if pad + 7 >= 8:
+ # have a full byte
+ b = carry & ((1<<pad)-1)
+ b += (c << pad) & 255
+ code += '%02X' % b
+ pad -= 1
+ carry = c >> (7-pad)
+ else:
+ # not a full byte yet, just a septet
+ pad = 7
+ carry = c
+ if pad:
+ # a few bits still to emit
+ b = carry & ((1<<pad)-1)
+ code += '%02X' % b
+ return code
+
+def add_len(mesg):
+ return ('%02X' % (len(mesg)/2)) + mesg
+
+def send(chan, dest, mesg):
+ n,c = chan.chat('AT+CMGS=%d' % (len(mesg)/2), ['OK','ERROR','>'])
+ if n == 2:
+ n,c = chan.chat('%s%s\032' % (dest, mesg), ['OK','ERROR'], 10000)
+ if n == 0 and atchan.found(c, re.compile('^\+CMGS: \d+') ):
+ return True
+ # Sometimes don't get the +CMGS: status - strange
+ if n == 0:
+ return True
+ return False
+
+recipient = sys.argv[2]
+mesg = sys.argv[3]
+
+chan = atchan.AtChannel(path="/dev/ttyHS_Control")
+chan.connect()
+
+# clear any pending error status
+chan.chat1('ATE0', ['OK', 'ERROR'])
+if chan.chat1('ATE0', ['OK', 'ERROR']) != 0:
+ print 'system error - message not sent'
+ sys.exit(1)
+
+want = re.compile('^\+COPS:.*".+"')
+n,c = chan.chat('AT+COPS?', ['OK', 'ERROR'], 2000 )
+if n != 0 or not atchan.found(c, want):
+ print 'No Service - message not sent'
+ sys.exit(1)
+
+# use PDU mode
+n,c = chan.chat('AT+CMGF=0', ['OK', 'ERROR'])
+if n != 0:
+ print 'Unknown error'
+ sys.exit(1)
+
+# SMSC header and TPDU header
+SMSC = '00' # No SMSC number given, use default
+##SMSC = '07911614910930F1' # This is my SMSC
+REF = '00' # ask network to assign ref number
+phone_num = encode_number(recipient)
+proto = '00' # don't know what this means
+encode = '00' # 7 bit ascii
+if len(mesg) <= 160:
+ # single SMS mesg
+ m1 = code7(0,mesg)
+ m2 = '%02X'%len(mesg) + m1
+ coded = "01" + REF + phone_num + proto + encode + m2
+ if send(chan, SMSC, coded):
+ print "OK message sent"
+ sys.exit(0)
+ else:
+ print "ERROR message not sent"
+ sys.exit(1)
+
+elif len(mesg) <= 5 * 153:
+ # Multiple messsage
+ packets = (len(mesg) + 152) / 153
+ packet = 0
+ mesgid = random.getrandbits(8)
+ while len(mesg) > 0:
+ m = mesg[0:153]
+ mesg = mesg[153:]
+ id = mesgid
+ packet = packet + 1
+ UDH = add_len('00' + add_len('%02X%02X%02X'%(id, packets, packet)))
+ m1 = UDH + code7(1, m)
+ m2 = '%02X'%(7+len(m)) + m1
+ coded = "41" + REF + phone_num + proto + encode + m2
+ if not send(chan, SMSC, coded):
+ print "ERROR message not sent at part %d/%d" % (packet,packets)
+ sys.exit(1)
+ print 'OK message sent in %d parts' % packets
+ sys.exit(0)
+else:
+ print 'Message is too long'
+ sys.exit(1)
+
--- /dev/null
+#!/usr/bin/env python
+
+#
+# Calls can be made by writing a number to
+# /run/gsm-state/call
+# Status get set call 'Calling' and then 'BUSY' or ''
+# Call can be answered by writing 'answer' to 'call'
+# or can be cancelled by writing ''.
+# During a call, chars can be written to
+# /run/gsm-state/dtmf
+# to send tones.
+
+## FIXME
+# e.g. receive AT response +CREG: 1,"08A7","6E48"
+# show that SIM is now ready
+# cope with /var/lock/suspend not existing yet
+# define 'reset'
+
+import re, time, gobject, os
+from atchan import AtChannel
+import dnotify, suspend
+from tracing import log
+from subprocess import Popen
+
+def record(key, value):
+ f = open('/run/gsm-state/.new.' + key, 'w')
+ f.write(value)
+ f.close()
+ os.rename('/run/gsm-state/.new.' + key,
+ '/run/gsm-state/' + key)
+
+def recall(key):
+ try:
+ fd = open("/run/gsm-state/" + key)
+ l = fd.read(1000)
+ fd.close()
+ except IOError:
+ l = ""
+ return l.strip()
+
+def set_alert(key, value):
+ path = '/run/alert/' + key
+ if value == None:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+ else:
+ try:
+ f = open(path, 'w')
+ f.write(value)
+ f.close()
+ except IOError:
+ pass
+
+def calllog(key, msg):
+ f = open('/var/log/' + key, 'a')
+ now = time.strftime("%Y-%m-%d %H:%M:%S")
+ f.write(now + ' ' + msg + "\n")
+ f.close()
+
+class Task:
+ def __init__(self, repeat):
+ self.repeat = repeat
+ pass
+ def start(self, channel):
+ # take the first action for this task
+ pass
+ def takeline(self, channel, line):
+ # a line has arrived that is presumably for us
+ pass
+ def timeout(self, channel):
+ # we asked for a timeout and got it
+ pass
+
+
+class AtAction(Task):
+ # An AtAction involves:
+ # optional sending an AT command to check some value
+ # matching the result against a string, possibly storing the value
+ # if there is no match send some other AT command, probably to set a value
+ #
+ # States are 'init' 'checking', 'setting', 'done'
+ ok = re.compile("^OK")
+ busy = re.compile("\+CMS ERROR.*SIM busy")
+ not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)")
+ def __init__(self, check = None, ok = None, record = None, at = None,
+ timeout=None, handle = None, repeat = None, arg = None,
+ critical = True, noreply=None):
+ Task.__init__(self, repeat)
+ self.check = check
+ self.okstr = ok
+ if ok:
+ self.okre = re.compile(ok)
+ self.record = record
+ self.at = at
+ self.arg = arg
+ self.timeout_time = timeout
+ self.handle = handle
+ self.critical = critical
+ self.noreply = noreply
+
+ def start(self, channel):
+ channel.state['retries'] = 0
+ channel.state['stage'] = 'init'
+ self.advance(channel)
+
+ def takeline(self, channel, line):
+ if line == None:
+ channel.set_state('reset')
+ channel.advance()
+ return
+ m = self.ok.match(line)
+ if m:
+ channel.cancel_timeout()
+ if self.handle:
+ self.handle(channel, line, None)
+ return self.advance(channel)
+
+ if self.busy.match(line):
+ channel.cancel_timeout()
+ channel.set_timeout(5000)
+ return
+ if self.not_ok.match(line):
+ channel.cancel_timeout()
+ return self.timeout(channel)
+
+ if channel.state['stage'] == 'checking':
+ m = self.okre.match(line)
+ if m:
+ channel.state['matched'] = True
+ if self.record:
+ record(self.record[0], m.expand(self.record[1]))
+ if self.handle:
+ self.handle(channel, line, m)
+ return
+
+ if channel.state['stage'] == 'setting':
+ # didn't really expect anything here..
+ pass
+
+ def timeout(self, channel):
+ if channel.state['retries'] >= 5:
+ if self.critical:
+ channel.set_state('reset')
+ channel.advance()
+ return
+ channel.state['retries'] += 1
+ channel.state['stage'] = 'init'
+ channel.atcmd('')
+
+ def advance(self, channel):
+ st = channel.state['stage']
+ if st == 'init' and self.check:
+ channel.state['stage'] = 'checking'
+ if self.timeout_time:
+ channel.atcmd(self.check, timeout = self.timeout_time)
+ else:
+ channel.atcmd(self.check)
+ elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state:
+ channel.state['stage'] = 'setting'
+ at = self.at
+ if self.arg:
+ at = at % channel.args[self.arg]
+ if self.timeout_time:
+ channel.atcmd(at, timeout = self.timeout_time)
+ else:
+ channel.atcmd(at)
+ if self.noreply:
+ channel.cancel_timeout()
+ channel.advance()
+ else:
+ channel.advance()
+
+class PowerAction(Task):
+ # A PowerAction ensure that we have a connection to the modem
+ # and sets the power on or off, or resets the modem
+ def __init__(self, cmd):
+ Task.__init__(self, None)
+ self.cmd = cmd
+
+ def start(self, channel):
+ if self.cmd == "on":
+ if not channel.connected:
+ channel.connect()
+ if not channel.altchan.connected:
+ channel.altchan.connect()
+ channel.check_flightmode()
+ elif self.cmd == "off":
+ record('carrier', '')
+ record('cell', '')
+ record('signal_strength','0/32')
+ channel.disconnect()
+ channel.altchan.disconnect()
+ elif self.cmd == 'reopen':
+ channel.disconnect()
+ channel.altchan.disconnect()
+ channel.connect()
+ channel.altchan.connect()
+ return channel.advance()
+
+def rel(handle):
+ handle.release()
+ log("did release")
+ return False
+
+class SuspendComplete(Task):
+ # This action simply allows suspend to continue
+ def __init__(self):
+ Task.__init__(self, None)
+
+ def start(self, channel):
+ if channel.suspend_pending:
+ channel.suspend_pending = False
+ log("queue release")
+ gobject.idle_add(rel, channel.suspend_handle)
+ return channel.advance()
+
+class ChangeStateAction(Task):
+ # This action changes to a new state, like a goto
+ def __init__(self, state):
+ Task.__init__(self, None)
+ self.newstate = state
+ def start(self, channel):
+ channel.set_state(self.newstate)
+ return channel.advance()
+
+class CheckSMS(Task):
+ def __init__(self):
+ Task.__init__(self, None)
+ def start(self, channel):
+ if channel.pending_sms:
+ channel.pending_sms = False
+ p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+ ok = p.wait()
+ return channel.advance()
+
+class RouteVoice(Task):
+ def __init__(self, on):
+ Task.__init__(self, None)
+ self.request = on
+ def start(self, channel):
+ if self.request:
+ channel.sound_on = True
+ try:
+ f = open("/run/sound/00-voicecall","w")
+ f.close()
+ except:
+ pass
+ p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True)
+ log('Running gsm-voice-routing pid', p.pid)
+ channel.voice_route = p
+ elif channel.sound_on:
+ if channel.voice_route:
+ channel.voice_route.send_signal(15)
+ channel.voice_route.wait()
+ channel.voice_route = None
+ try:
+ os.unlink("/run/sound/00-voicecall")
+ except OSError:
+ pass
+ channel.sound_on = False
+ return channel.advance()
+
+class BlockSuspendAction(Task):
+ def __init__(self, enable):
+ Task.__init__(self, None)
+ self.enable = enable
+ def start(self, channel):
+ if self.enable:
+ channel.suspend_blocker.block()
+ channel.advance()
+
+ if not self.enable:
+ channel.suspend_blocker.unblock()
+
+
+class Async:
+ def __init__(self, msg, handle, handle_extra = None):
+ self.msg = msg
+ self.msgre = re.compile(msg)
+ self.handle = handle
+ self.handle_extra = handle_extra
+
+ def match(self, line):
+ return self.msgre.match(line)
+
+# async handlers...
+LAC=0
+CELLID=0
+cellnames={}
+def status_update(channel, line, m):
+ if m and m.groups()[3] != None:
+ global LAC, CELLID, cellnames
+ LAC = int(m.groups()[2],16)
+ CELLID = int(m.groups()[3],16)
+ record('cellid', "%04X %06X" % (LAC, CELLID));
+ if CELLID in cellnames:
+ record('cell', cellnames[CELLID])
+ log("That one is", cellnames[CELLID])
+
+def new_sms(channel, line, m):
+ if m:
+ channel.pending_sms = False
+ record('newsms', m.groups()[1])
+ p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+ ok = p.wait()
+def maybe_sms(line, channel):
+ channel.pending_sms = True
+
+def sigstr(channel, line, m):
+ if m:
+ record('signal_strength', m.groups()[0] + '/32')
+
+global incoming_cell_id
+def cellid_update(channel, line, m):
+ # get something like +CBM: 1568,50,1,1,1
+ # don't know what that means, just collect the 'extra' line
+ # I think the '50' means 'this is a cell id'. I should
+ # probably test for that.
+ #
+ # response can be multi-line
+ global incoming_cell_id
+ incoming_cell_id = ""
+
+def cellid_new(channel, line):
+ global CELLID, cellnames, incoming_cell_id
+ if not line:
+ # end of message
+ if incoming_cell_id:
+ l = re.sub('[^!-~]+',' ',incoming_cell_id)
+ if CELLID:
+ cellnames[CELLID] = l
+ record('cell', l)
+ return False
+ line = line.strip()
+ if incoming_cell_id:
+ incoming_cell_id += ' ' + line
+ else:
+ incoming_cell_id = line
+ return True
+
+incoming_num = None
+def incoming(channel, line, m):
+ global incoming_num
+ if incoming_num:
+ record('incoming', incoming_num)
+ else:
+ record('incoming', '-')
+ set_alert('ring', 'new')
+ if channel.gstate not in ['incoming', 'answer']:
+ calllog('incoming', '-call-')
+ channel.set_state('incoming')
+ record('status', 'INCOMING')
+ global cpas_zero_cnt
+ cpas_zero_cnt = 0
+
+def incoming_number(channel, line, m):
+ global incoming_num
+ if m:
+ num = m.groups()[0]
+ if incoming_num == None:
+ calllog('incoming', num);
+ incoming_num = num
+ record('incoming', incoming_num)
+
+def no_carrier(channel, line, m):
+ record('status', '')
+ record('call', '')
+ if channel.gstate != 'idle':
+ channel.set_state('idle')
+
+
+def busy(channel, line, m):
+ record('status', 'BUSY')
+ record('call', '')
+
+def ussd(channel, line, m):
+ pass
+
+cpas_zero_cnt = 0
+def call_status(channel, line, m):
+ global cpas_zero_cnt
+ global calling
+ log("call_status got", line)
+ if not m:
+ return
+ s = int(m.groups()[0])
+ log("s = %d" % s)
+ if s == 0:
+ if calling:
+ return
+ cpas_zero_cnt += 1
+ if cpas_zero_cnt <= 3:
+ return
+ # idle
+ global incoming_num
+ incoming_num = None
+ record('incoming', '')
+ if channel.gstate == 'incoming':
+ calllog('incoming','-end-')
+ record('status', '')
+ if channel.gstate != 'idle' and channel.gstate != 'suspend':
+ channel.set_state('idle')
+ cpas_zero_cnt = 0
+ calling = False
+ if s == 3:
+ # incoming call
+ if channel.gstate not in ['incoming', 'answer']:
+ # strange ..
+ channel.set_state('incoming')
+ record('status', 'INCOMING')
+ set_alert('ring', 'new')
+ record('incoming', '-')
+ if s == 4:
+ # on a call
+ if channel.gstate != 'on-call' and channel.gstate != 'hangup':
+ channel.set_state('on-call')
+
+control = {}
+
+# For flight mode, we turn the power off.
+control['to_flight'] = [
+ AtAction(at='+CFUN=0'),
+ PowerAction('off'),
+ ChangeStateAction('flight'),
+ ]
+
+control['flight'] = [
+ SuspendComplete()
+ ]
+
+control['reset'] = [
+ # turning power off just kills everything!!!
+ PowerAction('reopen'),
+ #PowerAction('off'),
+ AtAction(at='E0', timeout=30000),
+ ChangeStateAction('init'),
+ ]
+
+# For suspend, we want power on, but no wakups for status or cellid
+control['suspend'] = [
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+ AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+ CheckSMS(),
+ AtAction(at='+CNMI=1,1,0,0,0'),
+ AtAction(at='_OSQI=0'),
+ AtAction(at='_OEANT=0'),
+ AtAction(at='_OSSYS=0'),
+ AtAction(at='_OPONI=0'),
+ AtAction(at='+CREG=0'),
+ SuspendComplete()
+ ]
+
+control['listenerr'] = [
+ PowerAction('on'),
+ AtAction(at='V1E0'),
+ AtAction(at='+CMEE=2;+CRC=1')
+ ]
+control['init'] = [
+ BlockSuspendAction(True),
+ SuspendComplete(),
+ PowerAction('on'),
+ AtAction(at='V1E0'),
+ AtAction(at='+CMEE=2;+CRC=1'),
+ # Turn the device on.
+ AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+ # Report carrier as long name
+ AtAction(at='+COPS=3,0'),
+ # register with a carrier
+ #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+ # record=('carrier', '\\1'), timeout=10000),
+ AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+ record=('carrier', '\\1'), timeout=10000, repeat=37000),
+ # text format for various messages such SMS
+ AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'),
+ # get location status updates
+ AtAction(at='+CREG=2'),
+ AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")',
+ handle=status_update, timeout=4000),
+ # Enable collection of Cell Info message
+ #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'),
+ #AtAction(at='+CSCB=0'),
+ AtAction(at='+CSCB=1', critical=False),
+ # Enable async reporting of TXT and Cell info messages
+ #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'),
+ AtAction(at='+CNMI=1,0,0,0,0', critical=False),
+ AtAction(at='+CNMI=1,1,2,0,0', critical=False),
+ # Enable async reporting of signal strength
+ AtAction(at='_OSQI=1', critical=False),
+
+ # Enable reporting of Caller number id.
+ AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000,
+ critical = False),
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+ ChangeStateAction('idle')
+ ]
+
+control['idle'] = [
+ RouteVoice(False),
+ CheckSMS(),
+ BlockSuspendAction(False),
+ SuspendComplete(),
+ AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+ record=('carrier', '\\1'), timeout=10000, repeat=37000),
+ # Make sure on GSM
+ AtAction(at='_OPSYS=3,2'),
+ # get signal string
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=29000)
+ ]
+
+control['incoming'] = [
+ BlockSuspendAction(True),
+ SuspendComplete(),
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500),
+
+ # monitor signal strength
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=30000)
+ ]
+
+control['answer'] = [
+ AtAction(at='A'),
+ RouteVoice(True),
+ ChangeStateAction('incoming')
+ ]
+
+control['call'] = [
+ AtAction(at='D%s;', arg='number'),
+ RouteVoice(True),
+ ChangeStateAction('on-call')
+ ]
+
+control['dtmf'] = [
+ AtAction(at='+VTS=%s', arg='dtmf', noreply=True),
+ ChangeStateAction('on-call')
+ ]
+
+control['hangup'] = [
+ AtAction(at='+CHUP'),
+ RouteVoice(False),
+ BlockSuspendAction(False),
+ ChangeStateAction('idle')
+ ]
+
+control['on-call'] = [
+ BlockSuspendAction(True),
+ SuspendComplete(),
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000),
+
+ # get signal strength
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=30000)
+ ]
+async = [
+ Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update),
+ Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms),
+ Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update,
+ handle_extra = cellid_new),
+ Async(msg='\+CRING: (.*)', handle = incoming),
+ Async(msg='RING', handle = incoming),
+ Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number),
+ Async(msg='NO CARRIER', handle = no_carrier),
+ Async(msg='BUSY', handle = busy),
+ Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd),
+ Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr),
+
+ ]
+
+class GsmD(AtChannel):
+
+ # gsmd works like a state machine
+ # the high level states are: flight suspend idle incoming on-call
+ # Note that the whole 'call-waiting' experience is not coverred here.
+ # That needs to be handled by whoever answers calls and allows interaction
+ # between user and phone system.
+ #
+ # Each state contains a list of tasks such as setting and
+ # checking config options and monitoring state (e.g. signal strength)
+ # Some tasks are single-shot and only need to complete each time the state is
+ # entered. Others are repeating (such as status monitoring).
+ # We take the first task of the current list and execute it, or wait
+ # until one will be ready.
+ # Tasks themselves can be state machines, so we keep track of what 'stage'
+ # we are up to in the current task.
+ #
+ # The system is (naturally) event driven. The main two events that we
+ # receive are:
+ # 'takeline' which presents one line of text from the GSM device, and
+ # 'timeout' which indicates that a timeout set when a command was sent has
+ # expired.
+ # Other events are:
+ # 'taskready' when the time of the next pending task arrives.
+ # 'flight' when the state of the 'flight mode' has changed
+ # 'suspend' when a suspend has been requested.
+ #
+ # Each event does some event specific processing to modify the state,
+ # Then calls 'self.advance' to progress the state machine.
+ # When high level state changes are requested, any pending task is discarded.
+ #
+ # If a task detects an error (gsm device not responding properly) it might
+ # request a reset. This involves sending a modem_reset command and then
+ # restarting the current state from the top.
+ # A task can also indicate:
+ # The next stage to try
+ # How long to wait before retrying (or None)
+ #
+
+ def __init__(self, path, altpath):
+ AtChannel.__init__(self, path = path)
+
+ self.extra = None
+ self.flightmode = True
+ self.state = None
+ self.args = {}
+ self.suspend_pending = False
+ self.pending_sms = False
+ self.sound_on = True
+ self.voice_route = None
+ self.tasknum = None
+ self.altpath = altpath
+ self.altchan = CarrierDetect(altpath, self)
+ self.gstate = None
+ self.nextstate = None
+ self.statechanged = False
+
+ record('carrier','')
+ record('cell','')
+ record('incoming','')
+ record('signal_strength','')
+ record('status', '')
+
+ # set the initial state
+ self.set_state('flight')
+
+ # Monitor other external events which affect us
+ d = dnotify.dir('/var/lib/misc/flightmode')
+ self.flightmode_watcher = d.watch('active', self.check_flightmode)
+ d = dnotify.dir('/run/gsm-state')
+ self.call_watcher = d.watch('call', self.check_call)
+ self.dtmf_watcher = d.watch('dtmf', self.check_dtmf)
+
+ self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume)
+ self.suspend_blocker = suspend.blocker()
+
+
+ # Check the externally imposed state
+ self.check_flightmode(self.flightmode_watcher)
+
+ # and GO!
+ self.advance()
+
+ def check_call(self, f = None):
+ l = recall('call')
+ log("Check call got", l)
+ if l == "":
+ if self.nextstate not in ['hangup', 'idle']:
+ self.set_state('hangup')
+ record('status','')
+ record('incoming','')
+ elif l == 'answer':
+ if self.nextstate == 'incoming':
+ record('status', 'on-call')
+ record('incoming','')
+ set_alert('ring', None)
+ self.set_state('answer')
+ else:
+ if self.nextstate == 'idle':
+ global calling
+ calling = True
+ self.args['number'] = l
+ self.set_state('call')
+ record('status', 'on-call')
+
+ def check_dtmf(self, f = None):
+ l = recall('dtmf')
+ log("Check dtmf got", l)
+ if len(l):
+ self.args['dtmf'] = l
+ self.set_state('dtmf')
+ record('dtmf','')
+
+ def check_flightmode(self, f = None):
+ try:
+ fd = open("/var/lib/misc/flightmode/active")
+ l = fd.read(1)
+ fd.close()
+ except IOError:
+ l = ""
+ log("check flightmode got", len(l))
+ if len(l) == 0:
+ if self.flightmode:
+ self.flightmode = False
+ if self.suspend_handle.suspended:
+ self.set_state('suspend')
+ else:
+ self.set_state('init')
+ else:
+ if not self.flightmode:
+ self.flightmode = True
+ self.set_state('to_flight')
+
+ def do_suspend(self):
+ log("do suspend")
+ if self.nextstate == 'flight':
+ return True
+ self.suspend_pending = True
+ self.set_state('suspend')
+ return False
+
+ def do_resume(self):
+ log("do resume")
+ if self.nextstate == 'suspend':
+ self.set_state('init')
+
+ def set_state(self, state):
+ # this happens asynchronously so we must be careful
+ # about changing things. Just record the new state
+ # and abort any timeout
+ log("state should become", state)
+ self.nextstate = state
+ self.statechanged = True
+ self.abort_timeout()
+
+ def advance(self):
+ # 'advance' is called by a 'Task' when it has finished
+ # It may have called 'set_state' first either to report
+ # an error or to effect a regular state change
+ now = int(time.time()*1000)
+ if self.tasknum != None:
+ self.lastrun[self.tasknum] = now
+ self.tasknum = None
+ if self.statechanged:
+ # time to effect 'set_state' synchronously
+ self.statechanged = False
+ self.gstate = self.nextstate
+ log("state becomes", self.gstate)
+ n = len(control[self.gstate])
+ self.lastrun = n * [0]
+ (t, delay) = self.next_cmd()
+ log("advance %s chooses %d, %d" % (self.gstate, t, delay))
+ if delay:
+ log("Sleeping for %f seconds" % (delay/1000.0))
+ self.set_timeout(delay)
+ else:
+ self.tasknum = t
+ self.state = {}
+ control[self.gstate][t].start(self)
+
+
+ def takeline(self, line):
+
+ if self.extra:
+ # an async message is multi-line and we need to handle
+ # the extra line.
+ if not self.extra.handle_extra(self, line):
+ self.extra = None
+ return False
+
+ if line == None:
+ self.set_state('reset')
+ self.advance()
+ if not line:
+ return False
+
+ # Check for an async message
+ for m in async:
+ mt = m.match(line)
+ if mt:
+ m.handle(self, line, mt)
+ if m.handle_extra:
+ self.extra = m
+ return False
+
+ # else pass it to the task
+ if self.tasknum != None:
+ control[self.gstate][self.tasknum].takeline(self, line)
+
+ def timedout(self):
+ if self.tasknum == None:
+ self.advance()
+ else:
+ control[self.gstate][self.tasknum].timeout(self)
+
+ def next_cmd(self):
+ # Find a command to execute, or a delay
+ # return (cmd,time)
+ # cmd is an index into control[state],
+ # time is seconds until try something
+ mindelay = 60*60*1000
+ cs = control[self.gstate]
+ n = len(cs)
+ now = int(time.time()*1000)
+ for i in range(n):
+ if self.lastrun[i] == 0 or (cs[i].repeat and
+ self.lastrun[i] + cs[i].repeat <= now):
+ return (i, 0)
+ if cs[i].repeat:
+ delay = (self.lastrun[i] + cs[i].repeat) - now;
+ if delay < mindelay:
+ mindelay = delay
+ return (0, mindelay)
+
+class CarrierDetect(AtChannel):
+ # on the hso modem in the GTA04, the 'NO CARRIER' signal
+ # arrives on the 'Modem' port, not on the 'Application' port.
+ # So we listen to the 'Modem' port, and report any
+ # 'NO CARRIER' we see - or indeed anything that we see.
+ def __init__(self, path, main):
+ AtChannel.__init__(self, path = path)
+ self.main = main
+
+ def takeline(self, line):
+ self.main.takeline(line)
+
+class SysfsWatcher:
+ # watch for changes on a sysfs file and report them
+ # We read the content, report that, wait for a change
+ # and report again
+ def __init__(self, path, action):
+ self.path = path
+ self.action = action
+ self.fd = open(path, "r")
+ self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read)
+ self.read()
+
+ def read(self, *args):
+ self.fd.seek(0)
+ try:
+ r = self.fd.read(4096)
+ except IOerror:
+ return True
+ self.action(r)
+ return True
+
+try:
+ os.mkdir("/run/gsm-state")
+except:
+ pass
+
+calling = False
+a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem')
+print "GsmD started"
+
+try:
+ f = open("/sys/class/gpio/gpio176/edge", "w")
+except IOError:
+ f = None
+if f:
+ f.write("rising")
+ f.close()
+ w = SysfsWatcher("/sys/class/gpio/gpio176/value",
+ lambda l: maybe_sms(l, a))
+else:
+ import evdev
+ def check_evt(dc, mom, typ, code, val):
+ if typ == 1 and val == 1:
+ # keypress
+ maybe_sms("", a)
+ try:
+ f = evdev.EvDev("/dev/input/incoming", check_evt)
+ except:
+ f = None
+c = gobject.main_context_default()
+while True:
+ c.iteration()
--- /dev/null
+#!/usr/bin/env python
+
+#
+# Calls can be made by writing a number to
+# /run/gsm-state/call
+# Status get set call 'Calling' and then 'BUSY' or ''
+# Call can be answered by writing 'answer' to 'call'
+# or can be cancelled by writing ''.
+# During a call, chars can be written to
+# /run/gsm-state/dtmf
+# to send tones.
+
+## FIXME
+# e.g. receive AT response +CREG: 1,"08A7","6E48"
+# show that SIM is now ready
+# cope with /var/lock/suspend not existing yet
+# define 'reset'
+
+import re, time, gobject, os
+from atchan import AtChannel
+import dnotify, suspend
+from tracing import log
+from subprocess import Popen
+
+def record(key, value):
+ f = open('/run/gsm-state/.new.' + key, 'w')
+ f.write(value)
+ f.close()
+ os.rename('/run/gsm-state/.new.' + key,
+ '/run/gsm-state/' + key)
+
+def recall(key):
+ try:
+ fd = open("/run/gsm-state/" + key)
+ l = fd.read(1000)
+ fd.close()
+ except IOError:
+ l = ""
+ return l.strip()
+
+def set_alert(key, value):
+ path = '/run/alert/' + key
+ if value == None:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+ else:
+ try:
+ f = open(path, 'w')
+ f.write(value)
+ f.close()
+ except IOError:
+ pass
+
+lastlog={}
+def calllog(key, msg):
+ f = open('/var/log/' + key, 'a')
+ now = time.strftime("%Y-%m-%d %H:%M:%S")
+ f.write(now + ' ' + msg + "\n")
+ f.close()
+ lastlog[key] = msg
+
+def calllog_end(key):
+ if key in lastlog:
+ calllog(key, '-end-')
+ del lastlog[key]
+
+class Task:
+ def __init__(self, repeat):
+ self.repeat = repeat
+ pass
+ def start(self, channel):
+ # take the first action for this task
+ pass
+ def takeline(self, channel, line):
+ # a line has arrived that is presumably for us
+ pass
+ def timeout(self, channel):
+ # we asked for a timeout and got it
+ pass
+
+class AtAction(Task):
+ # An AtAction involves:
+ # optionally sending an AT command to check some value
+ # matching the result against a string, possibly storing the value
+ # if there is no match send some other AT command, probably to set a value
+ #
+ # States are 'init' 'checking', 'setting', 'done'
+ ok = re.compile("^OK")
+ busy = re.compile("\+CMS ERROR.*SIM busy")
+ not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)")
+ def __init__(self, check = None, ok = None, record = None, at = None,
+ timeout=None, handle = None, repeat = None, arg = None,
+ critical = True, noreply=None):
+ Task.__init__(self, repeat)
+ self.check = check
+ self.okstr = ok
+ if ok:
+ self.okre = re.compile(ok)
+ self.record = record
+ self.at = at
+ self.arg = arg
+ self.timeout_time = timeout
+ self.handle = handle
+ self.critical = critical
+ self.noreply = noreply
+
+ def start(self, channel):
+ channel.state['retries'] = 0
+ channel.state['stage'] = 'init'
+ self.advance(channel)
+
+ def takeline(self, channel, line):
+ if line == None:
+ channel.set_state('reset')
+ channel.advance()
+ return
+ m = self.ok.match(line)
+ if m:
+ channel.cancel_timeout()
+ if self.handle:
+ self.handle(channel, line, None)
+ return self.advance(channel)
+
+ if self.busy.match(line):
+ channel.cancel_timeout()
+ channel.set_timeout(5000)
+ return
+ if self.not_ok.match(line):
+ channel.cancel_timeout()
+ return self.timeout(channel)
+
+ if channel.state['stage'] == 'checking':
+ m = self.okre.match(line)
+ if m:
+ channel.state['matched'] = True
+ if self.record:
+ record(self.record[0], m.expand(self.record[1]))
+ if self.handle:
+ self.handle(channel, line, m)
+ return
+
+ if channel.state['stage'] == 'setting':
+ # didn't really expect anything here..
+ pass
+
+ def timeout(self, channel):
+ if channel.state['retries'] >= 5:
+ if self.critical:
+ channel.set_state('reset')
+ channel.advance()
+ return
+ channel.state['retries'] += 1
+ channel.state['stage'] = 'init'
+ channel.atcmd('')
+
+ def advance(self, channel):
+ st = channel.state['stage']
+ if st == 'init' and self.check:
+ channel.state['stage'] = 'checking'
+ if self.timeout_time:
+ channel.atcmd(self.check, timeout = self.timeout_time)
+ else:
+ channel.atcmd(self.check)
+ elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state:
+ channel.state['stage'] = 'setting'
+ at = self.at
+ if self.arg:
+ at = at % channel.args[self.arg]
+ if self.timeout_time:
+ channel.atcmd(at, timeout = self.timeout_time)
+ else:
+ channel.atcmd(at)
+ if self.noreply:
+ channel.cancel_timeout()
+ channel.advance()
+ else:
+ channel.advance()
+
+class PowerAction(Task):
+ # A PowerAction ensure that we have a connection to the modem
+ # and sets the power on or off, or resets the modem
+ def __init__(self, cmd):
+ Task.__init__(self, None)
+ self.cmd = cmd
+
+ def start(self, channel):
+ if self.cmd == "on":
+ if not channel.connected:
+ channel.connect()
+ if not channel.altchan.connected:
+ channel.altchan.connect()
+ channel.check_flightmode()
+ elif self.cmd == "off":
+ record('carrier', '')
+ record('cell', '')
+ record('signal_strength','0/32')
+ channel.disconnect()
+ channel.altchan.disconnect()
+ elif self.cmd == 'reopen':
+ channel.disconnect()
+ channel.altchan.disconnect()
+ channel.connect()
+ channel.altchan.connect()
+ return channel.advance()
+
+class ChangeStateAction(Task):
+ # This action changes to a new state, like a goto
+ def __init__(self, state):
+ Task.__init__(self, None)
+ self.newstate = state
+ def start(self, channel):
+ if self.newstate:
+ state = self.newstate
+ elif channel.statechanged:
+ state = channel.nextstate
+ channel.statechanged = False
+ else:
+ state = None
+ if state:
+ channel.gstate = state
+ channel.tasknum = None
+ if not channel.statechanged:
+ channel.nextstate = state
+ log("ChangeStateAction chooses", channel.gstate)
+ n = len(control[channel.gstate])
+ channel.lastrun = n * [0]
+ return channel.advance()
+
+class CheckSMS(Task):
+ def __init__(self):
+ Task.__init__(self, None)
+ def start(self, channel):
+ if channel.pending_sms:
+ channel.pending_sms = False
+ p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+ ok = p.wait()
+ return channel.advance()
+
+class RouteVoice(Task):
+ def __init__(self, on):
+ Task.__init__(self, None)
+ self.request = on
+ def start(self, channel):
+ if self.request:
+ channel.sound_on = True
+ try:
+ f = open("/run/sound/00-voicecall","w")
+ f.close()
+ except:
+ pass
+ p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True)
+ log('Running gsm-voice-routing pid', p.pid)
+ channel.voice_route = p
+ elif channel.sound_on:
+ if channel.voice_route:
+ channel.voice_route.send_signal(15)
+ channel.voice_route.wait()
+ channel.voice_route = None
+ try:
+ os.unlink("/run/sound/00-voicecall")
+ except OSError:
+ pass
+ channel.sound_on = False
+ return channel.advance()
+
+class BlockSuspendAction(Task):
+ def __init__(self, enable):
+ Task.__init__(self, None)
+ self.enable = enable
+ def start(self, channel):
+ print "BlockSuspendAction sets", self.enable
+ if self.enable:
+ channel.suspend_blocker.block()
+ # No point holding a pending suspend any more
+ if channel.suspend_pending:
+ channel.suspend_pending = False
+ print "BlockSuspendAction calls release"
+ suspend.abort_cycle()
+ channel.suspend_handle.release()
+ if not self.enable:
+ channel.suspend_blocker.unblock()
+
+ channel.advance()
+
+class Async:
+ def __init__(self, msg, handle, handle_extra = None):
+ self.msg = msg
+ self.msgre = re.compile(msg)
+ self.handle = handle
+ self.handle_extra = handle_extra
+
+ def match(self, line):
+ return self.msgre.match(line)
+
+# async handlers...
+LAC=0
+CELLID=0
+cellnames={}
+def status_update(channel, line, m):
+ if m and m.groups()[3] != None:
+ global LAC, CELLID, cellnames
+ LAC = int(m.groups()[2],16)
+ CELLID = int(m.groups()[3],16)
+ record('cellid', "%04X %06X" % (LAC, CELLID));
+ if CELLID in cellnames:
+ record('cell', cellnames[CELLID])
+ log("That one is", cellnames[CELLID])
+
+def new_sms(channel, line, m):
+ if m:
+ channel.pending_sms = False
+ record('newsms', m.groups()[1])
+ p = Popen('gsm-getsms -n', shell=True, close_fds = True)
+ ok = p.wait()
+
+def maybe_sms(line, channel):
+ channel.pending_sms = True
+
+def sigstr(channel, line, m):
+ if m:
+ record('signal_strength', m.groups()[0] + '/32')
+
+global incoming_cell_id
+def cellid_update(channel, line, m):
+ # get something like +CBM: 1568,50,1,1,1
+ # don't know what that means, just collect the 'extra' line
+ # I think the '50' means 'this is a cell id'. I should
+ # probably test for that.
+ #
+ # response can be multi-line
+ global incoming_cell_id
+ incoming_cell_id = ""
+
+def cellid_new(channel, line):
+ global CELLID, cellnames, incoming_cell_id
+ if not line:
+ # end of message
+ if incoming_cell_id:
+ l = re.sub('[^!-~]+',' ',incoming_cell_id)
+ if CELLID:
+ cellnames[CELLID] = l
+ record('cell', l)
+ return False
+ line = line.strip()
+ if incoming_cell_id:
+ incoming_cell_id += ' ' + line
+ else:
+ incoming_cell_id = line
+ return True
+
+incoming_num = None
+def incoming(channel, line, m):
+ global incoming_num
+ if incoming_num:
+ record('incoming', incoming_num)
+ else:
+ record('incoming', '-')
+ set_alert('ring', 'new')
+ if channel.gstate not in ['on-call', 'incoming', 'answer']:
+ calllog('incoming', '-call-')
+ channel.set_state('incoming')
+ record('status', 'INCOMING')
+ global cpas_zero_cnt
+ cpas_zero_cnt = 0
+
+def incoming_number(channel, line, m):
+ global incoming_num
+ if m:
+ num = m.groups()[0]
+ if incoming_num == None:
+ calllog('incoming', num);
+ incoming_num = num
+ record('incoming', incoming_num)
+
+def no_carrier(channel, line, m):
+ record('status', '')
+ record('call', '')
+ if channel.gstate != 'idle':
+ channel.set_state('idle')
+
+def busy(channel, line, m):
+ record('status', 'BUSY')
+ record('call', '')
+
+def ussd(channel, line, m):
+ pass
+
+cpas_zero_cnt = 0
+def call_status(channel, line, m):
+ global cpas_zero_cnt
+ global calling
+ log("call_status got", line)
+ if not m:
+ return
+ s = int(m.groups()[0])
+ log("s = %d" % s)
+ if s == 0:
+ if calling:
+ return
+ cpas_zero_cnt += 1
+ if cpas_zero_cnt <= 3:
+ return
+ # idle
+ global incoming_num
+ incoming_num = None
+ record('incoming', '')
+ if channel.gstate in ['on-call','incoming','call']:
+ calllog_end('incoming')
+ calllog_end('outgoing')
+ record('status', '')
+ if channel.gstate != 'idle' and channel.gstate != 'suspend':
+ channel.set_state('idle')
+ cpas_zero_cnt = 0
+ calling = False
+ if s == 3:
+ # incoming call
+ if channel.gstate not in ['incoming', 'answer']:
+ # strange ..
+ channel.set_state('incoming')
+ record('status', 'INCOMING')
+ set_alert('ring', 'new')
+ record('incoming', '-')
+ if s == 4:
+ # on a call - but could be just a data call, so don't do anything
+ #if channel.gstate != 'on-call' and channel.gstate != 'hangup':
+ # channel.set_state('on-call')
+ pass
+
+control = {}
+
+# For flight mode, we turn the power off.
+control['flight'] = [
+ AtAction(at='+CFUN=0'),
+ PowerAction('off'),
+ BlockSuspendAction(False),
+ ]
+
+control['reset'] = [
+ # turning power off just kills everything!!!
+ PowerAction('reopen'),
+ #PowerAction('off'),
+ AtAction(at='E0', timeout=30000),
+ ChangeStateAction('init'),
+ ]
+
+# For suspend, we want power on, but no wakups for status or cellid
+control['suspend'] = [
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+ AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+ CheckSMS(),
+ ChangeStateAction(None), # allow async state change
+ AtAction(at='+CNMI=1,1,0,0,0'),
+ AtAction(at='_OSQI=0'),
+ AtAction(at='_OEANT=0'),
+ AtAction(at='_OSSYS=0'),
+ AtAction(at='_OPONI=0'),
+ AtAction(at='+CREG=0'),
+ ]
+control['resume'] = [
+ BlockSuspendAction(True),
+ AtAction(at='+CNMI=1,1,2,0,0', critical=False),
+ AtAction(at='_OSQI=1', critical=False),
+ AtAction(at='+CREG=2'),
+ CheckSMS(),
+ ChangeStateAction(None),
+ ChangeStateAction('idle'),
+ ]
+
+control['listenerr'] = [
+ PowerAction('on'),
+ AtAction(at='V1E0'),
+ AtAction(at='+CMEE=2;+CRC=1')
+ ]
+control['init'] = [
+ BlockSuspendAction(True),
+ PowerAction('on'),
+ AtAction(at='V1E0'),
+ AtAction(at='+CMEE=2;+CRC=1'),
+ # Turn the device on.
+ AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000),
+ # Report carrier as long name
+ AtAction(at='+COPS=3,0'),
+ # register with a carrier
+ #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+ # record=('carrier', '\\1'), timeout=10000),
+ AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+ record=('carrier', '\\1'), timeout=10000),
+ # text format for various messages such SMS
+ AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'),
+ # get location status updates
+ AtAction(at='+CREG=2'),
+ AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")',
+ handle=status_update, timeout=4000),
+ # Enable collection of Cell Info message
+ #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'),
+ #AtAction(at='+CSCB=0'),
+ AtAction(at='+CSCB=1', critical=False),
+ # Enable async reporting of TXT and Cell info messages
+ #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'),
+ AtAction(at='+CNMI=1,0,0,0,0', critical=False),
+ AtAction(at='+CNMI=1,1,2,0,0', critical=False),
+ # Enable async reporting of signal strength
+ AtAction(at='_OSQI=1', critical=False),
+
+ # Enable reporting of Caller number id.
+ AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000,
+ critical = False),
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status),
+ ChangeStateAction('idle')
+ ]
+
+control['idle'] = [
+ RouteVoice(False),
+ CheckSMS(),
+ BlockSuspendAction(False),
+ AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS',
+ record=('carrier', '\\1'), timeout=10000),
+ #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0',
+ # record=('carrier', '\\1'), timeout=10000, repeat=37000),
+ # Make sure to use both 2G and 3G
+ AtAction(at='_OPSYS=3,2'),
+ # get signal string
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=29000)
+ ]
+
+control['incoming'] = [
+ BlockSuspendAction(True),
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500),
+
+ # monitor signal strength
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=30000)
+ ]
+
+control['answer'] = [
+ AtAction(at='A'),
+ RouteVoice(True),
+ ChangeStateAction('on-call')
+ ]
+
+control['call'] = [
+ AtAction(at='D%s;', arg='number'),
+ RouteVoice(True),
+ ChangeStateAction('on-call')
+ ]
+
+control['dtmf'] = [
+ AtAction(at='+VTS=%s', arg='dtmf', noreply=True),
+ ChangeStateAction('on-call')
+ ]
+
+control['hangup'] = [
+ AtAction(at='+CHUP'),
+ ChangeStateAction('idle')
+ ]
+
+control['on-call'] = [
+ BlockSuspendAction(True),
+ AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000),
+
+ # get signal strength
+ AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)',
+ record=('signal_strength','\\1/32'), repeat=30000)
+ ]
+
+async = [
+ Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update),
+ Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms),
+ Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update,
+ handle_extra = cellid_new),
+ Async(msg='\+CRING: (.*)', handle = incoming),
+ Async(msg='RING', handle = incoming),
+ Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number),
+ Async(msg='NO CARRIER', handle = no_carrier),
+ Async(msg='BUSY', handle = busy),
+ Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd),
+ Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr),
+
+ ]
+
+class GsmD(AtChannel):
+
+ # gsmd works like a state machine
+ # the high level states are: flight suspend idle incoming on-call
+ # Note that the whole 'call-waiting' experience is not coverred here.
+ # That needs to be handled by whoever answers calls and allows interaction
+ # between user and phone system.
+ #
+ # Each state contains a list of tasks such as setting and
+ # checking config options and monitoring state (e.g. signal strength)
+ # Some tasks are single-shot and only need to complete each time the state is
+ # entered. Others are repeating (such as status monitoring).
+ # We take the first task of the current list and execute it, or wait
+ # until one will be ready.
+ # Tasks themselves can be state machines, so we keep track of what 'stage'
+ # we are up to in the current task.
+ #
+ # The system is (naturally) event driven. The main two events that we
+ # receive are:
+ # 'takeline' which presents one line of text from the GSM device, and
+ # 'timeout' which indicates that a timeout set when a command was sent has
+ # expired.
+ # Other events are:
+ # 'taskready' when the time of the next pending task arrives.
+ # 'flight' when the state of the 'flight mode' has changed
+ # 'suspend' when a suspend has been requested.
+ #
+ # Each event does some event specific processing to modify the state,
+ # Then calls 'self.advance' to progress the state machine.
+ # When high level state changes are requested, any pending task is discarded.
+ #
+ # If a task detects an error (gsm device not responding properly) it might
+ # request a reset. This involves sending a modem_reset command and then
+ # restarting the current state from the top.
+ # A task can also indicate:
+ # The next stage to try
+ # How long to wait before retrying (or None)
+ #
+
+ def __init__(self, path, altpath):
+ AtChannel.__init__(self, path = path)
+
+ self.extra = None
+ self.flightmode = True
+ self.state = None
+ self.args = {}
+ self.suspend_pending = False
+ self.pending_sms = False
+ self.sound_on = True
+ self.voice_route = None
+ self.tasknum = None
+ self.altpath = altpath
+ self.altchan = CarrierDetect(altpath, self)
+ self.gstate = None
+ self.nextstate = None
+ self.statechanged = False
+
+ record('carrier','')
+ record('cell','')
+ record('incoming','')
+ record('signal_strength','')
+ record('status', '')
+
+ # set the initial state
+ self.set_state('flight')
+
+ # Monitor other external events which affect us
+ d = dnotify.dir('/var/lib/misc/flightmode')
+ self.flightmode_watcher = d.watch('active', self.check_flightmode)
+ d = dnotify.dir('/run/gsm-state')
+ self.call_watcher = d.watch('call', self.check_call)
+ self.dtmf_watcher = d.watch('dtmf', self.check_dtmf)
+
+ self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume)
+ self.suspend_blocker = suspend.blocker()
+
+ # Check the externally imposed state
+ self.check_flightmode(self.flightmode_watcher)
+
+ # and GO!
+ self.advance()
+
+ def check_call(self, f = None):
+ l = recall('call')
+ log("Check call got", l)
+ if l == "":
+ if self.nextstate not in ['hangup', 'idle']:
+ global incoming_num
+ incoming_num = None
+ self.set_state('hangup')
+ record('status','')
+ record('incoming','')
+ calllog_end('incoming')
+ calllog_end('outgoing')
+ elif l == 'answer':
+ if self.nextstate == 'incoming':
+ record('status', 'on-call')
+ record('incoming','')
+ set_alert('ring', None)
+ self.set_state('answer')
+ else:
+ if self.nextstate == 'idle':
+ global calling
+ calling = True
+ self.args['number'] = l
+ self.set_state('call')
+ calllog('outgoing',l)
+ record('status', 'on-call')
+
+ def check_dtmf(self, f = None):
+ l = recall('dtmf')
+ log("Check dtmf got", l)
+ if len(l):
+ self.args['dtmf'] = l
+ self.set_state('dtmf')
+ record('dtmf','')
+
+ def check_flightmode(self, f = None):
+ try:
+ fd = open("/var/lib/misc/flightmode/active")
+ l = fd.read(1)
+ fd.close()
+ except IOError:
+ l = ""
+ log("check flightmode got", len(l))
+ if len(l) == 0:
+ if self.flightmode:
+ self.flightmode = False
+ if self.suspend_handle.suspended:
+ self.set_state('suspend')
+ else:
+ self.set_state('init')
+ else:
+ if not self.flightmode:
+ self.flightmode = True
+ self.set_state('flight')
+
+ def do_suspend(self):
+ self.suspend_pending = True
+ if self.nextstate not in ['flight', 'resume']:
+ print "do suspend sets suspend"
+ self.set_state('suspend')
+ else:
+ print "do suspend avoids suspend"
+ self.abort_timeout()
+ return False
+
+ def do_resume(self):
+ if self.nextstate == 'suspend':
+ self.set_state('resume')
+
+ def set_state(self, state):
+ # this happens asynchronously so we must be careful
+ # about changing things. Just record the new state
+ # and abort any timeout
+ log("state should become", state)
+ self.nextstate = state
+ self.statechanged = True
+ self.abort_timeout()
+
+ def advance(self):
+ # 'advance' is called by a 'Task' when it has finished
+ # It may have called 'set_state' first either to report
+ # an error or to effect a regular state change
+ now = int(time.time()*1000)
+ if self.tasknum != None:
+ self.lastrun[self.tasknum] = now
+ self.tasknum = None
+ (t, delay) = self.next_cmd()
+ log("advance %s chooses %d, %d" % (self.gstate, t, delay))
+ if delay and self.statechanged:
+ # time to effect 'set_state' synchronously
+ self.statechanged = False
+ self.gstate = self.nextstate
+ log("state becomes", self.gstate)
+ n = len(control[self.gstate])
+ self.lastrun = n * [0]
+ t, delay = self.next_cmd()
+
+ if delay and self.suspend_pending:
+ self.suspend_pending = False
+ print "advance calls release"
+ self.suspend_handle.release()
+
+ if delay:
+ log("Sleeping for %f seconds" % (delay/1000.0))
+ self.set_timeout(delay)
+ else:
+ self.tasknum = t
+ self.state = {}
+ control[self.gstate][t].start(self)
+
+ def takeline(self, line):
+
+ if self.extra:
+ # an async message is multi-line and we need to handle
+ # the extra line.
+ if not self.extra.handle_extra(self, line):
+ self.extra = None
+ return False
+
+ if line == None:
+ self.set_state('reset')
+ self.advance()
+ if not line:
+ return False
+
+ # Check for an async message
+ for m in async:
+ mt = m.match(line)
+ if mt:
+ m.handle(self, line, mt)
+ if m.handle_extra:
+ self.extra = m
+ return False
+
+ # else pass it to the task
+ if self.tasknum != None:
+ control[self.gstate][self.tasknum].takeline(self, line)
+
+ def timedout(self):
+ if self.tasknum == None:
+ self.advance()
+ else:
+ control[self.gstate][self.tasknum].timeout(self)
+
+ def next_cmd(self):
+ # Find a command to execute, or a delay
+ # return (cmd,time)
+ # cmd is an index into control[state],
+ # time is seconds until try something
+ mindelay = 60*60*1000
+ if self.gstate == None:
+ return (0, mindelay)
+ cs = control[self.gstate]
+ n = len(cs)
+ now = int(time.time()*1000)
+ for i in range(n):
+ if self.lastrun[i] == 0 or (cs[i].repeat and
+ self.lastrun[i] + cs[i].repeat <= now):
+ return (i, 0)
+ if cs[i].repeat:
+ delay = (self.lastrun[i] + cs[i].repeat) - now;
+ if delay < mindelay:
+ mindelay = delay
+ return (0, mindelay)
+
+class CarrierDetect(AtChannel):
+ # on the hso modem in the GTA04, the 'NO CARRIER' signal
+ # arrives on the 'Modem' port, not on the 'Application' port.
+ # So we listen to the 'Modem' port, and report any
+ # 'NO CARRIER' we see - or indeed anything that we see.
+ def __init__(self, path, main):
+ AtChannel.__init__(self, path = path)
+ self.main = main
+
+ def takeline(self, line):
+ self.main.takeline(line)
+
+class SysfsWatcher:
+ # watch for changes on a sysfs file and report them
+ # We read the content, report that, wait for a change
+ # and report again
+ def __init__(self, path, action):
+ self.path = path
+ self.action = action
+ self.fd = open(path, "r")
+ self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read)
+ self.read()
+
+ def read(self, *args):
+ self.fd.seek(0)
+ try:
+ r = self.fd.read(4096)
+ except IOerror:
+ return True
+ self.action(r)
+ return True
+
+try:
+ os.mkdir("/run/gsm-state")
+except:
+ pass
+
+calling = False
+a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem')
+print "GsmD started"
+
+try:
+ f = open("/sys/class/gpio/gpio176/edge", "w")
+except IOError:
+ f = None
+if f:
+ f.write("rising")
+ f.close()
+ w = SysfsWatcher("/sys/class/gpio/gpio176/value",
+ lambda l: maybe_sms(l, a))
+else:
+ import evdev
+ def check_evt(dc, mom, typ, code, val):
+ if typ == 1 and val == 1:
+ # keypress
+ maybe_sms("", a)
+ try:
+ f = evdev.EvDev("/dev/input/incoming", check_evt)
+ except:
+ f = None
+c = gobject.main_context_default()
+while True:
+ c.iteration()
--- /dev/null
+The state machine has become a mess and doesn't work.
+
+I regularly forget to call 'advance' and I'm not even sure where
+it should be called.
+
+Signals for file changes via dnotify come at arbitrary times and
+cause race problems.
+
+Some states need to progress fully before being changed and there
+is no mechanism to ensure that.
+
+So I need to make it all cleaner. First I need to understand what we have.
+
+
+1/ timeouts
+
+
+2/ response from GSM module
+
+3/ async message from GSM module
+ record details
+ run gsm-getsms
+ set alerts
+
+4/ file changes
+ 'flightmode'
+ check_flightmode -> suspend or init, or to_flight
+ 'call'
+ check_call ->
+ empty string might hang up
+ 'answer' should answer if 'incoming'
+ 'number' should go on-call
+
+ 'dtmf'
+ switch to 'dtmf' state which then goes back to on-call
+
+5/ suspend notification:
+ do_suspend : state to suspend
+ do_resume : state to init
+
+states:
+
+ to_flight -> flight
+ reset
+ suspend
+ listenerr
+ init
+ idle
+ incoming
+ answer
+ call
+ dtmf
+ hangup
+ on-call
+
+Actions are:
+ AtAction - this is only one with stages.
+ PowerAction
+ SuspendComplete - should combine with BlockSuspendAction - DONE
+ ChangeStateAction
+ CheckSMS
+ RouteVoice
+ BlockSuspendAction
+
+
+We have a transition into a state, then being in a state.
+so to_flight and flight
+ call and on-call
+ answer and on-call
+ hangup and idle
+
+Sometimes a sequence of events must complete in order. Sometimes they
+can be interrupted. 'suspend' performs a number of actions which
+might abort the suspend. e.g. it checks call status. Then it
+performs a number of actions that should complete. So maybe have a
+'CheckState' action which is allowed to switch state if a request has
+been made. This might be implicit at the end of every state, probably
+excepting states that have an explicit ChangeStateAction.
+
+The true states are:
+ idle
+ suspend
+ flight
+ incoming - blocks suspend
+ on-call - blocks suspend
+
+Transitions are:
+ init : ends in idle
+ to_flight
+ reset
+ answer: incoming -> on-call
+ hangup: incoming or on-call -> idle
+ dtmf: on-call -> on-call
+ call: idle -> on-call
+
+
+Async events check if there is an active task (channel.tasknum).
+If there is not, the timer is reset for 'now'.
+This is done in an 'idle' handler to ensure single-threaded.
+So if tasknum == None, channel.cancel_timeout(), timer_fired()
+
+if 'advance' finds there is nothing to do but wait, it will allow
+suspend to complete if pending.
+
+-----------
+
+When we come out of suspend we call init and that somehow does a hangup.
+We need to have some wake-from-suspend path that is less intense.
+ - DONE
+
+2 probs:
+- set_state will interrupt a timeout which can interfere with AtAction
+ sub states. That is bad.
+- resume is immediately replaced by 'suspend' which hangs around longer
+ than we want... I guess the cpas and cfun and checksms should protect
+ against staying in suspend... but how?
\ No newline at end of file
--- /dev/null
+
+Another possible tapboard:
+Based on Dextr
+
+1 ' A B C D , BS
+S ? E F G H . DEL
+A Z I J K L M RET
+SP N O P Q R S SP
+SP T U V W X Y SP
+
+
+-
+
++ - * { } < > / # [ ] =
+@ $ % ^ & * ( ) ~ ` _ \ |
+tap esc del home
+
+ ! @ $ % ^ & `
+ ( 1 2 3 + ~ )
+ [ 4 5 6 - _ ]
+ { 7 8 9 / \ }
+ < # 0 * = | >
+
+Default is 'lower'
+drag left/right gives caps
+drag left/right then up/down does 'extra'
+'1' switches to 'number' mode until pressed again
+'A' switches to 'caps' and back
+
+
+Need esc, tab, and other function keyss
'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
())
}
- def __init__(self):
+ def __init__(self, mode='lower'):
gtk.rc_parse_string("""
style "tap-button-style" {
GtkWidget::focus-padding = 0
# False with '-shift' for a single shift
# True with '-shift' for a locked shit
self.image_mode = ''
- self.mode = 'lower'
+ self.mode = mode
self.shift = ''
self.locked = None
self.size = 0
--- /dev/null
+#!/usr/bin/env python
+
+# Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#
+# a library to draw a widget for tap-input of text
+#
+# Have a 4x10 array of buttons. Some buttons enter a symbol,
+# others switch to a different array of buttons.
+# The 4 rows aren't the same, but vary like a qwerty keyboard.
+# Row1: 10 buttons
+# Row2: 9 buttons offset by half a button
+# Row3: 10 buttons just like Row1
+# Row4: 5 buttons each double-width
+#
+# vertial press/drag is passed to caller as movement.
+# press/hold is passed to caller as 'unmap'.
+# horizontal press/drag modifies the selected button
+# on the first 3 rows, it shifts
+# on NUM it goes straight to punctuation
+#
+# Different configs are:
+# lower-case alpha, with minimal punctuation
+# upper-case alpha, with different punctuation
+# numeric with phone and calculator punctuation
+# Remaining punctuation with some cursor control.
+#
+# Bottom row is normally:
+# Shift NUM SPC ENTER BackSpace
+# When 'shift' is pressed, the keyboard flips between
+# upper/lower or numeric/punc
+# and bottom row maybe should become:
+# lock control alt ... something.
+
+import gtk, pango, gobject
+
+keymap = {}
+
+keymap['lower'] = [
+ ['\'','a','b','c','d',','],
+ ['?', 'e','f','g','h','.'],
+ ['z', 'i','j','k','l','m'],
+ ['n', 'o','p','q','r','s'],
+ ['t', 'u','v','w','x','y']
+]
+keymap['caps'] = [
+ ['"', 'A','B','C','D',';'],
+ ['!', 'E','F','G','H',':'],
+ ['Z', 'I','J','K','L','M'],
+ ['N', 'O','P','Q','R','S'],
+ ['T', 'U','V','W','X','Y']
+]
+keymap['lower-shift'] = keymap['caps']
+keymap['lower-xtra'] = [
+ [' ',' ',' ',' ',' ',' '],
+ [' ','1','2','3','+',' '],
+ [' ','4','5','6','-',' '],
+ [' ','7','8','9',' ',' '],
+ [' ','*','0','#',' ',' ']
+]
+keymap['caps-xtra'] = keymap['lower-xtra']
+keymap['number'] = [
+ ['!','@','$','%','^','&','`'],
+ ['(','1','2','3','+','~',')'],
+ ['[','4','5','6','-','_',']'],
+ ['{','7','8','9','/','\\','}'],
+ ['<','#','0','*','=','|','>'],
+]
+
+class TapBoard(gtk.VBox):
+ __gsignals__ = {
+ 'key' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_STRING,)),
+ 'move': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_INT, gobject.TYPE_INT)),
+ 'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ ())
+ }
+ def __init__(self):
+ gtk.rc_parse_string("""
+ style "tap-button-style" {
+ GtkWidget::focus-padding = 0
+ GtkWidget::focus-line-width = 1
+ xthickness = 1
+ ythickness = 0
+ }
+ widget "*.tap-button" style "tap-button-style"
+ """)
+
+ gtk.VBox.__init__(self)
+ self.keysize = 44
+ self.aspect = 1.4
+ self.width = int(10*self.keysize)
+ self.height = int(4*self.aspect*self.keysize)
+
+ self.dragx = None
+ self.dragy = None
+ self.moved = False
+ self.xmoved = False
+ self.xmin = 100000
+ self.xmax = 0
+
+ self.isize = gtk.icon_size_register("mine", 40, 40)
+
+ self.button_timeout = None
+
+ self.buttons = []
+
+ self.set_homogeneous(True)
+
+ for row in range(5):
+ h = gtk.HBox()
+ h.show()
+ self.add(h)
+ bl = []
+ h.set_homogeneous(True)
+ for col in range(8):
+ b = self.add_button(None, self.tap, (row,col), h)
+ bl.append(b)
+ self.buttons.append(bl)
+
+ # mode can be 'lower', 'upper', 'func' (later) or 'number'
+ self.image_mode = ''
+ self.mode = 'lower'
+ self.size = 0
+ self.connect("size-allocate", self.update_buttons)
+ self.connect("realize", self.update_buttons)
+
+
+ def add_button(self, label, click, arg, box, font = None):
+ if not label:
+ b = gtk.Button()
+ b.set_name("tap-button")
+ elif label[0:4] == 'gtk-':
+ img = gtk.image_new_from_stock(label, self.isize)
+ img.show()
+ b = gtk.Button()
+ b.add(img)
+ else:
+ b = gtk.Button(label)
+ b.show()
+ b.set_property('can-focus', False)
+ if font:
+ b.child.modify_font(font)
+ b.connect('button_press_event', self.press, arg)
+ b.connect('button_release_event', self.release, click, arg)
+ b.connect('motion_notify_event', self.motion)
+ b.add_events(gtk.gdk.POINTER_MOTION_MASK|
+ gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+ box.add(b)
+ return b
+
+ def update_buttons(self, *a):
+ if self.window == None:
+ return
+
+ alloc = self.buttons[0][0].get_allocation()
+ w = alloc.width; h = alloc.height
+ if w > h:
+ size = h
+ else:
+ size = w
+ size -= 6
+ if size <= 10 or size == self.size:
+ return
+ #print "update buttons", size
+ self.size = size
+
+ # For each button in 3x3 we need 10 images,
+ # one for initial state, and one for each of the new states
+ # So there are two fonts we want.
+ # First we make the initial images
+ fdsingle = pango.FontDescription('sans 10')
+ fdsingle.set_absolute_size(size / 1.1 * pango.SCALE)
+ fdword = pango.FontDescription('sans 10')
+ fdword.set_absolute_size(size / 2 * pango.SCALE)
+ fdxtra = pango.FontDescription('sans 10')
+ fdxtra.set_absolute_size(size/2.3 * pango.SCALE)
+
+ bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
+ fg = self.get_style().fg_gc[gtk.STATE_NORMAL]
+ red = self.window.new_gc()
+ red.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('red')))
+ blue = self.window.new_gc()
+ blue.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('blue')))
+ base_images = {}
+ for mode in keymap.keys():
+ if mode[-5:] == 'xtra':
+ continue
+ base_images[mode] = 30*[None]
+ for row in range(5):
+ for col in range(6):
+ if not self.buttons[row][col+1]:
+ continue
+ sym = keymap[mode][row][col]
+ pm = gtk.gdk.Pixmap(self.window, size, size)
+ pm.draw_rectangle(bg, True, 0, 0, size, size)
+ if len(sym) == 1:
+ self.modify_font(fdsingle)
+ else:
+ self.modify_font(fdword)
+ layout = self.create_pango_layout(sym[0:3])
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ colour = fg
+ if sym in "aeiouAEUIO0123456789":
+ colour = blue
+ pm.draw_layout(colour,
+ int(size/2 - ew/2),
+ int(size/2 - eh/2),
+ layout)
+ if (mode+'-xtra') in keymap:
+ self.modify_font(fdxtra)
+ sym = keymap[mode+'-xtra'][row][col]
+ layout = self.create_pango_layout(sym)
+ (ink, (ex,ey,ew2,eh2)) = layout.get_pixel_extents()
+ colour = fg
+ if sym in "aeiouAEIOU0123456789":
+ colour = blue
+ pm.draw_layout(colour, int(size/2)+ew2,int(size/2)-eh2,layout)
+ im = gtk.Image()
+ im.set_from_pixmap(pm, None)
+ base_images[mode][row*6+col] = im
+
+ base_images['common'] = 24*[None]
+ for sym in [ (0,0,"123"), (1,0,"ABC"), (2,0,"Func"),
+ (0,7,"BkSp"), (1,7,"Ret"), (2,7,"Tab")]:
+ (r,c,s) = sym
+ pm = gtk.gdk.Pixmap(self.window, size, size)
+ pm.draw_rectangle(bg, True, 0, 0, size, size)
+ self.modify_font(fdword)
+ layout = self.create_pango_layout(s)
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ colour = red
+ pm.draw_layout(colour,
+ int(size/2 - ew/2),
+ int(size/2 - eh/2),
+ layout)
+ im = gtk.Image()
+ im.set_from_pixmap(pm, None)
+ base_images['common'][r*8+c] = im
+
+ self.base_images = base_images
+
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(size / 1.5 * pango.SCALE)
+ self.modify_font(fd)
+ self.set_button_images()
+
+ def set_button_images(self):
+ mode = self.mode
+ if self.image_mode == mode:
+ return
+ for row in range(5):
+ if row < 3:
+ for col in [0,7]:
+ b = self.buttons[row][col]
+ b.set_image(self.base_images['common'][row*8+col])
+ for col in range(6):
+ b = self.buttons[row][col+1]
+ if not b:
+ continue
+ im = self.base_images[mode][row*6+col]
+ if im:
+ b.set_image(im)
+ self.image_mode = mode
+
+
+ def tap(self, rc, moved):
+ (row,col) = rc
+ m = self.mode
+ if col == 0 or col == 7:
+ # special buttons
+ if row >= 3:
+ sym = ' '
+ elif rc == (0,7):
+ sym = '\b'
+ elif rc == (1,7):
+ sym = '\n'
+ elif rc == (2,7):
+ sym = '\t'
+ elif rc == (0,0):
+ if m == 'number':
+ self.mode = 'lower'
+ else:
+ self.mode = 'number'
+ self.set_button_images()
+ return
+ elif rc == (1,0):
+ if m == 'caps':
+ self.mode = 'lower'
+ else:
+ self.mode = 'caps'
+ self.set_button_images()
+ return
+ else:
+ return
+ elif moved:
+ if moved == 2 and (self.mode + '-xtra') in keymap\
+ and keymap[self.mode + '-xtra'][row][col-1] != ' ':
+ m = self.mode + '-xtra'
+ else:
+ m = self.mode + '-shift'
+ sym = keymap[m][row][col-1]
+ else:
+ sym = keymap[m][row][col-1]
+ self.emit('key', sym)
+
+ def press(self, widget, ev, arg):
+ self.dragx = int(ev.x_root)
+ self.dragy = int(ev.y_root)
+ self.moved = False
+ self.xmoved = False
+ self.xmin = self.dragx
+ self.xmax = self.dragx
+
+ # press-and-hold makes us disappear
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ self.button_timeout = gobject.timeout_add(500, self.disappear)
+
+ def release(self, widget, ev, click, arg):
+ dx = self.dragx
+ dy = self.dragy
+ y = int(ev.y_root)
+ self.dragx = None
+ self.dragy = None
+
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if self.moved:
+ self.moved = False
+ # If we are half way off the screen, hide
+ root = gtk.gdk.get_default_root_window()
+ (rx,ry,rwidth,rheight,depth) = root.get_geometry()
+ (x,y,width,height,depth) = self.window.get_geometry()
+ xmid = int(x + width / 2)
+ ymid = int(y + height / 2)
+ if xmid < 0 or xmid > rwidth or \
+ ymid < 0 or ymid > rheight:
+ self.hide()
+ else:
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if self.xmoved:
+ if self.xmin < dx and self.xmax > dx:
+ click(arg, 2)
+ elif abs(y-dy) > 40:
+ click(arg, 2)
+ else:
+ click(arg, 1)
+ else:
+ click(arg, 0)
+ self.xmoved = False
+
+ def motion(self, widget, ev):
+ if self.dragx == None:
+ return
+ x = int(ev.x_root)
+ y = int(ev.y_root)
+
+ if (not self.xmoved and abs(y-self.dragy) > 40) or self.moved:
+ if not self.moved:
+ self.emit('move', 0, 0)
+ self.emit('move', x-self.dragx, y-self.dragy)
+ self.moved = True
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if (not self.moved and abs(x-self.dragx) > 40) or self.xmoved:
+ self.xmoved = True
+ if x < self.xmin:
+ self.xmin = x
+ if x > self.xmax:
+ self.xmax = x
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if ev.is_hint:
+ gtk.gdk.flush()
+ ev.window.get_pointer()
+
+ def disappear(self):
+ self.dragx = None
+ self.dragy = None
+ self.emit('hideme')
+
+ def do_buttons(self):
+ self.set_button_images()
+ self.button_timeout = None
+ return False
+
+ def set_buttons_soon(self):
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = gobject.timeout_add(500, self.do_buttons)
+
+
+if __name__ == "__main__" :
+ w = gtk.Window()
+ w.connect("destroy", lambda w: gtk.main_quit())
+ ti = TapBoard()
+ w.add(ti)
+ w.set_default_size(ti.width, ti.height)
+ root = gtk.gdk.get_default_root_window()
+ (x,y,width,height,depth) = root.get_geometry()
+ x = int((width-ti.width)/2)
+ y = int((height-ti.height)/2)
+ w.move(x,y)
+ def pkey(ti, str):
+ print 'key', str
+ ti.connect('key', pkey)
+ def hideme(ti):
+ print 'hidememe'
+ w.hide()
+ ti.connect('hideme', hideme)
+ ti.show()
+ w.show()
+
+ gtk.main()
+
--- /dev/null
+
+#
+# manage a list of current windows and allow a selected
+# window to be raised.
+# I'm using Xlib for this, which doesn't have a built-in event
+# mechanism like gtk does in gobject.
+# So if you want to make sure property change notify events
+# get handled, you need to arrange that read events on
+# winlist.fd are passed to winlist.events.
+# e.g. gobject.io_add_watch(winlist.fd, gobject.IO_IN, winlist.events)
+#
+
+import Xlib.X
+import Xlib.display
+import Xlib.protocol.event
+
+class mywindow:
+ def __init__(self, win, name, pid, id, list):
+ self.id = id
+ self.win = win
+ self.name = name
+ self.pid = pid
+ self.list = list
+
+ def raise_win(self):
+ msg = Xlib.protocol.event.ClientMessage(window = self.win,
+ client_type = self.list.ACTIVE_WINDOW,
+ data = (32, [0,0,0,0,0])
+ )
+ msg.send_event = 1
+ mask = (Xlib.X.SubstructureRedirectMask |
+ Xlib.X.SubstructureNotifyMask)
+ self.list.root.send_event(msg, event_mask = mask)
+ self.win.map()
+ self.win.raise_window()
+ #p = w.query_tree().parent
+ #if p:
+ # p.map()
+ # p.raise_window()
+ self.list.display.flush()
+
+ def close_win(self):
+ msg = Xlib.protocol.event.ClientMessage(window = self.win,
+ client_type = self.list.CLOSE_WINDOW,
+ data = (32, [0,0,0,0,0])
+ )
+ msg.send_event = 1
+ mask = (Xlib.X.SubstructureRedirectMask |
+ Xlib.X.SubstructureNotifyMask)
+ self.list.root.send_event(msg, event_mask = mask)
+ self.list.display.flush()
+
+class winlist:
+ def __init__(self, add_handle = None):
+ self.display = Xlib.display.Display()
+ self.root = self.display.screen().root
+ self.winfo = {}
+ self.windows = ()
+ self.WM_STRUT = self.display.intern_atom('_NET_WM_STRUT')
+ self.CARDINAL = self.display.intern_atom('CARDINAL')
+ self.ACTIVE_WINDOW = self.display.intern_atom('_NET_ACTIVE_WINDOW')
+ self.CLOSE_WINDOW = self.display.intern_atom('_NET_CLOSE_WINDOW')
+ self.NAME = self.display.intern_atom('WM_NAME')
+ self.STRING = self.display.intern_atom('STRING')
+ self.PID = self.display.intern_atom('_NET_WM_PID')
+ self.LIST = self.display.intern_atom('_NET_CLIENT_LIST_STACKING')
+ self.WINDOW = self.display.intern_atom('WINDOW')
+
+ self.fd = self.display.fileno()
+ self.change_handle = None
+ self.add_handle = add_handle
+ self.del_handle = None
+
+ self.root.change_attributes(event_mask = Xlib.X.PropertyChangeMask )
+ self.get_list()
+
+
+ def add_win(self, id):
+ if id in self.winfo:
+ return self.winfo[id]
+ w = self.display.create_resource_object('window', id)
+ p = w.get_property(self.WM_STRUT, self.CARDINAL, 0, 100)
+ self.winfo[id] = None
+ if p:
+ return None
+ p = w.get_property(self.NAME, self.STRING, 0, 100)
+ if p and p.format == 8:
+ name = p.value
+ name = name.replace('&','&')
+ name = name.replace('<','<')
+ name = name.replace('>','>')
+ else:
+ return None
+
+ p = w.get_property(self.PID, self.CARDINAL, 0, 100)
+ if p and p.format == 32:
+ pid = p.value[0]
+ else:
+ pid = 0
+
+ self.winfo[id] = mywindow(w, name, pid, id, self)
+
+ if self.add_handle:
+ self.add_handle(self.winfo[id])
+ return self.winfo[id]
+
+
+ def get_list(self):
+ l = self.root.get_property(self.LIST, self.WINDOW, 0, 100)
+ windows = []
+ for w in l.value:
+ if self.add_win(w):
+ windows.append(w)
+ self.windows = windows
+ self.clean_winfo()
+ if self.change_handle:
+ self.change_handle()
+
+ def clean_winfo(self):
+ togo = []
+ for w in self.winfo:
+ if w not in self.windows:
+ togo.append(w)
+ for w in togo:
+ del self.winfo[w]
+ if self.del_handle:
+ self.del_handle(w)
+
+ def events(self, *a):
+ i = self.display.pending_events()
+ while i > 0:
+ event = self.display.next_event()
+ self.handle_event(event)
+ i = i - 1
+ return True
+
+ def handle_event(self, event):
+ if event.atom != self.LIST:
+ return False
+ self.get_list()
+ return True
+
+ def top(self, num = 0):
+ if num > len(self.windows) or num < 0:
+ return None
+ return self.winfo[self.windows[-1-num]]
+
+ def on_change(self, func, add=None, delete=None):
+ self.change_handle = func
+ self.add_handle = add
+ self.del_handle = delete
+
+
+if __name__ == '__main__':
+ w = winlist()
+ for i in w.winfo:
+ print i, w.winfo[i].name
+ while 1:
+ event = w.display.next_event()
+ if w.handle_event(event):
+ print "First is", w.top(1).name
+ w.top(1).raise_win()
+
--- /dev/null
+dhcp-range=192.168.202.2,192.168.202.6,255.255.255.248,6h
+dhcp-range=192.168.202.10,192.168.202.14,255.255.255.248,6h
+dhcp-range=192.168.202.18,192.168.202.22,255.255.255.248,6h
--- /dev/null
+
+auto lo
+iface lo inet loopback
+
+auto usb0=usb0-p2p
+
+# usb0-client is used when no other network is available
+iface usb0-client inet static
+ pre-up rmmod g_ether || true
+ pre-up modprobe g_ether host_addr=56:88:91:5F:AF:81
+ address 192.168.202.1
+ netmask 255.255.255.248
+ network 192.168.202.0
+ gateway 192.168.202.2
+ post-down rmmod g_ether
+ up echo nameserver 192.168.1.3 >/etc/resolv.conf
+
+# usb0-p2p (peer-to-peer) is used when something else provides
+# the default route but we don't want the laptop to use us to get it.
+iface usb0-p2p inet static
+ pre-up rmmod g_ether || true
+ pre-up modprobe g_ether host_addr=56:88:91:5F:AF:81
+ address 192.168.202.1
+ netmask 255.255.255.248
+ network 192.168.202.0
+ post-down rmmod g_ether
+
+# usb0-hotspot is used when usb is used to tether to a hotspit.
+# Note it has a different host_addr so notebook knows to act differently
+# and use DHCP to get a route.
+iface usb0-hotspot inet static
+ pre-up rmmod g_ether || true
+ pre-up modprobe g_ether host_addr=56:88:91:5F:AF:82
+ address 192.168.202.1
+ netmask 255.255.255.248
+ network 192.168.202.0
+ post-down rmmod g_ether
+
+iface wlan0-hotspot inet static
+ address 192.168.202.9
+ netmask 255.255.255.248
+ network 192.168.202.8
+
+ifbase pan0-hostspot inet static
+ address 192.168.202.17
+ netmask 255.255.255.248
+ network 192.168.202.16
--- /dev/null
+#!/usr/bin/env python
+
+#TODO
+# handle iptables masquerade directly so it can be tuned to the IP address
+# discovered.
+# up iptables -t nat -A POSTROUTING -s 192.168.202.16/29 -j MASQUERADE
+# down iptables -t nat -D POSTROUTING -s 192.168.202.16/29 -j MASQUERADE
+# This should be done on any interface that is the 'hotspot'
+#DONE - rfkill unblock
+#DONE - kill children on exit
+#- add 3G support
+# - connect
+# - disconnect
+# - poll for status
+# - configure access point
+#- USB detect presence of connector?
+#- wifi:
+# - report crypto status and strength
+#DONE - extract 'id=' from CONNECTED to find current
+#DONE - notice when wifi goes away
+# CTRL-EVENT-DISCONNECTED
+#DONE - if no dhcp response for a while, reassociate
+# - config page to:
+# list known and visible networks
+# disble/enable, forget, set-password
+# make sure to save config
+# kill supplicant when done?
+#DONE - ensure label is refreshed on different connect and disconnect stages.
+
+# Manage networks for the openmoko phoenux
+# There are 4 devices (unless I add VPN support)
+# USB, WIFI, WWAN, Bluetooth
+#
+# USB is normally on, though it can be turned off to
+# allow a different USB widget.
+# WIFI, WWAN, Bluetooth need to be explicitly enabled
+# for now at least
+# There is only one default route, and one DNS server
+# If WWAN is active, it provides route and DNS else
+# if WIFI is active and DHCP responds, it provides route and DNS
+# else if BT is active and DHCP responds, it provides route and DNS
+# else USB should provide route and DNS
+#
+# When we have a route, we provide DHCP to other interfaces
+# using dnsmasq, and provide masquarading to Internet.
+#
+# Main page shows each interface with status including
+# IP address
+# One can be selected, and bottom buttons can be used to
+# enable / disable, hotspot, and configure
+# 'configure' goes to a new page, different for each interface.
+#
+# listen configures the interface to allow incoming...
+#
+# WWAN: configure allows APN to be set. This is stored per SIM card.
+# WIFI: lists known and visible ESSIDs and show password which can be
+# editted. Also bottom buttons for 'activate' and 'forget' or 'allow'.
+# BT: lists visible peers which support networking and allows 'bind'
+# or 'connect'
+# USB: ???
+#
+
+# WWAN in managed using atchan talking to the ttyHS_Control port
+# We cannot hold it open, so poll status occasionally.
+# ifconfig and route is managed directly.
+# WIFI is managed by talking to wpa_supplicant over a socket.
+# ifconfig up/down might need to be done directly for power management
+# udhcp is managed directly
+# BT ?? don't know yet
+# USB if managed with ifup/ifdown
+#
+# dnsmasq is stopped and restarted as needed with required options.
+#
+# Addresses:
+# This program doesn't know about specific IP addresses, that all
+# lives in config files. However the plan is as follows:
+# Everything lives in a single class-C: 192.168.202.0/24
+# usb-server gets 6 addresses: 0/29 1 is me, 2-7 is you
+# usb-client gets same 6 addresses: 0/29 2 is you, 1 is me
+# wifi-hotspot gets 6 addresses: 8/29 9 is me, 10-14 are for clients
+# bt-server gets 6 addresses: 16/29 17 is me, 18-23 are for clients
+#
+# dnsmasq only responds to DHCP on 1, 9, 17
+#
+# only one of 3g, wifi, bt, usb-client can be active at at time
+# They set the server in resolv.conf, and set a default.
+# usb-hotspot, wifi-hotspot, bt-hotspot set up IP masqurading.
+#
+#
+
+# so...
+# Each of 'usb', 'wifi', 'bt', can be in 'hotspot' mode at any time.
+# Of 'wifi', 'gsm', 'bt', 'usb', the first that is active and accessable
+# and not configured for 'hotspot' is configured as default route and others
+# that are not 'hotspot' are disabled - except 'usb' which becomes
+# 'p2p' for local traffic only.
+#
+# An interface can be in one of several states:
+# - hotspot (not gsm) - active and providing access
+# - disabled - explicitly disabled, not to be used
+# - active - has found a connection, provides the default route
+# - over-ridden - a higher-precedence interface is active
+# - pending - no higher interface is active, but haven't found connection yet.
+#
+# When 'active' is released, we switch to 'disabled' and deconfig interface
+# When 'active' is pressed, we switch to 'pending' and rescan which might
+# switch to 'over-ridden', or might start configuring, leading to 'active'
+# When 'hotspot' is pressed, we deconfig interface if up, then enable hotspot
+# When 'hotspot' is released, we deconfig and switch to 'pending'
+
+
+import gtk, pango
+from subprocess import Popen, PIPE
+import suspend
+
+class iface:
+ def __init__(self, parent):
+ self.state = 'disabled'
+ self.parent = parent
+ self.make_button()
+ self.can_hotspot = True
+ self.hotspot_net = None
+ self.hotspot = False
+ self.addr = 'No Address'
+ self.set_label()
+
+ def make_button(self):
+ self.button = gtk.ToggleButton(self.name)
+ self.button.set_alignment(0,0.5)
+ self.button.show()
+ self.button.connect('pressed', self.press)
+
+ def set_label(self):
+ self.addr = self.get_addr()
+ if self.state == 'disabled':
+ config = ''
+ else:
+ self.get_config()
+ config = ' (%s)' % self.config
+ if self.hotspot:
+ hs = 'hotspot '
+ else:
+ hs = ''
+ self.label = ('<span color="blue" size="30000">%s</span>\n<span size="12000"> %s%s\n%s</span>\n'
+ % (self.name, hs, self.state, config))
+ if self.state == 'active' and not self.hotspot:
+ self.label = self.label + '<span size="12000">Gateway: %s</span>' % self.get_default()
+ self.button.child.set_markup(self.label)
+
+ if (self.hotspot and
+ self.addr != self.hotspot_net and
+ self.addr != 'No Address') :
+ if self.hotspot_net:
+ Popen(['iptables','-t','nat','-D','POSTROUTING','-s',
+ self.hotspot_net,'-j','MASQUERADE']).wait()
+ self.hotspot_net = self.addr
+ Popen(['iptables','-t','nat','-A','POSTROUTING','-s',
+ self.hotspot_net,'-j','MASQUERADE']).wait()
+ if not self.hotspot and self.hotspot_net != None:
+ Popen(['iptables','-t','nat','-D','POSTROUTING','-s',
+ self.hotspot_net,'-j','MASQUERADE']).wait()
+ self.hotspot_net = None
+
+ def get_config(self):
+ self.config = '-'
+
+ def press(self, ev):
+ self.parent.select(self)
+
+ def get_addr(self):
+ p = Popen(['ip', 'add', 'show', 'dev', self.iface],
+ stdout = PIPE, close_fds = True)
+ ip = 'No Address'
+ for l in p.stdout:
+ l = l.strip().split()
+ if l[0] == 'inet':
+ ip = l[1]
+ p.wait()
+ return ip
+
+ def get_default(self):
+ p = Popen(['ip', 'route', 'list', 'match', '128.0.0.1'],
+ stdout = PIPE, close_fds = True)
+ route = 'none'
+ for l in p.stdout:
+ l = l.strip().split()
+ if l[0] == 'default' and l[1] == 'via':
+ route = l[2]
+ return route
+
+ def rfkill(self, state):
+ Popen(['rfkill', state, self.rfkill_name]).wait()
+
+ def shutdown(self):
+ pass
+
+ def config_widget(self):
+ return None
+
+from socket import *
+import os, gobject, time
+from listselect import ListSelect
+class WLAN_iface(iface):
+ # We run wpa_supplicant and udhcpc as needed, killing them when done
+ # For 'hotspot' we also run 'ifup' or 'ifdown'
+ # When wpa_supplicant is running, we connect to the socket to
+ # communicate and add new networks
+ # Some wpa_cli commands:
+ # ATTACH -> results in 'OK'
+ # PING -> results in 'PONG' (wpa_cli does this every 5 seconds)
+ # LIST_NETWORKS
+ # 0 LinuxRules any [CURRENT]
+ # 1 n-squared any
+ # 2 JesusIsHere any
+ # 3 TorchNet any
+ # 4 freedom any
+
+ #
+ # Some async responses:
+ # <3>CTRL-EVENT-BSS-ADDED 0 00:60:64:24:0a:22
+ # <3>CTRL-EVENT-BSS-ADDED 1 00:04:ed:1e:98:48
+ # <3>CTRL-EVENT-SCAN-RESULTS
+ # <3>Trying to associate with 00:60:64:24:0a:22 (SSID='LinuxRules' freq=2427 MHz)
+ # <3>Associated with 00:60:64:24:0a:22
+ # <3>WPA: Key negotiation completed with 00:60:64:24:0a:22 [PTK=CCMP GTK=TKIP]
+ # <3>CTRL-EVENT-CONNECTED - Connection to 00:60:64:24:0a:22 completed (auth) [id=0 id_str=]
+ # <3>CTRL-EVENT-BSS-REMOVED 1 00:04:ed:1e:98:48
+ # <3>CTRL-EVENT-DISCONNECTED bssid=..... reason=0
+
+ def __init__(self, parent):
+ self.name = 'WLAN'
+ self.rfkill_name = 'wifi'
+ self.iface = 'wlan0'
+ self.ssid = 'unset'
+ self.supplicant = None
+ self.udhcpc = None
+ self.netlist = {}
+ self.scanlist = {}
+ self.configing = False
+ self.pending = None
+ self.to_send = []
+ iface.__init__(self, parent)
+ self.set_label()
+ self.cfg = None
+
+ def get_config(self):
+ self.config = self.addr + ' ESSID=%s' % self.ssid
+
+ def activate(self):
+ self.rfkill('unblock')
+ self.supplicant_up()
+
+ def shutdown(self):
+ self.udhcpc_close()
+ self.supplicant_close()
+ Popen('ifconfig wlan0 down', shell=True).wait()
+ self.rfkill('block')
+
+ def supplicant_up(self):
+ if self.supplicant:
+ return
+ self.supplicant = Popen(['wpa_supplicant','-i','wlan0',
+ '-c','/etc/wpa_supplicant.conf','-W'],
+ close_fds = True)
+ try:
+ os.unlink('/run/wpa_ctrl_netman')
+ except:
+ pass
+ s = socket(AF_UNIX, SOCK_DGRAM)
+ s.bind('/run/wpa_ctrl_netman')
+ ok = False
+ while not ok:
+ try:
+ s.connect('/run/wpa_supplicant/wlan0')
+ ok = True
+ except:
+ time.sleep(0.1)
+ self.sock = s
+ self.watch = gobject.io_add_watch(s, gobject.IO_IN, self.recv)
+ self.request('ATTACH')
+
+ def restart_dhcp(self):
+ # run udhcpc -i wlan0 -f (in foreground)
+ # recognise message like
+ # Lease of 192.168.1.184 obtained, lease time 600
+
+ self.udhcpc_close()
+ env = os.environ.copy()
+ if self.hotspot:
+ env['NO_DEFAULT'] = 'yes'
+ self.udhcpc = Popen(['udhcpc','--interface=wlan0','--foreground',
+ '--script=/etc/wifi-udhcpc.script'],
+ close_fds = True,
+ env = env,
+ stdout = PIPE)
+ self.udhcpc_watch = gobject.io_add_watch(
+ self.udhcpc.stdout, gobject.IO_IN,
+ self.dhcp_read)
+
+ def dhcp_read(self, dir, foo):
+ if not self.udhcpc:
+ return False
+ m = self.udhcpc.stdout.readline()
+ if not m:
+ return False
+ l = m.strip().split()
+ print 'dhcp got', l
+ if len(l) >= 4 and l[0] == 'Lease' and l[3] == 'obtained,' and self.state != 'active':
+ self.state = 'active'
+ self.parent.update_active()
+ self.set_label()
+ if self.checker:
+ gobject.source_remove(self.checker)
+ self.checker = None
+ return True
+
+ def request(self, msg):
+ if self.pending:
+ self.to_send.append(msg)
+ return
+ self.pending = msg
+ self.sock.sendall(msg)
+ print 'sent request', msg
+
+ def recv(self, dir, foo):
+ try:
+ r = self.sock.recv(4096)
+ except error:
+ return False
+ if not r:
+ self.supplicant_close()
+ return False
+
+ if r[0] == '<':
+ self.async(r[r.find('>')+1:])
+ return True
+ elif self.pending == 'ATTACH':
+ # assume it is OK
+ self.request('LIST_NETWORKS')
+ elif self.pending == 'LIST_NETWORKS':
+ self.take_list(r)
+ elif self.pending == 'SCAN_RESULTS':
+ self.take_scan(r)
+
+ self.pending = None
+ if len(self.to_send) > 0:
+ self.request(self.to_send.pop(0))
+ return True
+
+ def udhcpc_close(self):
+ if self.udhcpc:
+ gobject.source_remove(self.udhcpc_watch)
+ self.udhcpc.terminate()
+ self.udhcpc.wait()
+ self.udhcpc = None
+ self.udhcpc_watch = None
+
+ def supplicant_close(self):
+ if self.supplicant:
+ self.supplicant.terminate()
+ gobject.source_remove(self.watch)
+ self.watch = None
+ self.supplicant.wait()
+ self.supplicant = None
+
+ def async(self, mesg):
+ print 'GOT ', mesg
+ print 'got (%s)' % mesg[:20]
+ if mesg[:20] == 'CTRL-EVENT-CONNECTED':
+ p = mesg.find('id=')
+ if p > 0:
+ id = mesg[p+3:].split()[0]
+ for ssid in self.netlist:
+ if id == self.netlist[ssid]:
+ self.ssid = ssid
+ self.set_label()
+ self.checker = gobject.timeout_add(30000, self.reassoc)
+ return self.restart_dhcp()
+ if mesg[:23] == 'CTRL-EVENT-DISCONNECTED':
+ if self.state == 'active':
+ self.state = 'pending'
+ self.udhcpc_close()
+ self.set_label()
+ self.parent.update_active()
+ return
+ if mesg[:23] == 'CTRL-EVENT-SCAN-RESULTS':
+ self.request('SCAN_RESULTS')
+ return
+ if mesg[:19] == 'Trying to associate':
+ l = mesg.split("'")
+ print 'len', len(l)
+ if len(l) > 1:
+ print 'ssid = ', l[1]
+ self.ssid = l[1]
+ return
+
+ def reassoc(self):
+ if self.state == 'active':
+ return False
+ self.request('REASSOCIATE')
+ self.checker = gobject.timeout_add(30000, self.reassoc)
+ return False
+
+ def take_list(self, mesg):
+ self.netlist = {}
+ for line in mesg.split('\n'):
+ if line != '' and line[0:7] != 'network':
+ l = line.split('\t')
+ self.netlist[l[1]] = l[0]
+ print 'netlist', self.netlist
+ self.fill_list()
+
+ def take_scan(self, mesg):
+ self.scanlist = {}
+ for line in mesg.split('\n'):
+ if line != '' and line[:5] != 'bssid':
+ l = line.split('\t')
+ self.scanlist[l[4]] = (l[2],l[3])
+ print 'scan', self.scanlist
+ self.fill_list()
+
+ def config_widget(self):
+ if self.cfg == None:
+ self.make_cfg()
+ self.fill_list()
+ self.configing = True
+ self.supplicant_up()
+ self.request('SCAN')
+ return self.cfg
+
+ def make_cfg(self):
+ # config widget is an entry, a ListSel, and a button row
+ cfg = gtk.VBox()
+ entry = gtk.Entry()
+ ls = ListSelect()
+ bb = gtk.HBox()
+
+ cfg.show()
+ cfg.pack_start(entry, expand = False)
+ entry.show()
+
+ cfg.pack_start(ls, expand = True)
+ ls.show()
+ ls.set_zoom(40)
+ cfg.pack_start(bb, expand = False)
+ bb.show()
+ bb.set_homogeneous(True)
+ bb.set_size_request(-1, 80)
+
+ entry.modify_font(self.parent.fd)
+ self.add_button(bb, 'Allow', self.allow)
+ self.add_button(bb, 'Forget', self.forget)
+ self.add_button(bb, 'Set Key', self.set_key)
+ self.add_button(bb, 'Done', self.done)
+
+ self.cfg = cfg
+ self.key_entry = entry
+ self.net_ls = ls
+
+ def add_button(self, bb, name, cmd):
+ btn = gtk.Button()
+ btn.set_label(name)
+ btn.child.modify_font(self.parent.fd)
+ btn.show()
+ bb.pack_start(btn, expand = True)
+ btn.connect('clicked', cmd)
+
+ def allow(self, x):
+ self.net_ls.reconfig(self.net_ls,1)
+ print 'alloc is', self.net_ls.get_allocation()
+ pass
+ def forget(self, x):
+ pass
+ def set_key(self, x):
+ pass
+ def done(self, x):
+ self.configing = False
+ self.parent.deconfig()
+
+ def fill_list(self):
+ if not self.configing:
+ return
+ l = [('none','green')]
+ for i in self.scanlist:
+ if i in self.netlist:
+ l.append((i, 'blue'))
+ else:
+ l.append((i, 'black'))
+ for i in self.netlist:
+ if i not in self.scanlist:
+ l.append((i, 'red'))
+ print 'filled list', l
+ self.net_ls.list = l
+ self.net_ls.list_changed()
+
+ def config_hotspot(self):
+ self.rfkill('unblock')
+ self.supplicant_up()
+
+
+class WWAN_iface(iface):
+ def __init__(self, parent):
+ self.name = 'GSM/3G'
+ self.rfkill_name = 'wwan'
+ self.conf = None
+ self.iface = 'hso0'
+ self.APN = 'INTERNET'
+ iface.__init__(self, parent)
+ self.can_hotspot = False
+
+ def get_config(self):
+ self.config = self.addr + ' APN=%s' % self.APN
+
+ def activate(self):
+ self.rfkill('unblock')
+ Popen('/usr/local/bin/gsm-data up', shell=True).wait()
+ self.state = 'active'
+
+ def shutdown(self):
+ Popen('/usr/local/bin/gsm-data down', shell=True).wait()
+ self.rfkill('block')
+
+class BT_iface(iface):
+ def __init__(self, parent):
+ self.name = 'BT'
+ self.iface = 'pan0'
+ self.rfkill_name = 'bluetooth'
+ iface.__init__(self, parent)
+
+class USB_iface(iface):
+ def __init__(self, parent):
+ self.name = 'USB'
+ self.conf = 'p2p'
+ self.iface = 'usb0'
+ self.state = 'pending'
+ iface.__init__(self, parent)
+ self.set_label()
+
+ def get_config(self):
+ self.config = self.addr
+
+ def setconf(self, mode):
+ if self.conf == mode:
+ return
+ if mode:
+ cmd = 'ifdown --force usb0; ifup usb0=usb0-%s' % mode
+ else:
+ cmd = 'ifdown --force usb0'
+ print 'run command', cmd
+ Popen(cmd, shell=True).wait()
+ self.conf = mode
+
+ def shutdown(self):
+ # interpret this as 'switch to p2p mode', unless 'disabled'
+ if self.state == 'disabled':
+ self.setconf(None)
+ else:
+ self.setconf('p2p')
+
+ def activate(self):
+ # must set state to 'active' once it is - then maybe update_active
+ print 'activate usb'
+ self.setconf('client')
+ self.state = 'active'
+
+ def config_hotspot(self):
+ self.setconf('hotspot')
+ self.state = 'active'
+
+class Netman(gtk.Window):
+ # Main window has one radio button for each interface.
+ # Along bottom are 3 buttons: toggle 'enable', toggle 'hotspot', 'config'
+ #
+ def __init__(self):
+ gtk.Window.__init__(self)
+ self.connect('destroy', self.close_application)
+ self.set_title('NetMan')
+
+ self.iface = [ WLAN_iface(self), WWAN_iface(self),
+ BT_iface(self), USB_iface(self) ]
+
+ # 'v' fills the window, 'i' holds the interface, 'b' holds the buttons
+ v = gtk.VBox()
+ v.show()
+ self.main_window = v
+ self.add(v)
+ i = gtk.VBox()
+ i.show()
+ v.pack_start(i, expand = True)
+ i.set_homogeneous(True)
+
+ for iface in self.iface:
+ i.pack_start(iface.button, expand = True)
+
+ b = gtk.HBox()
+ b.show()
+ b.set_size_request(-1, 80)
+ b.set_homogeneous(True)
+ v.pack_end(b, expand = False)
+
+ ctx = self.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(35*pango.SCALE)
+ self.fd = fd
+
+ self.blocker = suspend.blocker()
+
+ self.enable_btn = self.add_button(b, 'enabled', fd, True, self.enable)
+ self.hotspot_btn = self.add_button(b, 'hotspot', fd, True, self.hotspot)
+ self.add_button(b, 'config', fd, False, self.config)
+ self.selected = self.iface[0]
+ self.iface[0].button.set_active(True)
+
+ self.update_active()
+
+ def close_application(self, ev):
+ for i in self.iface:
+ i.shutdown()
+ gtk.main_quit()
+
+ def select(self, child):
+ for i in self.iface:
+ if i != child:
+ i.button.set_active(False)
+ self.selected = None
+ self.enable_btn.set_active(child.state != 'disabled')
+ self.hotspot_btn.set_inconsistent(not child.can_hotspot)
+ self.hotspot_btn.set_active(child.hotspot)
+ self.selected = child
+
+ def add_button(self, bb, name, fd, toggle, func):
+ if toggle:
+ btn = gtk.ToggleButton()
+ else:
+ btn = gtk.Button()
+ btn.set_label(name)
+ btn.child.modify_font(fd)
+ btn.show()
+ bb.pack_start(btn, expand = True)
+ btn.connect('clicked', func)
+ return btn
+
+ def enable(self, ev):
+ if not self.selected:
+ return
+ print 'active?', self.enable_btn.get_active()
+ if not self.enable_btn.get_active():
+ return self.disable()
+
+ if self.selected.state != 'disabled':
+ return
+ print 'enable %s, was %s' % (self.selected.name, self.selected.state)
+ self.selected.state = 'pending'
+ self.update_active()
+ def disable(self):
+ if self.selected.state == 'disabled':
+ return
+ self.selected.state = 'disabled'
+ self.selected.shutdown()
+ self.update_active()
+ self.selected.set_label()
+
+ def hotspot(self, ev):
+ if not self.selected:
+ return
+ if self.hotspot_btn.get_active():
+ # enable hotspot
+ s = self.selected
+ self.selected = None
+ self.enable_btn.set_active(True)
+ self.selected = s
+ s.hotspot = True
+ try:
+ f = open('/proc/sys/net/ipv4/ip_forward','w')
+ f.write('1')
+ f.close()
+ except:
+ pass
+ s.config_hotspot()
+ s.set_label()
+ self.blocker.block()
+ else:
+ # disable hotspot
+ self.selected.hotspot = False
+ self.selected.shutdown()
+ self.selected.set_label()
+ self.update_active()
+
+ def config(self, ev):
+ w = self.selected.config_widget()
+ if not w:
+ return
+ self.remove(self.main_window)
+ self.add(w)
+ w.show()
+
+ def deconfig(self):
+ self.remove(self.get_child())
+ self.add(self.main_window)
+
+ def update_active(self):
+ # try to activate the first available interface,
+ # and stop others
+ active_found = False
+ print 'update_active start'
+ hotspot = False
+ for i in self.iface:
+ print i.name, i.state
+ if i.hotspot:
+ hotspot = True
+ if i.state in ['disabled'] or i.hotspot:
+ continue
+ if active_found:
+ if i.state != 'over-ridden':
+ i.shutdown()
+ i.state = 'over-ridden'
+ i.set_label()
+ elif i.state == 'active':
+ active_found = True
+ else:
+ if i.state == 'over-ridden':
+ i.state = 'pending'
+ i.activate()
+ if i.state == 'active':
+ active_found = True
+ i.set_label()
+ if not hotspot:
+ self.blocker.unblock()
+ print 'update_active end'
+
+if __name__ == '__main__':
+
+ n = Netman()
+ n.set_default_size(480, 640)
+ n.show()
+ gtk.main()
+
--- /dev/null
+net.ipv4.ip_forward=1
+net.ipv6.conf.all.forwarding=1
--- /dev/null
+#!/bin/sh
+ifconfig wlan0 down
+ifdown usb0
+if [ " $1" = " on" ]
+then
+ ifup usb0
+ ifconfig usb0
+fi
--- /dev/null
+#!/bin/sh
+# udhcpc script for netman - modified to recognised NO_DEFAULT env var
+# to mean that default route and DNS config should not be included.
+#
+# Busybox udhcpc dispatcher script. Copyright (C) 2009 by Axel Beckert.
+#
+# Based on the busybox example scripts and the old udhcp source
+# package default.* scripts.
+
+RESOLV_CONF="/etc/resolv.conf"
+
+echo udhcp script $*
+
+case $1 in
+ bound|renew)
+ [ -n "$broadcast" ] && BROADCAST="broadcast $broadcast"
+ [ -n "$subnet" ] && NETMASK="netmask $subnet"
+
+ /sbin/ifconfig $interface $ip $BROADCAST $NETMASK
+
+ if [ -z "$NO_DEFAULT" ]; then
+ if [ -n "$router" ]; then
+ echo "$0: Resetting default routes"
+ while /sbin/route del default gw 0.0.0.0 dev $interface; do :; done
+
+ metric=0
+ for i in $router; do
+ /sbin/route add default gw $i dev $interface metric $metric
+ metric=$(($metric + 1))
+ done
+ fi
+
+ # Update resolver configuration file
+ R=""
+ [ -n "$domain" ] && R="domain $domain
+"
+ for i in $dns; do
+ echo "$0: Adding DNS $i"
+ R="${R}nameserver $i
+"
+ done
+
+ if [ -x /sbin/resolvconf ]; then
+ echo -n "$R" | resolvconf -a "${interface}.udhcpc"
+ else
+ echo -n "$R" > "$RESOLV_CONF"
+ fi
+ fi
+ ;;
+
+ deconfig)
+ if [ -z "$NO_DEFAULT" ]; then
+ if [ -x /sbin/resolvconf ]; then
+ resolvconf -d "${interface}.udhcpc"
+ fi
+ fi
+ /sbin/ifconfig $interface 0.0.0.0
+ ;;
+
+ leasefail)
+ echo "$0: Lease failed: $message"
+ ;;
+
+ nak)
+ echo "$0: Received a NAK: $message"
+ ;;
+
+ *)
+ echo "$0: Unknown udhcpc command: $1";
+ exit 1;
+ ;;
+esac
--- /dev/null
+#!/bin/sh
+
+ifdown usb0
+ifconfig wlan0 down
+fuser -k /sbin/wpa_*
+rmmod libertas_sdio
+if [ " $1" = " up" ]
+then
+ modprobe libertas_sdio
+ rfkill unblock wifi
+ wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf -W
+ wpa_cli -B -i wlan0 -a /root/wpa_updown
+ sleep 5
+ ifconfig wlan0
+ iwconfig wlan0
+fi
--- /dev/null
+#!/usr/bin/env python
+
+#
+# Freerunner app to track petrol usage in new car.
+# We need to keep a log of entries. Each entry:
+#
+# date kilometers litres whether-full price-paid
+#
+# These are displayed with l/100K number and can get
+# overall l/100K and c/l between two points
+#
+# Must be able to edit old entries.
+#
+# So: 2 pages:
+#
+# 1/
+# summary line: l/100K, c/l from selected to mark
+# list of entries, scrollable and selectable
+# buttons: new, edit, mark/unmark
+#
+# 2/ Entry fields for a new entry
+# date: default to 'today' with plus/minus button on either side
+# kilometer - simple text entry
+# litres + 'fill' indicator
+# c/l
+# $
+# keyboard in number mode
+# Buttons: Fill, not-fill, Discard, Save
+#
+# Should I be able to select between different vehicles? Not now.
+
+import sys, os, time
+import pygtk, gtk, pango
+from listselect import ListSelect
+from tapboard import TapBoard
+
+
+class petrol_list:
+ def __init__(self, list):
+ self.list = list
+ self.mark = None
+ def set_list(self, list):
+ self.list = list
+ def set_mark(self, ind):
+ self.mark = ind
+
+ def __len__(self):
+ return len(self.list)
+ def __getitem__(self, ind):
+ i = self.list[ind]
+ dt = i[0]
+ tm = time.strptime(dt, '%Y-%m-%d')
+ dt = time.strftime('%d/%b/%Y', tm)
+ k = "%06d" % int(i[1])
+ l = "%06.2f" % float(i[2])
+ if len(i) > 3 and i[3] == "full":
+ f = "F"
+ else:
+ f = "-"
+ if len(i) > 4:
+ p = "$%06.2f" % float(i[4])
+ else:
+ p = "----.--"
+ str = "%s %s %s %s %s" % (dt, k, l, p, f)
+ if self.mark == ind:
+ type = 'mark'
+ else:
+ type = 'normal'
+ return (str, type)
+
+
+
+class Petrol(gtk.Window):
+ def __init__(self, file):
+ gtk.Window.__init__(self)
+ self.connect("destroy",self.close_application)
+ self.set_title("Petrol")
+
+ ctx = self.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(35*pango.SCALE)
+ vb = gtk.VBox(); self.add(vb); vb.show()
+ self.isize = gtk.icon_size_register("petrol", 40, 40)
+
+ self.listui = self.make_list_ui(fd)
+ self.editui = self.make_edit_ui(fd)
+ vb.add(self.listui); vb.add(self.editui)
+ self.listui.show()
+ self.editui.hide()
+
+ self.active_entry = None
+ self.colours={}
+
+ self.filename = file
+ self.load_file()
+ self.pl.set_list(self.list)
+
+ def close_application(self, ev):
+ gtk.main_quit()
+
+ def make_list_ui(self, fd):
+ ui = gtk.VBox()
+ l = gtk.Label("Petrol Usage")
+ l.modify_font(fd)
+ l.show(); ui.pack_start(l, expand=False)
+
+ h = gtk.HBox(); h.show(); ui.pack_start(h, expand=False)
+ h.set_homogeneous(True)
+ ui.usage_summary = gtk.Label("??.??? l/CKm")
+ ui.usage_summary.show()
+ ui.usage_summary.modify_font(fd)
+ h.add(ui.usage_summary)
+
+ ui.price_summary = gtk.Label("???.? c/l")
+ ui.price_summary.show()
+ ui.price_summary.modify_font(fd)
+ h.add(ui.price_summary)
+
+ ui.list = ListSelect()
+ ui.list.show()
+ ui.pack_start(ui.list, expand=True)
+ ui.list.set_format("normal","black", background="grey", selected="white")
+ ui.list.set_format("mark", "black", bullet=True,
+ background="grey", selected="white")
+ ui.list.set_format("blank", "black", background="lightblue")
+ ui.list.connect('selected', self.selected)
+ self.pl = petrol_list([])
+ ui.list.list = self.pl
+ ui.list.set_zoom(34)
+
+ bbar = gtk.HBox(); bbar.show(); ui.pack_start(bbar, expand=False)
+ self.button(bbar, "New", fd, self.new)
+ self.button(bbar, "Edit", fd, self.edit)
+ self.button(bbar, "Mark", fd, self.mark)
+ return ui
+
+ def make_edit_ui(self, fd):
+ ui = gtk.VBox()
+
+ # title
+ l = gtk.Label("Petrol Event")
+ l.modify_font(fd)
+ l.show(); ui.pack_start(l, fill=False)
+
+ # date - with prev/next buttons.
+ h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+ self.button(h, gtk.STOCK_GO_BACK, fd, self.date_prev, expand=False)
+ l = gtk.Label("Today")
+ l.modify_font(fd)
+ l.show(); h.pack_start(l, expand=True)
+ self.button(h, gtk.STOCK_GO_FORWARD, fd, self.date_next, expand=False)
+ ui.date = l
+
+ # text entry for kilometers
+ h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+ e = self.entry(h, "Km:", fd, self.KM)
+ self.km_entry = e;
+
+ # text entry for litres, with 'fill' indicator
+ h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+ e = self.entry(h, "litres:", fd, self.Litres)
+ self.l_entry = e;
+ l = gtk.Label("(full)")
+ l.modify_font(fd)
+ l.show(); h.pack_start(l, expand=False)
+ self.full_label = l
+
+ # text entry for cents/litre
+ h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+ e = self.entry(h, "cents/l:", fd, self.Cents)
+ self.cl_entry = e;
+
+ # text entry for price paid
+ h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False)
+ e = self.entry(h, "Cost: $", fd, self.Cost)
+ self.cost_entry = e;
+
+ self.entry_priority = [self.l_entry, self.cl_entry]
+
+ # keyboard
+ t = TapBoard(); t.show()
+ ui.pack_start(t, fill=True)
+ t.connect('key', self.use_key)
+
+ # Buttons: fill/non-fill, Discard, Save
+ bbar = gtk.HBox(); bbar.show(); ui.pack_start(bbar, fill=False)
+ bbar.set_homogeneous(True)
+ self.fill_button = self.button(bbar, "non-Fill", fd, self.fill)
+ self.button(bbar, "Discard", fd, self.discard)
+ self.button(bbar, "Save", fd, self.save)
+
+ return ui
+
+ def button(self, bar, label, fd, func, expand = True):
+ btn = gtk.Button()
+ if label[0:3] == "gtk" :
+ img = gtk.image_new_from_stock(label, self.isize)
+ img.show()
+ btn.add(img)
+ else:
+ btn.set_label(label)
+ btn.child.modify_font(fd)
+ btn.show()
+ bar.pack_start(btn, expand = expand)
+ btn.connect("clicked", func)
+ btn.set_focus_on_click(False)
+ return btn
+
+ def entry(self, bar, label, fd, func):
+ l = gtk.Label(label)
+ l.modify_font(fd)
+ l.show(); bar.pack_start(l, expand=False)
+ e = gtk.Entry(); e.show(); bar.pack_start(e, expand=True)
+ e.modify_font(fd)
+ e.set_events(e.get_events()|gtk.gdk.FOCUS_CHANGE_MASK)
+ e.connect('focus-in-event', self.focus)
+ e.connect('changed', func)
+ return e
+ def focus(self, ent, ev):
+ self.active_entry = ent
+ if (len(self.entry_priority) == 0 or
+ self.entry_priority[0] != ent):
+ # Make this entry the most recent
+ self.entry_priority = [ent] + self.entry_priority[:1]
+
+ def calc_other(self):
+ if len(self.entry_priority) != 2:
+ return
+ if self.l_entry not in self.entry_priority:
+ cl = self.check_entry(self.cl_entry)
+ cost = self.check_entry(self.cost_entry)
+ if cl != None and cost != None:
+ self.force_entry(self.l_entry, "%.2f" %(cost * 100.0 / cl))
+ if self.cl_entry not in self.entry_priority:
+ l = self.check_entry(self.l_entry)
+ cost = self.check_entry(self.cost_entry)
+ if l != None and l > 0 and cost != None:
+ self.force_entry(self.cl_entry, "%.2f" %(cost * 100.0 / l))
+ if self.cost_entry not in self.entry_priority:
+ cl = self.check_entry(self.cl_entry)
+ l = self.check_entry(self.l_entry)
+ if cl != None and l != None:
+ self.force_entry(self.cost_entry, "%.2f" %(cl * l / 100.0))
+
+ def force_entry(self, entry, val):
+ entry.set_text(val)
+ entry.modify_base(gtk.STATE_NORMAL, self.get_colour('yellow'))
+
+ def use_key(self, tb, str):
+ if not self.active_entry:
+ return
+ if str == '\b':
+ self.active_entry.emit('backspace')
+ elif str == '\n':
+ self.active_entry.emit('activate')
+ else:
+ self.active_entry.emit('insert-at-cursor', str)
+
+ def set_colour(self, name, col):
+ self.colours[name] = col
+ def get_colour(self, col):
+ # col is either a colour name, or a pre-set colour.
+ # so if it isn't in the list, add it
+ if col == None:
+ return None
+ if col not in self.colours:
+ self.set_colour(col, col)
+ if type(self.colours[col]) == str:
+ self.colours[col] = \
+ self.get_colormap().alloc_color(gtk.gdk.color_parse(self.colours[col]))
+ return self.colours[col]
+
+
+ def check_entry(self, entry):
+ txt = entry.get_text()
+ v = None
+ try:
+ if txt != "":
+ v = eval(txt)
+ colour = None
+ except:
+ colour = 'red'
+ entry.modify_base(gtk.STATE_NORMAL, self.get_colour(colour))
+ return v
+
+ def KM(self,entry):
+ v = self.check_entry(entry)
+ def Litres(self,entry):
+ self.calc_other()
+ def Cents(self,entry):
+ self.calc_other()
+ def Cost(self,entry):
+ self.calc_other()
+
+ def load_file(self):
+ # date:km:litre:full?:price
+ list = []
+ try:
+ f = open(self.filename)
+ l = f.readline()
+ while len(l) > 0:
+ l = l.strip()
+ w = l.split(':')
+ if len(w) >= 3:
+ list.append(w)
+ l = f.readline()
+ except:
+ pass
+
+ list.sort(reverse=True)
+ self.list = list
+ self.curr = 0
+ self.mark = None
+
+ def save_file(self):
+ try:
+ f = open(self.filename + '.tmp', 'w')
+ for l in self.list:
+ f.write(':'.join(l) + '\n')
+ f.close()
+ os.rename(self.filename + '.tmp', self.filename)
+ except:
+ pass
+
+ def selected(self, ev, ind):
+ self.curr = ind
+ l = self.list[ind]
+ ind+= 1
+ ltr = float(l[2])
+ while ind < len(self.list) and \
+ (len(self.list[ind]) <= 3 or self.list[ind][3] != 'full'):
+ ltr += float(self.list[ind][2])
+ ind += 1
+ if ind >= len(self.list) or len(l) <= 3 or l[3] != 'full':
+ lckm = "??.??? l/CKm"
+ else:
+ km = float(l[1]) - float(self.list[ind][1])
+ lckm = "%6.3f l/CKm" % (ltr*100/km)
+ self.listui.usage_summary.set_text(lckm)
+
+ if len(l) >= 5 and float(l[2]) > 0:
+ cl = "%6.2f c/l" % (float(l[4])*100/float(l[2]))
+ else:
+ cl = "???.? c/l"
+ self.listui.price_summary.set_text(cl)
+
+ def new(self, ev):
+ self.curr = None
+ self.editui.date.set_text('Today')
+ self.km_entry.set_text('')
+ self.l_entry.set_text('')
+ self.full_label.set_text('(full)')
+ self.cl_entry.set_text('')
+ self.cost_entry.set_text('')
+ self.listui.hide()
+ self.editui.show()
+
+ def edit(self, ev):
+ if self.curr == None:
+ self.curr = 0
+ l = self.list[self.curr]
+ self.editui.date.set_text(time.strftime('%d/%b/%Y', time.strptime(l[0], "%Y-%m-%d")))
+ self.km_entry.set_text(l[1])
+ self.l_entry.set_text(l[2])
+ self.full_label.set_text('(full)' if l[3]=='full' else '(not full)')
+ self.cost_entry.set_text(l[4])
+ self.entry_priority=[self.l_entry, self.cost_entry];
+ self.calc_other()
+ self.listui.hide()
+ self.editui.show()
+
+ def mark(self, ev):
+ pass
+
+ def date_get(self):
+ x = self.editui.date.get_text()
+ try:
+ then = time.strptime(x, "%d/%b/%Y")
+ except ValueError:
+ then = time.localtime()
+ return then
+ def date_prev(self, ev):
+ tm = time.localtime(time.mktime(self.date_get()) - 12*3600)
+ self.editui.date.set_text(time.strftime("%d/%b/%Y", tm))
+
+ def date_next(self, ev):
+ t = time.mktime(self.date_get()) + 25*3600
+ if t > time.time():
+ t = time.time()
+ tm = time.localtime(t)
+ self.editui.date.set_text(time.strftime("%d/%b/%Y", tm))
+
+ def fill(self, ev):
+ if self.full_label.get_text() == "(full)":
+ self.full_label.set_text("(not full)")
+ self.fill_button.child.set_text("Fill")
+ else:
+ self.full_label.set_text("(full)")
+ self.fill_button.child.set_text("non-Fill")
+
+
+ def discard(self, ev):
+ self.listui.show()
+ self.editui.hide()
+ pass
+
+ def save(self, ev):
+ self.listui.show()
+ self.editui.hide()
+ date = time.strftime('%Y-%m-%d', self.date_get())
+ km = "%d" % self.check_entry(self.km_entry)
+ ltr = "%.2f" % self.check_entry(self.l_entry)
+ full = 'full' if self.full_label.get_text() == "(full)" else 'notfull'
+ price = "%.2f" % self.check_entry(self.cost_entry)
+ if self.curr == None:
+ self.list.append([date,km,ltr,full,price])
+ else:
+ self.list[self.curr] = [date,km,ltr,full,price]
+ self.list.sort(reverse=True)
+ self.pl.set_list(self.list)
+ self.listui.list.list_changed()
+ if self.curr == None:
+ self.listui.list.select(0)
+ self.curr = 0
+ else:
+ self.listui.list.select(self.curr)
+ self.save_file()
+ self.selected(None, self.curr)
+
+
+if __name__ == "__main__":
+
+ p = Petrol("/data/RAV4")
+ p.set_default_size(480, 640)
+ p.show()
+ gtk.main()
+
--- /dev/null
+#
+# Support for running commands from the Laucher
+#
+# Once a command is run, we watch for it to finish
+# and update status when it does.
+# We also maintain window list and for commands that appear
+# in windows, we associate the window with the command.
+# ShellTask() runs a command and captures output in a text buffer
+# that can be displayed in a FingerScroll
+# WinTask() runs a command in a window
+
+import os,fcntl, gobject
+import pango
+from subprocess import Popen, PIPE
+from fingerscroll import FingerScroll
+
+class ShellTask:
+ # Normally there is one button : "Run"
+ # When this is pressed we create a 'FingerScroll' text buffer
+ # to hold the output.
+ # The button then becomes 'ReRun'
+ # When we get deselected, the buffer gets hidden and we get
+ # new button 'Display'
+ def __init__(self, name, line, owner):
+ self.format = "cmd"
+ self.append = False
+ self.is_full = True
+ # remove leading '!'
+ line = line[1:]
+ if line[0] == '-':
+ self.is_full = False
+ line = line[1:]
+ if line[0] == '+':
+ self.append = True
+ line = line[1:]
+ if name == None:
+ self.name = line
+ else:
+ self.name = name
+ self.cmd = line
+ self.buffer = None
+ self.displayed = False
+ self.job = None
+ self.owner = owner
+
+ def buttons(self):
+ if self.displayed:
+ if self.job:
+ return ['-','Kill']
+ else:
+ return ['ReRun', 'Close']
+ if self.buffer != None:
+ if self.job:
+ return ['-', 'Display']
+ else:
+ return ['Run', 'Display']
+ return ['Run']
+
+ def embedded(self):
+ if self.displayed:
+ return self.buffer
+ return None
+ def embed_full(self):
+ return self.is_full
+
+ def press(self, num):
+ if num == 1:
+ if self.displayed and self.job:
+ # must be a 'kill' request'
+ os.kill(self.job.pid, 15)
+ return
+ self.displayed = not self.displayed
+ self.owner.update(self, False)
+ return
+
+ if self.job:
+ return
+
+ if self.buffer == None:
+ self.buffer = FingerScroll()
+ self.buffer.show()
+ self.buffer.connect('hide', self.unmap_buff)
+
+ fd = pango.FontDescription('Monospace 10')
+ fd.set_absolute_size(15*pango.SCALE)
+ self.buffer.modify_font(fd)
+ self.buff = self.buffer.get_buffer()
+ if not self.append:
+ self.buff.delete(self.buff.get_start_iter(), self.buff.get_end_iter())
+ # run the program
+ self.job = Popen(self.cmd, shell=True, close_fds = True,
+ stdout=PIPE, stderr=PIPE)
+
+ def set_noblock(f):
+ flg = fcntl.fcntl(f, fcntl.F_GETFL, 0)
+ fcntl.fcntl(f, fcntl.F_SETFL, flg | os.O_NONBLOCK)
+ set_noblock(self.job.stdout)
+ set_noblock(self.job.stderr)
+ self.wc = gobject.child_watch_add(self.job.pid, self.done)
+ self.wout = gobject.io_add_watch(self.job.stdout, gobject.IO_IN, self.read)
+ self.werr = gobject.io_add_watch(self.job.stderr, gobject.IO_IN, self.read)
+
+ self.displayed = True
+
+ def read(self, f, dir):
+ l = f.read()
+ self.buff.insert(self.buff.get_end_iter(), l)
+ gobject.idle_add(self.adjust)
+ if l == "":
+ return False
+ return True
+
+ def adjust(self):
+ adj = self.buffer.vadj
+ adj.set_value(adj.upper - adj.page_size)
+
+ def done(self, *a):
+ self.read(self.job.stdout, None)
+ self.read(self.job.stderr, None)
+ gobject.source_remove(self.wout)
+ gobject.source_remove(self.werr)
+ self.job.stdout.close()
+ self.job.stderr.close()
+ self.job = None
+ self.owner.update(self, None)
+
+ def unmap_buff(self, widget):
+ if self.job == None:
+ self.displayed = False
+
+
+class WinTask:
+ # A WinTask runs a command and expects it to
+ # create a window. This window can then be
+ # raised or closed, or the process killed.
+ # we find out about windows appearing
+ # by connecting to the 'new-window' signal in
+ # the owner
+ # When there is no process, button in "Run"
+ # When there is a process but no window, "Kill"
+ # When there is process and window, "Raise", "Close", "Kill"
+ def __init__(self, name, window, line, owner):
+ self.name = name
+ self.job = None
+ self.win_id = None
+ self.win = None
+ self.window_name = window
+ self.cmd = line
+ self.embedded = lambda:None
+ self.owner = owner
+ owner.connect('new-window', self.new_win)
+ owner.connect('lost-window', self.lost_win)
+ owner.connect('request-window', self.request_win)
+
+ def buttons(self):
+ if not self.job:
+ return ['Run']
+ if not self.win_id:
+ return ['-','Kill']
+ return ['Raise','Close','Kill']
+
+ def get_format(self):
+ if self.job or self.win:
+ return "win"
+ else:
+ return "cmd"
+
+ def press(self, num):
+ if not self.job and not self.win:
+ if num != 0:
+ return
+ self.job = Popen(self.cmd, shell=True, close_fds = True)
+ self.wc = gobject.child_watch_add(self.job.pid, self.done)
+ return
+
+ if not self.win_id and self.job:
+ if num != 1:
+ return
+ os.kill(self.job.pid, 15)
+ return
+
+ # We have a win_id and a job
+ if num == 0:
+ self.win.raise_win()
+ elif num == 1:
+ self.win.close_win()
+ elif num == 2 and self.job:
+ os.kill(self.job.pid, 15)
+
+ def done(self, *a):
+ self.job = None
+ self.win_id = None
+ self.owner.update(self, None)
+
+ def new_win(self, source, win):
+ if self.job and self.job.pid == win.pid:
+ self.win_id = win.id
+ self.win = win
+ if self.window_name and self.window_name == win.name:
+ self.win_id = win.id
+ self.win = win
+ if self.win_id:
+ self.format = "win"
+ self.owner.update(self, None)
+
+ def lost_win(self, source, id):
+ if self.win_id == id:
+ self.win_id = None
+ self.win = None
+ self.format = "cmd"
+ self.owner.update(self, None)
+
+ def request_win(self, source, name):
+ if self.window_name == name:
+ self.press(0)
--- /dev/null
+#
+# Generic code for group types
+#
+# A 'group' must provide:
+#
+# - 'parse' to take a line from the config file and interpret it.
+# - 'name' and 'format' which are present to ListSelect.
+# 'format' is normally 'group'.
+# - 'tasks' which provides a task list to display in the other
+# ListSelect.
+#
+# A 'group' can generate the signal:
+# - 'new-task-list' to report that the task list has changed
+# ???
+#
+
+import cmd, re
+
+class IgnoreType:
+ def __init__(self, win, name):
+ self.name = name
+ self.format = 'group'
+ self.tasks = []
+
+ def parse(self, line):
+ pass
+
+class ListType:
+ """A ListType group parses a list of tasks out of
+ the config file and provides them as a static list.
+ Tasks can be:
+ !command - run command and capture text output
+ (window)command - run command and expect it to appear as 'window'
+ command.command - run an internal command from the given module
+ In each case, arguments can follow and are treated as you would expect.
+ """
+
+ def __init__(self, win, name):
+ self.name = name
+ self.format = 'group'
+ self.tasks = []
+ self.win = win
+
+ def parse(self, line):
+ line = line.strip()
+ m = re.match('^([A-Za-z0-9_ ]+)=(.*)', line)
+ if m:
+ name = m.groups()[0].strip()
+ line = m.groups()[1].strip()
+ else:
+ name = None
+ if line[0] == '!':
+ self.parse_txt(name, line)
+ elif line[0] == '(':
+ self.parse_win(name, line)
+ else:
+ self.parse_internal(name, line)
+
+ def parse_txt(self, name, line):
+ task = cmd.ShellTask(name, line, self)
+ if task.name:
+ self.tasks.append(task)
+
+ def parse_win(self, name, line):
+ f = line[1:].split(')', 1)
+ if len(f) != 2:
+ return
+ if name == None:
+ name = f[0]
+ task = cmd.WinTask(name, f[0], f[1], self)
+ if task:
+ self.tasks.append(task)
+
+ def parse_internal(self, name, line):
+ # split in to words, honouring quotes
+ words = map(lambda a: a[0].strip('"')+a[1].strip("'")+a[2],
+ re.findall('("[^"]*")?(\'[^\']*\')?([^ "\'][^ "\']*)? *',
+ line))[:-1]
+
+ cmd = words[0].split('.', 1)
+ if len(cmd) == 1:
+ words[0] = "internal."+cmd[0]
+ cmd = ['internal',cmd[0]]
+ if len(cmd) != 2:
+ return
+
+ exec "import plato_" + cmd[0]
+ fn = eval("plato_"+words[0])
+ if name == None:
+ name = cmd[0]
+ task = fn(name, self, *words[1:])
+ if task:
+ self.tasks.append(task)
+
+ def update(self, task, refresh):
+ if refresh:
+ self.win.select(self, task)
+ self.win.update(task)
+ def emit(self, *args):
+ self.win.emit(*args)
+
+ def connect(self, name, *cmd):
+ return self.win.connect(name, *cmd)
+ def queue_draw(self):
+ return self.win.queue_draw()
--- /dev/null
+#!/usr/bin/env python
+
+#TODO
+# aux button
+# calculator
+# sms
+# entry shows number of new messages
+# embed shows latest new message if there is one
+# buttons allow 'read' or 'open'
+# phone calls
+# number of missed calls, or incoming number
+# embed shows call log, or tap board
+# buttons allow 'call' or 'answer' or 'open' ....
+# embed calendar
+# selected by 'date'
+# tapping can select a date
+# run windowed program
+# monitor list of windows
+# address list
+# this is a 'group' where 'tasks' are contacts
+
+
+# Launcher, version 2.
+# This module just defines the UI and plugin interface
+# All tasks etc go in loaded modules.
+#
+# The elements of the UI are:
+#
+# ----------------------------------
+# | Text (Entry) box (one line) |
+# +--------------------------------+
+# | | |
+# | group | task |
+# | selection | selection |
+# | list | list |
+# | | |
+# | | |
+# +----------------+ |
+# | optional | |
+# | task- | |
+# | specific | |
+# | widget +---------------|
+# | | secondary |
+# | | button row |
+# +----------------+---------------+
+# | Main Button Row |
+# +--------------------------------+
+#
+# If the optional widget is present, then the Main Button Row
+# disappears and the secondary button row is used instead.
+#
+# The selection lists auto-scroll when an item near top or bottom
+# is selected. Selecting an entry only updates other parts of
+# the UI and does not perform any significant action.
+# Actions are performed by buttons which appear in a button
+# row.
+# The optional widget can be anything provided by the group
+# or task, for example:
+# - tap-board for entering text
+# - calendar for selecting a date
+# - display area for small messages (e.g. SMS)
+# - preview during file selection
+# - map of current location
+#
+# Entered text alway appears in the Text Entry box and
+# is made available to the current group and task
+# That object may use the text and may choose to delete it.
+# e.g.
+# A calculator task might display an evaluation of the text
+# A call task might offer to call the displayed number, and
+# delete it when the call completes
+# An address-book group might restrict displayed tasks
+# to those which match the text
+# A 'todo' task might set the text to match the task content,
+# then update the content as the text changes.
+#
+# A config file describes the groups.
+# It is .platorc in $HOME
+# A group is enclosed in [] and can have an optional type:
+# [group1]
+# [Windows/winlist]
+# If no type is given the 'list' type is used.
+# this parses following lines to create a static list of tasks.
+# Other possible types are:
+# winlist: list of windows on display
+# contacts: list of contacts
+# notifications: list of recent notifications (alarm/battery/whatever)
+#
+# the 'list' type is described in more detail in 'grouptypes'.
+#
+
+import sys, gtk, os, pango
+import gobject
+
+if __name__ == '__main__':
+ sys.path.insert(1, '/home/neilb/home/freerunner/lib')
+ sys.path.insert(1, '/home/neilb/home/freerunner/sms')
+ sys.path.insert(1, '/root/lib')
+
+from tapboard import TapBoard
+import grouptypes, listselect, scrawl
+from grouptypes import *
+from window_group import *
+from evdev import EvDev
+
+import plato_gsm
+
+class MakeList:
+ # This class is a wrapper on a 'tasklist' which presents an
+ # entry list that can be used by ListSelect.
+ # Each entry in the list is a name and a format
+ # The group can either contain a list of task: tasks
+ # or a function: get_task
+ # in the later case the length is reported as -1
+ #
+ # a task must provide a .name or a get_name function
+ # similarly a .format or a .get_format function
+ def __init__(self, group):
+ self.group = group
+
+ def __getitem__(self, ind):
+ i = self.get_item(ind)
+ if i == None:
+ return None
+ try:
+ name = i.name
+ except AttributeError:
+ name = i.get_name()
+ try:
+ format = i.format
+ except AttributeError:
+ format = i.get_format()
+ return (name, format)
+
+ def __len__(self):
+ try:
+ e = self.group.tasks
+ l = len(e)
+ except AttributeError:
+ l = 50000
+ return l
+
+ def get_item(self, ind):
+ try:
+ e = self.group
+ if ind >= 0 and ind < len(e):
+ i = e[ind]
+ else:
+ i = None
+ except AttributeError:
+ try:
+ e = self.group.tasks
+ if ind >= 0 and ind < len(e):
+ i = e[ind]
+ else:
+ i = None
+ except AttributeError:
+ i = self.group.get_task(ind)
+ return i
+
+
+class LaunchWin(gtk.Window):
+ __gsignals__ = {
+ 'new-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,)),
+ 'lost-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_INT,)),
+ 'request-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_STRING,)),
+ }
+ def __init__(self):
+ gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+ self.set_default_size(480, 640)
+ self.connect('destroy', lambda w: gtk.main_quit())
+
+ self.embeded_widget = None
+ self.embeded_full = False
+ self.hidden = True
+ self.next_group = 0
+ self.widgets = {}
+ self.buttons = []
+ self.create_ui()
+ self.load_config()
+ self.grouplist.select(0)
+
+ def create_ui(self):
+ # Create the UI framework, first the components
+
+ # The Entry
+ e1 = gtk.Entry()
+ e1.set_alignment(0.5) ; # Center text
+ e1.connect('changed', self.entry_changed)
+ e1.connect('backspace', self.entry_changed)
+ e1.show()
+ self.entry = e1
+
+ # Label for the entry
+ lbl = gtk.Label("Label")
+ lbl.hide()
+ self.label = lbl
+
+ # The text row: Entry fills, label doesn't
+ tr = gtk.HBox()
+ tr.pack_start(lbl, expand = False)
+ tr.pack_end(e1, expand = True)
+ tr.show()
+
+ # The group list
+ l1 = listselect.ListSelect(center = False, linescale = 1.3, markup = True)
+ l1.connect('selected', self.group_select)
+ # set appearance here
+ l1.set_colour('group','blue')
+ self.grouplist = l1
+ l1.set_zoom(35)
+ l1.show()
+
+ # The task list
+ l2 = listselect.ListSelect(center = True, linescale = 1.3, markup = True)
+ l2.connect('selected', self.task_select)
+ l2.set_colour('cmd', 'black')
+ l2.set_colour('win', 'blue')
+ l2.set_zoom(35)
+ # set appearance
+ self.tasklist = l2
+ l2.show()
+
+ # The embedded widget: provide a VBox as a place holder
+ v1 = gtk.VBox()
+ self.embed_box = v1
+
+ # The Main button box - buttons are added later
+ h1 = gtk.HBox(True)
+ self.main_buttons = h1
+ # HACK
+ h1.set_size_request(-1, 80)
+ h1.show()
+
+ # The Secondary button box
+ h2 = gtk.HBox(True)
+ h2.set_size_request(-1, 60)
+ self.secondary_buttons = h2
+
+ # Now make the two columns
+
+ v2 = gtk.VBox(True)
+ v2.pack_start(self.grouplist, expand = True)
+ v2.pack_end(self.embed_box, expand = False)
+ v2.show()
+
+ v3 = gtk.VBox()
+ v3.pack_start(self.tasklist, expand = True)
+ v3.pack_end(self.secondary_buttons, expand = False)
+ v3.show()
+
+ # and bind them together
+ h3 = gtk.HBox(True)
+ h3.pack_start(v2, expand=True)
+ h3.pack_end(v3, expand=True)
+ h3.show()
+
+ # A vbox to hold tr and main buttons
+ v3a = gtk.VBox()
+ v3a.show()
+ v3a.pack_start(tr, expand=False)
+ v3a.pack_end(h3, expand=True)
+ self.non_buttons = v3a
+
+ # a box for a 'full screen' embedded widget
+ v3b = gtk.VBox()
+ v3b.hide()
+ self.embed_full_box = v3b
+
+
+ # And now one big vbox to hold it all
+ v4 = gtk.VBox()
+ v4.pack_start(self.non_buttons, expand=True)
+ v4.pack_start(self.embed_full_box, expand=True)
+ v4.pack_end(self.main_buttons, expand=False)
+ v4.show()
+ self.add(v4)
+ self.show()
+
+ ## We want writing recognistion to work
+ ## over the whole middle section. Only that
+ ## turns out to be too hard for my lowly gtk
+ ## skills. So we do recognition separately
+ ## on each selection box only.
+ s1 = scrawl.Scrawl(l1, self.getsym, lambda p: l1.tap(p[0],p[1]))
+ s2 = scrawl.Scrawl(l2, self.getsym, lambda p: l2.tap(p[0],p[1]))
+ s1.set_colour('red')
+ s2.set_colour('red')
+
+
+ ctx = self.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(30 * pango.SCALE)
+ self.button_font = fd;
+ self.entry.modify_font(fd)
+ self.label.modify_font(fd)
+
+ def load_config(self):
+ fname = os.path.join(os.environ['HOME'], ".platorc")
+ types = {}
+ types['ignore'] = IgnoreType
+ types['list' ] = ListType
+ types['winlist'] = WindowType
+ types['call_list']=plato_gsm.call_list
+ groups = []
+ f = open(fname)
+ gobj = None
+ for line in f:
+ l = line.strip()
+ if not l:
+ continue
+ if l[0] == '[':
+ l = l.strip('[]')
+ f = l.split('/', 1)
+ group = f[0]
+ if len(f) > 1:
+ group_type = f[1]
+ else:
+ group_type = "list"
+ if group_type in types:
+ gobj = types[group_type](self, group)
+ else:
+ gobj = types['ignore'](self, group)
+ groups.append(gobj)
+ elif gobj != None:
+ gobj.parse(l)
+ self.grouplist.list = MakeList(groups)
+ self.grouplist.list_changed()
+
+
+ def entry_changed(self, entry):
+ print "fixme", entry.get_text()
+
+ def group_select(self, list, item):
+ g = list.list.get_item(item)
+ self.tasklist.list = MakeList(g)
+ self.tasklist.list_changed()
+ self.tasklist.select(None)
+ if self.tasklist.list != None:
+ self.task_select(self.tasklist,
+ self.tasklist.selected)
+
+ def set_group(self, group):
+ self.tasklist.list = MakeList(group)
+ self.tasklist.list_changed()
+ self.tasklist.select(None)
+ if self.tasklist.list != None:
+ self.task_select(self.tasklist,
+ self.tasklist.selected)
+
+ def task_select(self, list, item):
+ if item == None:
+ self.set_buttons(None)
+ self.set_embed(None, None)
+ else:
+ task = self.tasklist.list.get_item(item)
+ if task:
+ self.set_buttons(task.buttons())
+ bed = task.embedded()
+ full = False
+ if bed != None and type(bed) != str:
+ full = task.embed_full()
+ self.set_embed(bed, full)
+
+ def update(self, task):
+ if self.tasklist.selected != None and \
+ self.tasklist.list.get_item(self.tasklist.selected) == task:
+ self.set_buttons(task.buttons())
+ bed = task.embedded()
+ full = False
+ if bed != None and type(bed) != str:
+ full = task.embed_full()
+ self.set_embed(bed, full)
+
+ def select(self, group, task):
+ i = 0
+ gl = self.grouplist.list
+ while i < len(gl) and gl.get_item(i) != None:
+ if gl.get_item(i) == group:
+ self.grouplist.select(i)
+ break
+ i += 1
+ tl = self.tasklist.list
+ i = 0
+ while i < len(tl) and tl.get_item(i) != None:
+ if tl.get_item(i) == task:
+ self.tasklist.select(i)
+ self.present()
+ break
+ i += 1
+
+ def set_buttons(self, list):
+ if not list:
+ # hide the button boxes
+ self.secondary_buttons.hide()
+ self.main_buttons.hide()
+ self.buttons = []
+ return
+ if self.same_buttons(list):
+ return
+
+ self.buttons = list
+ self.update_buttons(self.main_buttons)
+ self.update_buttons(self.secondary_buttons)
+ if self.embeded_widget and not self.embeded_full:
+ self.main_buttons.hide()
+ self.secondary_buttons.show()
+ else:
+ self.secondary_buttons.hide()
+ self.main_buttons.show()
+
+ def same_buttons(self, list):
+ if len(list) != len(self.buttons):
+ return False
+ for i in range(len(list)):
+ if list[i] != self.buttons[i]:
+ return False
+ return True
+
+ def update_buttons(self, box):
+ # make sure there are enough buttons
+ have = len(box.get_children())
+ need = len(self.buttons) - have
+ if need > 0:
+ for i in range(need):
+ b = gtk.Button("?")
+ b.child.modify_font(self.button_font)
+ b.set_property('can-focus', False)
+ box.add(b)
+ b.connect('clicked', self.button_pressed, have + i)
+ have += need
+ b = box.get_children()
+ # hide extra buttons
+ if need < 0:
+ for i in range(-need):
+ b[have-i-1].hide()
+ # update each button
+ for i in range(len(self.buttons)):
+ b[i].child.set_text(self.buttons[i])
+ b[i].show()
+
+ def button_pressed(self, widget, num):
+ self.hidden = True
+ self.tasklist.list.get_item(self.tasklist.selected).press(num)
+ self.task_select(self.tasklist,
+ self.tasklist.selected)
+
+ def set_embed(self, widget, full):
+ if type(widget) == str:
+ widget, full = self.make_widget(widget)
+
+ if widget == self.embeded_widget and full == self.embeded_full:
+ return
+ if self.embeded_widget:
+ self.embeded_widget.hide()
+ if self.embeded_full:
+ self.embed_full_box.remove(self.embeded_widget)
+ else:
+ self.embed_box.remove(self.embeded_widget)
+ self.embeded_widget = None
+ self.embeded_widget = widget
+ self.embeded_full = full
+ if widget and not full:
+ self.embed_box.add(widget)
+ widget.show()
+ self.main_buttons.hide()
+ self.embed_box.show()
+ self.non_buttons.show()
+ self.embed_full_box.hide()
+ if self.buttons:
+ self.secondary_buttons.show()
+ elif widget:
+ self.embed_full_box.add(widget)
+ widget.show()
+ self.embed_full_box.show()
+ self.non_buttons.hide()
+ if self.buttons:
+ self.main_buttons.show()
+ else:
+ self.embed_box.hide()
+ self.embed_full_box.hide()
+ self.non_buttons.show()
+ self.secondary_buttons.hide()
+ if self.buttons:
+ self.main_buttons.show()
+
+ def make_widget(self, name):
+ if name in self.widgets:
+ return self.widgets[name]
+ if name == "tapboard":
+ w = TapBoard()
+ def key(w, k):
+ if k == '\b':
+ self.entry.emit('backspace')
+ elif k == 'Return':
+ self.entry.emit('activate')
+ elif len(k) == 1:
+ self.entry.emit('insert-at-cursor', k)
+ w.connect('key', key)
+ self.widgets[name] = (w, False)
+ return (w, False)
+ return None
+
+ def getsym(self, sym):
+ print "gotsym", sym
+ if sym == '<BS>':
+ self.entry.emit('backspace')
+ elif sym == '<newline>':
+ self.entry.emit('activate')
+ elif len(sym) == 1:
+ self.entry.emit('insert-at-cursor', sym)
+
+ def activate(self, *a):
+ # someone pressed a button or icon to get our
+ # attention.
+ # We raise the window and if nothing has happened recently,
+ # select first group
+ if self.hidden:
+ self.hidden = False
+ self.next_group = 0
+ else:
+ if (self.next_group > len(self.grouplist.list) or
+ self.grouplist.list[self.next_group] == None):
+ self.next_group = 0
+ self.grouplist.select(self.next_group)
+ self.next_group += 1
+
+ self.present()
+
+ def ev_activate(self, down, moment, typ, code, value):
+ if typ != 1:
+ # not a key press
+ return
+ if code != 169 and code != 116:
+ # not AUX or Power
+ return
+ if value == 0:
+ # down press, wait for up
+ self.down_at = moment
+ return
+ self.activate()
+
+class LaunchIcon(gtk.StatusIcon):
+ def __init__(self, callback):
+ gtk.StatusIcon.__init__(self)
+ self.set_from_stock(gtk.STOCK_EXECUTE)
+ self.connect('activate', callback)
+
+
+if __name__ == '__main__':
+ sys.path.insert(1, '/home/neilb/home/freerunner/lib')
+ sys.path.insert(1, '/root/lib')
+ l = LaunchWin()
+ i = LaunchIcon(l.activate)
+ try:
+ aux = EvDev("/dev/input/aux", l.ev_activate)
+ except OSError:
+ aux = None
+
+ gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
+ gtk.main()
--- /dev/null
+
+#
+# plato plugins for mobile phone functionality.
+# carrier:
+# displays current carrier
+# button will search for possible carriers
+# while search, button show how long search has progressed
+# on succcess, get one button per carrier valid for 2 minutes
+# When one is selected, write that to /var/run/gsm/request_carrier
+#
+# calls:
+# display incoming phone number and when there is one, allows
+# 'answer' or 'reject', then '-' or 'hangup'
+# When no call, buttons for 'dial' or 'contacts'
+#
+# newmsg:
+# displays new message and allows more to be shown in embedded
+# also allows sms reader to be run
+
+from plato_internal import file
+from subprocess import Popen, PIPE
+import os.path, fcntl, time, gobject, re, gtk
+
+def record(key, value):
+ f = open('/var/run/gsm-state/.new.' + key, 'w')
+ f.write(value)
+ f.close()
+ os.rename('/var/run/gsm-state/.new.' + key,
+ '/var/run/gsm-state/' + key)
+
+
+def recall(key):
+ try:
+ fd = open("/var/run/gsm-state/" + key)
+ l = fd.read(1000)
+ fd.close()
+ except IOError:
+ l = ""
+ return l.strip()
+
+class carrier(file):
+ def __init__(self, name, owner):
+ file.__init__(self, name, owner, "/var/run/gsm-state/carrier")
+ self.buttons = self.cbuttons
+
+ self.carriers = []
+ self.carrier_buttons = None
+ self.carrier_valid = 0
+ self.reader = None
+ self.job = None
+ self.job_start = 0
+ self.timeout = None
+
+ def cbuttons(self):
+ if self.job:
+ # waiting for carriers - can cancel
+ if self.timeout == None:
+ self.timeout = gobject.timeout_add(3000, self.change_button)
+ waited = time.time() - self.job_start
+ return [ 'Cancel after %d seconds' % waited]
+
+ if self.carrier_buttons and time.time() - self.carrier_valid < 120:
+ return self.carrier_buttons
+ self.carrier_buttons = None
+ if (os.path.exists('/var/run/gsm-state/request_carrier') and
+ os.path.getsize('/var/run/gsm-state/request_carrier') > 1):
+ return ['Search Carriers','Cancel Request']
+ return ['Search Carriers']
+
+ def change_button(self):
+ self.timeout = None
+ self.owner.update(self, None)
+
+ def press(self, ind):
+ if self.job:
+ if ind == 0:
+ self.job.terminate()
+ return
+
+ if self.carriers and self.carrier_buttons:
+ if ind < len(self.carriers):
+ num = self.carriers[ind][0]
+ record('request_carrier',num)
+ self.carrier_buttons = None
+ return
+
+ if ind == 1:
+ record('request_carrier', '')
+ return
+ if ind != 0:
+ return
+
+ # initiate search
+ # run gsm-carriers expecting output like:
+ # 50503 0 "voda AU" "vodafone AU"
+ # record output in carriers[] and short names in carrer_buttons
+ self.job = Popen("gsm-carriers", shell=True, close_fds=True,
+ stdout = PIPE)
+ self.job_start = time.time()
+ gobject.child_watch_add(self.job.pid, self.done)
+ flg = fcntl.fcntl(self.job.stdout, fcntl.F_GETFL, 0)
+ fcntl.fcntl(self.job.stdout, fcntl.F_SETFL, flg | os.O_NONBLOCK)
+ self.buf = ''
+ self.reader = gobject.io_add_watch(self.job.stdout, gobject.IO_IN,
+ self.read)
+
+ def read(self, f, dir):
+ try:
+ l = f.read()
+ except IOError:
+ l = None
+ if l == None:
+ if self.job.poll() == None:
+ return True
+ elif l != '':
+ self.buf += l
+ return True
+ self.job.wait()
+ lines = self.buf.split('\n')
+ c=[]
+ b=[]
+ for line in lines:
+ words = re.findall('([^"][^ ]*|"[^"]*["]) *', line)
+ if len(words) == 4:
+ c.append(words)
+ b.append(words[2].strip('"'))
+ self.carriers = c
+ self.carrier_buttons = b
+ self.carrier_valid = time.time()
+ self.job = None
+ self.owner.update(self, None)
+ return False
+
+ def done(self, *a):
+ if not self.job:
+ return
+ self.job.wait()
+ #flg = fcntl.fcntl(self.job.stdout, fcntl.F_GETFL, 0)
+ #fcntl.fcntl(self.job.stdout, fcntl.F_SETFL, flg & ~os.O_NONBLOCK)
+ self.read(self.job.stdout, None)
+
+
+
+class calls(file):
+ # Monitor 'status' which can be
+ # INCOMING BUSY on-call or empty
+ # INCOMING:
+ # get caller from 'incoming' and display - via contacts if possibe
+ # Buttons are 'answer','reject'
+ # BUSY:
+ # display BUSY
+ # Buttons are '-', 'cancel'
+ # on-call:
+ # Buttons are 'hold', 'cancel'
+ # empty:
+ # display (call logs)
+ # Buttons are "Received" "Dialled" "Dialer" "Contacts"
+ def __init__(self, name, owner):
+ file.__init__(self, name, owner, '/var/run/gsm-state/status')
+ self.buttons = self.cbuttons
+ self.get_name = self.cgetname
+ self.status = ''
+ self.choose_logs = False
+ self.caller = None
+ self.dialer_win = None
+ self.contacts_win = None
+ self.dj = None
+ self.cj = None
+ # catch queue_draw from 'file'
+ self.owner = self
+ self.gowner = owner
+ owner.connect('new-window', self.new_win)
+ owner.connect('lost-window', self.lost_win)
+
+ def cbuttons(self):
+ if self.status == '' or self.status == '-':
+ if self.choose_logs:
+ return ['Call', "Recv'd\nCalls","Dialed\nCalls"]
+ else:
+ return ['Logs', "Dialer", "Contacts"]
+ if self.status == 'INCOMING':
+ return ['Answer','Reject']
+ if self.status == 'BUSY':
+ return ['-','Cancel']
+ if self.status == 'on-call':
+ return ['Hold','Cancel']
+ return []
+
+ def cgetname(self):
+ status = file.get_name(self)
+ if self.status != status:
+ self.status = status
+ print "update for", status
+ # was "status == 'INCOMING'" to raise on incoming calls
+ # but want Dailer to do that
+ self.gowner.update(self, False)
+ if self.status == 'INCOMING':
+ if self.caller == None:
+ self.caller = recall('incoming')
+ if self.caller == '':
+ self.caller = '(private)'
+ else:
+ contacts = contactdb.contacts()
+ n = contacts.find_num(self.caller)
+ if n:
+ self.caller = n.name
+ else:
+ self.caller = None
+ if self.status == '' or self.status == '-':
+ if self.choose_logs:
+ return '(call logs)'
+ else:
+ return '(no call)'
+ if self.status == 'INCOMING':
+ return self.caller + ' calling'
+ if self.status == 'BUSY':
+ return 'BUSY'
+ if self.status == 'on-call':
+ return "On Call"
+ return '??'+self.status
+
+ def press(self, ind):
+ if self.status == '' or self.status == '-':
+ if self.choose_logs:
+ if ind == 0:
+ self.choose_logs = False
+ elif ind == 1:
+ self.gowner.win.set_group(call_list(self.gowner.win, '', True))
+ elif ind == 2:
+ self.gowner.win.set_group(call_list(self.gowner.win, '', False))
+ else:
+ if ind == 0:
+ self.choose_logs = True
+ elif ind == 1:
+ if self.dialer_win:
+ self.dialer_win.raise_win()
+ elif not self.dj:
+ self.dj = Popen("dialer", shell=True, close_fds = True)
+ gobject.child_watch_add(self.dj.pid, self.djdone)
+ elif ind == 2:
+ if self.contacts_win:
+ self.contacts_win.raise_win()
+ elif not self.cj:
+ self.cj = Popen("contacts", shell=True, close_fds = True)
+ gobject.child_watch_add(self.cj.pid, self.cjdone)
+ self.gowner.update(self, None)
+ return
+
+ if self.status == 'INCOMING':
+ if ind == 0:
+ record('call','answer')
+ elif ind == 1:
+ record('call','')
+ return
+
+ if ind == 0:
+ print "on hold??"
+ return
+ if ind == 1:
+ record('call','')
+
+ def queue_draw(self):
+ # file changed
+ self.cgetname()
+ self.gowner.queue_draw()
+
+ def djdone(self, *a):
+ self.dj.wait()
+ self.dj = None
+ def cjdone(self, *a):
+ self.cj.wait()
+ self.cj = None
+
+ def new_win(self, source, win):
+ if win.name == "Dialer":
+ self.dialer_win = win
+ if win.name == "Contacts":
+ self.contacts_win = win
+
+ def lost_win(self, source, id):
+ if self.dialer_win and self.dialer_win.id == id:
+ self.dialer_win = None
+ if self.contacts_win and self.contacts_win.id == id:
+ self.contacts_win = None
+
+import contactdb
+
+contacts = None
+
+def friendly_time(tm, secs):
+ now = time.time()
+ if secs > now or secs < now - 7*24*3600:
+ return time.strftime("%Y-%m-%d %H:%M:%S", tm)
+ age = now - secs
+ if age < 12*3600:
+ return time.strftime("%H:%M:%S", tm)
+ return time.strftime("%a %H:%M:%S", tm)
+
+class corresp:
+ # This task represents a correspondant in the
+ # lists of dialled numbers and received calls
+ # It displays the contact name/number and time/date
+ # in a friendly form
+ # If there is a number we provide a button to call-back
+ # and one to find contact
+ def __init__(self, name, owner, when):
+ global contacts
+ self.format="brown"
+ self.owner = owner
+ self.number = name
+ self.embedded = lambda:None
+ dt, tm = when
+ self.when = time.strptime(dt+'='+tm, '%Y-%m-%d=%H:%M:%S')
+ self.secs = time.mktime(self.when)
+
+ if contacts == None:
+ contacts = contactdb.contacts()
+ if name == None:
+ self.contact = "-private-number-"
+ else:
+ n = contacts.find_num(name)
+ if n:
+ self.contact = n.name
+ else:
+ self.contact = name
+
+ def get_name(self):
+ w = friendly_time(self.when, self.secs)
+ return '<span size="xx-small">%s</span>\n<span size="xx-small">%s</span>' % (
+ self.contact, w)
+
+ def buttons(self):
+ if self.number:
+ return ['Call','Txt','Contacts']
+ return []
+
+ def press(self, ind):
+ if ind == 0:
+ record('call',self.number)
+ send_number('voice-dial', self.number)
+ self.owner.emit('request-window','Dialer')
+ if ind == 1:
+ send_number('sms-new', self.number)
+ self.owner.emit('request-window','SendSMS')
+ if ind == 2:
+ send_number('contact-find', self.number)
+ self.owner.emit('request-window','Contacts')
+
+sel_number = None
+clips = {}
+def clip_get_data(clip, sel, info, data):
+ global sel_number
+ sel.set_text(sel_number)
+def clip_clear_data(clip, data):
+ global sel_number
+ sel_number = None
+ c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
+ clip_get_data, clip_clear_data, None)
+
+def send_number(sel, num):
+ global sel_number, clips
+ sel_number = num
+ if sel not in clips:
+ clips[sel] = gtk.Clipboard(selection = sel)
+ c = clips[sel]
+ c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)],
+ clip_get_data, clip_clear_data, None)
+
+
+
+class call_list:
+ # A list of 'corresp' tasks taken from /var/log/incoming or
+ # /var/log/outgoing
+ def __init__(self, win, name, incoming = None):
+ self.win = win
+ if incoming == None:
+ incoming = name == 'incoming'
+
+ if incoming:
+ self.list = incoming_list(self.update)
+ self.name = "Incoming Calls"
+ else:
+ self.list = outgoing_list(self.update)
+ self.name = "Outgoing Calls"
+ self.format = 'group'
+
+ def get_task(self, ind):
+ if ind >= len(self.list):
+ return None
+ start, end, num = self.list[ind]
+ return corresp(num, self, start)
+
+ def update(self):
+ self.win.queue_draw()
+ def emit(self, *a):
+ self.win.emit(*a)
+
+
+import dnotify
+class incoming_list:
+ # present as a list of received calls
+ # and notify whenever it changes
+ # Each entry is a triple (start, end, number)
+ # start and end are (date, time)
+ def __init__(self, notify, file='incoming'):
+ self.list = []
+ self.notify = notify
+ self.watch = dnotify.dir("/var/log")
+ self.fwatch = self.watch.watch(file, self.changed)
+ self.changed()
+ def __len__(self):
+ return len(self.list)
+ def __getitem__(self, ind):
+ return self.list[-1-ind]
+ def flush_one(self, start, end, number):
+ if start:
+ self.lst.append((start, end, number))
+ def changed(self):
+ self.lst = []
+
+ try:
+ f = open("/var/log/incoming")
+ except IOError:
+ f = []
+ start = None; end=None; number=None
+ for l in f:
+ w = l.split()
+ if len(w) != 3:
+ continue
+ if w[2] == '-call-':
+ self.flush_one(start, end, number)
+ start = (w[0], w[1])
+ number = None; end = None
+ elif w[2] == '-end-':
+ end = (w[0], w[1])
+ self.flush_one(start, end, number)
+ start = None; end = None; number = None
+ else:
+ number = w[2]
+ if not start:
+ start = (w[0], w[1])
+ if number:
+ self.flush_one(start, end, number)
+ try:
+ f.close()
+ except AttributeError:
+ pass
+ self.list = self.lst
+ self.notify()
+
+class outgoing_list(incoming_list):
+ # present similar list for outgoing calls
+ def __init__(self, notify):
+ incoming_list.__init__(self, notify, file='outgoing')
+
+ def changed(self):
+ self.lst = []
+ try:
+ f = open("/var/log/outgoing")
+ except IOError:
+ f = []
+ start = None; end=None; number=None
+ for l in f:
+ w = l.split()
+ if len(w) != 3:
+ continue
+ if w[2] == '-end-':
+ end = (w[0], w[1])
+ self.flush_one(start, end, number)
+ start = None; end = None; number = None
+ else:
+ self.flush_one(start, end, number)
+ start = (w[0], w[1])
+ number = w[2]
+ if number:
+ self.flush_one(start, end, number)
+ try:
+ f.close()
+ except AttributeError:
+ pass
+ self.list = self.lst
+ self.notify()
+
+
--- /dev/null
+#
+# internal commands for 'plato'
+# some of these affect plato directly, some are just
+# ad hoc simple things.
+
+import time as _time
+import gobject, gtk
+import dnotify, os
+
+class date:
+ def __init__(self, name, owner):
+ self.buttons = lambda:['Calendar']
+ self.embedded = lambda:None
+ self.format = 'cmd'
+ self.timeout = None
+ self.owner = owner
+
+ def press(self, ind):
+ if ind != 0:
+ return
+ self.gowner.emit('request-window','cal')
+
+
+ def get_name(self):
+ now = _time.time()
+ if self.timeout == None:
+ next_hour = int(now/60/60)+1
+ self.timeout = gobject.timeout_add(
+ int (((next_hour*3600) - now) * 1000),
+ self.do_change)
+ return _time.strftime('%d-%b-%Y',
+ _time.localtime(now))
+
+ def do_change(self):
+ self.timeout = None
+ if self.owner:
+ self.owner.queue_draw()
+
+
+class time:
+ def __init__(self, name, owner):
+ self.buttons = lambda:None
+ self.embedded = lambda:None
+ self.format = 'cmd'
+ self.timeout = None
+ self.owner = owner
+
+ def get_name(self):
+ now = _time.time()
+ if self.timeout == None:
+ next_min = int(now/60)+1
+ self.timeout = gobject.timeout_add(
+ int (((next_min*60) - now) * 1000),
+ self.do_change)
+ return _time.strftime('%H:%M',
+ _time.localtime(_time.time()))
+
+ def do_change(self):
+ self.timeout = None
+ if self.owner:
+ self.owner.queue_draw()
+
+zonelist = None
+def get_zone(ind):
+ global zonelist
+ if zonelist == None:
+ try:
+ f = open("/data/timezone_list")
+ l = f.readlines()
+ except IOError:
+ l = []
+ zonelist = map(lambda x:x.strip(), l)
+ if ind < 0 or ind >= len(zonelist):
+ return "UTC"
+ return zonelist[ind]
+
+class tz:
+ # Arg is either a timezone name or a number
+ # to index into a file containing a list of
+ # timezone names '0' is first line
+ def __init__(self, name, owner, zone):
+ try:
+ ind = int(zone)
+ self.zone = get_zone(ind)
+ except ValueError:
+ self.zone = zone
+ self.buttons = lambda:None
+ self.embedded = lambda:None
+ self.format = 'darkgreen'
+ self.owner = owner
+
+ def get_name(self):
+ if 'TZ' in os.environ:
+ TZ = os.environ['TZ']
+ else:
+ TZ = None
+ os.environ['TZ'] = self.zone
+ _time.tzset()
+ now = _time.time()
+ tm = _time.strftime("%d-%b-%Y %H:%M", _time.localtime(now))
+
+ if TZ:
+ os.environ['TZ'] = TZ
+ else:
+ del(os.environ['TZ'])
+ _time.tzset()
+ return '<span size="10000">'+tm+"\n"+self.zone+'</span>'
+
+
+class file:
+ def __init__(self, name, owner, path):
+ self.buttons = lambda:None
+ self.embedded = lambda:None
+ self.path = path
+ self.format = 'cmd'
+ self.owner = owner
+ self.watch = None
+ self.fwatch = None
+
+ def get_name(self):
+ try:
+ f = open(self.path)
+ l = f.readline().strip()
+ f.close()
+ except IOError:
+ l = "-"
+ self.set_watch()
+
+ return l
+
+ def set_watch(self):
+ if self.watch == None:
+ try:
+ self.watch = dnotify.dir(os.path.dirname(self.path))
+ except OSError:
+ self.watch = None
+ if self.watch == None:
+ return
+ if self.fwatch == None:
+ self.fwatch = self.watch.watch(os.path.basename(self.path),
+ self.file_changed)
+
+ def file_changed(self, thing):
+ if self.fwatch:
+ self.fwatch.cancel()
+ self.fwatch = None
+ if self.owner:
+ self.owner.queue_draw()
+
+class quit:
+ def __init__(self, name, owner):
+ self.buttons = lambda:['Exit']
+ self.embedded = lambda:None
+ self.name = name
+ self.format = 'cmd'
+
+ def press(self, ind):
+ gtk.main_quit()
+
+class zoom:
+ def __init__(self, name, owner):
+ self.buttons = lambda:['group+','group-','task+','task-']
+ self.owner = owner
+ self.name = name
+ self.format = 'cmd'
+ self.embedded = lambda:None
+ def press(self, ind):
+ w = self.owner.win
+ if ind == 0:
+ w.grouplist.set_zoom(w.grouplist.zoom+1)
+ if ind == 1:
+ w.grouplist.set_zoom(w.grouplist.zoom-1)
+ if ind == 2:
+ w.tasklist.set_zoom(w.tasklist.zoom+1)
+ if ind == 3:
+ w.tasklist.set_zoom(w.tasklist.zoom-1)
--- /dev/null
+import os, stat
+
+class alert:
+ def __init__(self, name, owner):
+ blist = []
+ for i in os.listdir("/etc/alert"):
+ if stat.S_ISDIR(os.lstat("/etc/alert/"+i)[0]):
+ blist.append(i)
+ self.blist = blist
+
+ self.embedded = lambda:None
+ self.format = 'cmd'
+ self.timeout = None
+ self.owner = owner
+ try:
+ self.mode = os.readlink("/etc/alert/normal")
+ except:
+ self.mode = '??'
+
+ def buttons(self):
+ return self.blist
+
+ def press(self, ind):
+ if ind < 0 or ind >= len(self.blist):
+ return
+ o = self.blist[ind]
+ os.unlink("/etc/alert/normal")
+ os.symlink(o, "/etc/alert/normal")
+ self.mode = o
+ self.owner.queue_draw()
+
+ def get_name(self):
+ return 'mode: ' + self.mode
+
--- /dev/null
+#
+# plato plugin for text messages
+#
+#
+
+import gtk
+from plato_internal import file
+from fingerscroll import FingerScroll
+from storesms import SMSstore
+
+import contactdb
+contacts = None
+
+def protect(txt):
+ txt = txt.replace('&', '&')
+ txt = txt.replace('<', '<')
+ txt = txt.replace('>', '>')
+ return txt
+
+class newmsg(file):
+ # display either (SMS) or recipient of last message
+ # button is "open" to load SendSMS
+ # embedded is most recent text message
+ def __init__(self, name, owner):
+ file.__init__(self, name, owner, "/data/SMS/newmesg")
+ self.owner = self
+ self.gowner = owner
+ self.get_name = self.mgetname
+ self.buttons = self.mbuttons
+ self.embedded = self.membedded
+ self.messages = 0
+ self.who_from = None
+ self.buffer = FingerScroll(gtk.WRAP_WORD_CHAR)
+ self.buffer.show()
+ self.buff = self.buffer.get_buffer()
+ self.store = SMSstore("/data/SMS")
+ self.last_txt = 0
+ global contacts
+ if contacts == None:
+ contacts = contactdb.contacts()
+ self.queue_draw()
+
+ def mgetname(self):
+ if self.messages == 0:
+ return '(SMS)'
+ if self.messages == 1:
+ return self.who_from
+ return '%s (+%d)' % (self.who_from, self.messages-1)
+
+ def mbuttons(self):
+ return ['Open']
+
+ def membedded(self):
+ if self.messages == 0:
+ return None
+ return self.buffer
+ def embed_full(self):
+ return False
+
+ def press(self, ind):
+ self.gowner.emit('request-window','SendSMS')
+
+ def set_display(self, mesg):
+ self.buff.delete(self.buff.get_start_iter(),
+ self.buff.get_end_iter())
+ self.buff.insert(self.buff.get_end_iter(), mesg)
+
+ def queue_draw(self):
+ # newmesg has changed
+ # need to get new messages and set
+ # embedded, message, and from
+ #
+ (next, l) = self.store.lookup(None, 'NEW')
+ self.messages = len(l)
+ if len(l) >= 1:
+ self.set_display(l[0].text)
+ self.who_from = l[0].correspondent
+ global contacts
+ if self.who_from:
+ c = contacts.find_num(self.who_from)
+ if c:
+ self.who_from = c.name
+
+ if l[0].stamp > self.last_txt:
+ self.last_txt = l[0].stamp
+ self.gowner.update(self, True)
+ self.gowner.update(self, False)
+ self.gowner.queue_draw()
+ file.set_watch(self)
--- /dev/null
+#
+# A plato 'group' of windows.
+# This needs to be present for any window management
+# to work. i.e. a WinTask won't see the window unless this
+# was been activated.
+
+from wmctrl import *
+import gobject
+
+class Wwrap:
+ # Wrap a window from wmctrl as a Task
+ def __init__(self, w):
+ self.w = w
+ self.name = w.name
+ self.format = "blue"
+ self.embedded = lambda:None
+
+ def buttons(self):
+ if self.w.pid > 0:
+ return ['Raise', 'Close', 'Kill']
+ return ['Raise','Close']
+
+ def press(self, ind):
+ if ind == 0:
+ self.w.raise_win()
+ return
+ if ind == 1:
+ self.w.close_win()
+ return
+ if ind == 2:
+ if self.w.pid > 1:
+ os.kill(self.w.pid, 15)
+ return
+
+class WindowType:
+ def __init__(self, win, name):
+ self.owner = win
+ self.format = 'group'
+ self.name = name
+ self.buttons = lambda:None
+ self.embedded = lambda:None
+ self.ignore = []
+ self.list = winlist(add_handle = self.add)
+ self.current = {}
+ gobject.io_add_watch(self.list.fd, gobject.IO_IN, self.list.events)
+ self.list.on_change(self.change, self.add, self.delete)
+
+ def parse(self, line):
+ # any window names listed in config file are ignored
+ self.ignore.append(line)
+
+ def get_task(self, ind):
+ w = self.list.winfo
+ if ind >= len(w):
+ return None
+ return Wwrap(w[self.list.windows[ind]])
+
+ def change(self):
+ self.owner.queue_draw()
+
+ def add(self, window):
+ print "emit new window", window.name
+ self.owner.emit('new-window',window)
+
+ def delete(self, wid):
+ self.owner.emit('lost-window',wid)
--- /dev/null
+"black":64,81:"Welcome to"
+"black":72,159:62,162:45,170:34,178:36,192:48,201:63,207:79,214:92,226:97,245:95,263:83,278:65,287:51,289:50,278
+"black":146,200:136,204:122,212:120,241:131,246:144,249:159,243:169,235
+"black":177,206:188,221:193,231:186,211:186,199:199,187:209,183:219,182:234,181:238,191
+"black":255,186:261,196:270,213
+"black":263,111:270,142:276,170:280,186:284,199:286,209:292,224:292,213:293,203:294,192:299,178:317,187:317,201:307,210:292,202
+"black":321,103:321,119:327,156:330,176:332,193:333,207:337,194:345,175:357,171:365,182:360,197:333,194
+"black":371,96:371,108:374,124:376,138:380,154:382,171:387,195
+"black":416,182:421,171:433,161:429,151:417,155:407,168:405,183:410,193:422,196:454,183
+"red":433,211:423,212:405,214:393,215:380,218:367,221:355,225:344,230:332,233:319,237:307,241:295,245:283,250:258,258:235,264:224,267:214,270:201,273:189,276:177,279:153,285:142,289:129,293:117,296:106,300:96,303:79,310:63,316
+"black":92,384:91,396:92,417:91,429:89,417:88,400:90,385:94,371:102,360:113,357:123,359:131,371:129,383:123,394:112,402:100,403
+"black":133,403:139,414:137,401:148,390:162,390:173,395
+"black":174,411:189,407:200,404:208,394:198,388:186,391:178,403:182,418:195,426:208,426:221,422
+"black":253,398:240,394:227,396:232,406:243,410:225,421
+"black":288,405:264,404:275,413:286,418:276,428:259,431:249,431
+"red":359,349:370,359:381,370:391,377:404,391:399,401:389,407:379,411:366,418:356,425:346,431
+"red":94,506:"to continue"
+"red":65,48:71,36:70,23:69,12:59,23
+"red":68,7:76,17:85,29
--- /dev/null
+"black":31,42:"Here is a page number"
+"black":350,36:361,38:372,40:383,36:395,35:405,33:416,29:426,25:441,18:450,7:440,8:451,7:454,17:453,27
+"black":33,103:"Use"
+"black":202,82:192,80:180,90:168,101:158,106:171,118:189,129:199,135
+"black":241,113:"To go back"
+"black":92,161:106,168:116,174:128,183:140,193:134,204:124,214:113,225:103,234:93,242
+"black":176,198:"for next"
+"black":58,259:54,274:58,291:65,303:79,308:91,303:101,264:101,254
+"black":112,276:117,289:120,300:124,289:138,284:149,295
+"black":192,283:182,282:172,285:170,296:180,298:190,290:192,279:191,265:186,246:183,234:186,251:189,262:190,273:192,284:194,294:197,304
+"black":217,291:226,280:236,278:247,287:245,299:226,305:213,294:215,284:225,280:241,280
+"black":58,349:67,359:71,371:70,360:71,350:77,338:90,335:102,337:112,342
+"black":123,352:134,355:146,355:156,354:163,343:151,338:135,340:129,356:141,371:167,375:184,372
+"black":224,336:222,346:208,350:201,364:208,376:218,372:225,357:225,337:222,321:224,342:227,353:230,363:235,373
+"black":261,361:262,349:274,343:285,348:288,359:272,374:258,369:258,359:264,349
+"black":65,406:68,395:69,409:71,425:73,436
+"black":86,412:76,414:58,416:46,416
+"black":110,436:"add page"
+"black":96,499:84,499:73,500:63,500:53,502
+"black":121,510:"remove page"
+"black":365,503:375,500:378,510:368,514:360,504:371,503
+"black":402,510:402,500:408,510:397,515:390,504:407,499
+"black":429,507:440,504:444,516:430,515:422,503:435,502
--- /dev/null
+"black":117,45:122,57:108,51:96,48:82,53:70,62:57,74:47,86:41,100:40,117:41,128:48,146:56,156:66,165:78,171:110,177:133,169
+"black":158,119:"Clear page"
+"black":195,159:"Before removal"
+"black":83,235:84,224:80,250:79,270:78,289:78,319:79,330
+"black":165,229:149,223:128,220:105,218:60,213:46,213:36,213
+"black":120,308:130,303:143,298:156,306:154,320:139,329:129,326:127,308
+"black":186,297:175,305:168,315:178,318:189,315:199,304:201,321:203,332:169,367:162,357:165,344
+"black":241,304:229,298:219,306:216,319:230,325:241,320:251,310:251,299:252,310:257,340:256,362:250,376:236,379:224,372:221,361
+"black":271,227:271,240:274,255:275,271:277,291:278,308:279,318
+"black":299,322:309,320:328,305:318,300:303,313:333,335:361,322
+"black":219,392:222,409:224,426:226,437
+"black":273,405:261,398:217,395:204,394:193,393:179,394
+"black":249,426:261,424:271,424:268,413:252,418:247,431:263,439:285,441:297,441
+"black":295,424:311,427:322,434
+"black":324,420:312,441:302,455
+"black":354,373:349,388:350,400:351,414:353,427:354,444
+"black":380,414:366,405:355,401:343,400:332,398:322,397
+"black":66,492:"Tap to enable"
--- /dev/null
+[Desktop Entry]
+Name=Scribble pad
+Comment=Note pad for scibbles and note taking
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Exec=scribble.py
+Icon=scribble
+Terminal=false
+Categories=GTK;Application;PIM;Office
+SingleInstance=true
+StartupNotify=true
--- /dev/null
+#!/usr/bin/env python
+
+
+# scribble - scribble pad designed for Neo Freerunner
+#
+# Copyright (C) 2008 Neil Brown <neil@brown.name>
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# The GNU General Public License, version 2, is available at
+# http://www.fsf.org/licensing/licenses/info/GPLv2.html
+# Or you can write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Author: Neil Brown
+# Email: <neil@brown.name>
+
+
+# TODO
+# - index page
+# list of pages
+# Buttons: select, delete, new
+# - bigger buttons - fewer
+# Need: undo, redo, colour, text/line
+# When text is selected, these change to:
+# align, join, make-title
+
+import pygtk
+import gtk
+import os
+import pango
+import gobject
+import time
+import listselect
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+def LoadDict(dict):
+ # Upper case.
+ # Where they are like lowercase, we either double
+ # the last stroke (L, J, I) or draw backwards (S, Z, X)
+ # U V are a special case
+
+ dict.add('A', "R(4)6,8")
+ dict.add('B', "R(4)6,4.R(7)1,6")
+ dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+ dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+ dict.add('C', "R(4)8,2")
+ dict.add('D', "R(4)6,6")
+ dict.add('E', "L(1)2,8.L(7)2,8")
+ # double the stem for F
+ dict.add('F', "L(4)2,6.S(3)7,1")
+ dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+ dict.add('G', "L(4)2,5.S(8)1,7")
+ dict.add('G', "L(4)2,5.R(8)6,8")
+ # FIXME I need better straight-curve alignment
+ dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+ dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+ # capital I is down/up
+ dict.add('I', "S(4)1,7.S(4)7,1")
+
+ # Capital J has a left/right tail
+ dict.add('J', "R(4)1,6.S(7)3,5")
+
+ dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+ # Capital L, like J, doubles the foot
+ dict.add('L', "L(4)0,8.S(7)4,3")
+
+ dict.add('M', "R(3)6,5.R(5)3,8")
+ dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+ dict.add('N', "R(3)6,8.L(5)0,2")
+
+ # Capital O is CW, but can be CCW in special dict
+ dict.add('O', "R(4)1,1", bot='0')
+
+ dict.add('P', "R(4)6,3")
+ dict.add('Q', "R(4)7,7.S(8)0,8")
+
+ dict.add('R', "R(4)6,4.S(8)0,8")
+
+ # S is drawn bottom to top.
+ dict.add('S', "L(7)6,1.R(1)7,2")
+
+ # Double the stem for capital T
+ dict.add('T', "R(4)0,8.S(5)7,1")
+
+ # U is L to R, V is R to L for now
+ dict.add('U', "L(4)0,2")
+ dict.add('V', "R(4)2,0")
+
+ dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+ dict.add('W', "R(5)2,3.R(3)5,0")
+
+ dict.add('X', "R(4)6,0")
+
+ dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+ dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+ dict.add('Z', "R(4)8,2.L(4)6,0")
+
+ # Lower case
+ dict.add('a', "L(4)2,2.L(5)1,7")
+ dict.add('a', "L(4)2,2.L(5)0,8")
+ dict.add('a', "L(4)2,2.S(5)0,8")
+ dict.add('b', "S(3)1,7.R(7)6,3")
+ dict.add('c', "L(4)2,8", top='C')
+ dict.add('d', "L(4)5,2.S(5)1,7")
+ dict.add('d', "L(4)5,2.L(5)0,8")
+ dict.add('e', "S(4)3,5.L(4)5,8")
+ dict.add('e', "L(4)3,8")
+ dict.add('f', "L(4)2,6", top='F')
+ dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+ dict.add('g', "L(1)2,2.R(4)1,6")
+ dict.add('h', "S(3)1,7.R(7)6,8")
+ dict.add('h', "L(3)0,5.R(7)6,8")
+ dict.add('i', "S(4)1,7", top='I', bot='1')
+ dict.add('j', "R(4)1,6", top='J')
+ dict.add('k', "L(3)0,5.L(7)2,8")
+ dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+ dict.add('l', "L(4)0,8", top='L')
+ dict.add('l', "S(4)0,8", top='L')
+ dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+ dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+ dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+ dict.add('n', "S(3)1,7.R(4)6,8")
+ dict.add('o', "L(4)1,1", top='O', bot='0')
+ dict.add('p', "S(3)1,7.R(4)6,3")
+ dict.add('q', "L(1)2,2.L(5)1,5")
+ dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+ dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+ # FIXME this double 1,7 is due to a gentle where the
+ # second looks like a line because it is narrow.??
+ dict.add('r', "S(3)1,7.R(4)6,2")
+ dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+ dict.add('s', "L(5)1,8.R(7)2,3", top='S', bot='5')
+ dict.add('t', "R(4)0,8", top='T', bot='7')
+ dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+ dict.add('u', "L(4)0,2.S(5)1,7")
+ dict.add('v', "L(4)0,2.L(2)0,2")
+ dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+ dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+ dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+ dict.add('x', "L(4)0,6", top='X')
+ dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+ dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+ dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+ # Digits
+ dict.add('0', "L(4)7,7")
+ dict.add('0', "R(4)7,7")
+ dict.add('1', "S(4)7,1")
+ dict.add('2', "R(4)0,6.S(7)3,5")
+ dict.add('2', "R(4)3,6.L(4)2,8")
+ dict.add('3', "R(1)0,6.R(7)1,6")
+ dict.add('4', "L(4)7,5")
+ dict.add('5', "L(1)2,6.R(7)0,3")
+ dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+ dict.add('6', "L(4)2,3")
+ dict.add('7', "S(1)3,5.R(4)1,6")
+ dict.add('7', "R(4)0,6")
+ dict.add('7', "R(4)0,7")
+ dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+ dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+ dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+ dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+ dict.add('9', "L(1)2,2.S(5)1,7")
+
+ dict.add(' ', "S(4)3,5")
+ dict.add('<BS>', "S(4)5,3")
+ dict.add('-', "S(4)3,5.S(4)5,3")
+ dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+ dict.add("<left>", "S(4)5,3.S(3)3,5")
+ dict.add("<right>","S(4)3,5.S(5)5,3")
+ dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+ # Each segment has four elements:
+ # direction: Right Straight Left (R=cw, L=ccw)
+ # location: 0-8.
+ # start: 0-8
+ # finish: 0-8
+ # Segments match if the difference at each element
+ # is 0, 1, or 3 (RSL coded as 012)
+ # A difference of 1 required both to be same / 3
+ # On a match, return number of 0s
+ # On non-match, return -1
+ def __init__(self, str):
+ # D(L)S,R
+ # 0123456
+ self.e = [0,0,0,0]
+ if len(str) != 7:
+ raise ValueError
+ if str[1] != '(' or str[3] != ')' or str[5] != ',':
+ raise ValueError
+ if str[0] == 'R':
+ self.e[0] = 0
+ elif str[0] == 'L':
+ self.e[0] = 2
+ elif str[0] == 'S':
+ self.e[0] = 1
+ else:
+ raise ValueError
+
+ self.e[1] = int(str[2])
+ self.e[2] = int(str[4])
+ self.e[3] = int(str[6])
+
+ def match(self, other):
+ cnt = 0
+ for i in range(0,4):
+ diff = abs(self.e[i] - other.e[i])
+ if diff == 0:
+ cnt += 1
+ elif diff == 3:
+ pass
+ elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+ pass
+ else:
+ return -1
+ return cnt
+
+class DictPattern:
+ # A Dict Pattern is a list of segments.
+ # A parsed pattern matches a dict pattern if
+ # the are the same nubmer of segments and they
+ # all match. The value of the match is the sum
+ # of the individual matches.
+ # A DictPattern is printers as segments joined by periods.
+ #
+ def __init__(self, str):
+ self.segs = map(DictSegment, str.split("."))
+ def match(self,other):
+ if len(self.segs) != len(other.segs):
+ return -1
+ cnt = 0
+ for i in range(0,len(self.segs)):
+ m = self.segs[i].match(other.segs[i])
+ if m < 0:
+ return m
+ cnt += m
+ return cnt
+
+
+class Dictionary:
+ # The dictionary hold all the pattern for symbols and
+ # performs lookup
+ # Each pattern in the directionary can be associated
+ # with 3 symbols. One when drawing in middle of screen,
+ # one for top of screen, one for bottom.
+ # Often these will all be the same.
+ # This allows e.g. s and S to have the same pattern in different
+ # location on the touchscreen.
+ # A match requires a unique entry with a match that is better
+ # than any other entry.
+ #
+ def __init__(self):
+ self.dict = []
+ def add(self, sym, pat, top = None, bot = None):
+ if top == None: top = sym
+ if bot == None: bot = sym
+ self.dict.append((DictPattern(pat), sym, top, bot))
+
+ def _match(self, p):
+ max = -1
+ val = None
+ for (ptn, sym, top, bot) in self.dict:
+ cnt = ptn.match(p)
+ if cnt > max:
+ max = cnt
+ val = (sym, top, bot)
+ elif cnt == max:
+ val = None
+ return val
+
+ def match(self, str, pos = "mid"):
+ p = DictPattern(str)
+ m = self._match(p)
+ if m == None:
+ return m
+ (mid, top, bot) = self._match(p)
+ if pos == "top": return top
+ if pos == "bot": return bot
+ return mid
+
+
+class Point:
+ # This represents a point in the path and all the points leading
+ # up to it. It allows us to find the direction and curvature from
+ # one point to another
+ # We store x,y, and sum/cnt of points so far
+ def __init__(self,x,y) :
+ self.xsum = x
+ self.ysum = y
+ self.x = x
+ self.y = y
+ self.cnt = 1
+
+ def copy(self):
+ n = Point(0,0)
+ n.xsum = self.xsum
+ n.ysum = self.ysum
+ n.x = self.x
+ n.y = self.y
+ n.cnt = self.cnt
+ return n
+
+ def add(self,x,y):
+ if self.x == x and self.y == y:
+ return
+ self.x = x
+ self.y = y
+ self.xsum += x
+ self.ysum += y
+ self.cnt += 1
+
+ def xlen(self,p):
+ return abs(self.x - p.x)
+ def ylen(self,p):
+ return abs(self.y - p.y)
+ def sqlen(self,p):
+ x = self.x - p.x
+ y = self.y - p.y
+ return x*x + y*y
+
+ def xdir(self,p):
+ if self.x > p.x:
+ return 1
+ if self.x < p.x:
+ return -1
+ return 0
+ def ydir(self,p):
+ if self.y > p.y:
+ return 1
+ if self.y < p.y:
+ return -1
+ return 0
+ def curve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ if curve > 6:
+ return 1
+ if curve < -6:
+ return -1
+ return 0
+
+ def Vcurve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ return curve
+
+ def meanpoint(self,p):
+ x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+ y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+ return (x,y)
+
+ def is_sharp(self,A,C):
+ # Measure the cosine at self between A and C
+ # as A and C could be curve, we take the mean point on
+ # self.A and self.C as the points to find cosine between
+ (ax,ay) = self.meanpoint(A)
+ (cx,cy) = self.meanpoint(C)
+ a = ax-self.x; b=ay-self.y
+ c = cx-self.x; d=cy-self.y
+ x = a*c + b*d
+ y = a*d - b*c
+ h = math.sqrt(x*x+y*y)
+ if h > 0:
+ cs = x*1000/h
+ else:
+ cs = 0
+ return (cs > 900)
+
+class BBox:
+ # a BBox records min/max x/y of some Points and
+ # can subsequently report row, column, pos of each point
+ # can also locate one bbox in another
+
+ def __init__(self, p):
+ self.minx = p.x
+ self.maxx = p.x
+ self.miny = p.y
+ self.maxy = p.y
+
+ def width(self):
+ return self.maxx - self.minx
+ def height(self):
+ return self.maxy - self.miny
+
+ def add(self, p):
+ if p.x > self.maxx:
+ self.maxx = p.x
+ if p.x < self.minx:
+ self.minx = p.x
+
+ if p.y > self.maxy:
+ self.maxy = p.y
+ if p.y < self.miny:
+ self.miny = p.y
+ def finish(self, div = 3):
+ # if aspect ratio is bad, we adjust max/min accordingly
+ # before setting [xy][12]. We don't change self.min/max
+ # as they are used to place stroke in bigger bbox.
+ # Normally divisions are at 1/3 and 2/3. They can be moved
+ # by setting div e.g. 2 = 1/2 and 1/2
+ (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+ if (maxx - minx) * 3 < (maxy - miny) * 2:
+ # too narrow
+ mid = int((maxx + minx)/2)
+ halfwidth = int ((maxy - miny)/3)
+ minx = mid - halfwidth
+ maxx = mid + halfwidth
+ if (maxy - miny) * 3 < (maxx - minx) * 2:
+ # too wide
+ mid = int((maxy + miny)/2)
+ halfheight = int ((maxx - minx)/3)
+ miny = mid - halfheight
+ maxy = mid + halfheight
+
+ div1 = div - 1
+ self.x1 = int((div1*minx + maxx)/div)
+ self.x2 = int((minx + div1*maxx)/div)
+ self.y1 = int((div1*miny + maxy)/div)
+ self.y2 = int((miny + div1*maxy)/div)
+
+ def row(self, p):
+ # 0, 1, 2 - top to bottom
+ if p.y <= self.y1:
+ return 0
+ if p.y < self.y2:
+ return 1
+ return 2
+ def col(self, p):
+ if p.x <= self.x1:
+ return 0
+ if p.x < self.x2:
+ return 1
+ return 2
+ def box(self, p):
+ # 0 to 9
+ return self.row(p) * 3 + self.col(p)
+
+ def relpos(self,b):
+ # b is a box within self. find location 0-8
+ if b.maxx < self.x2 and b.minx < self.x1:
+ x = 0
+ elif b.minx > self.x1 and b.maxx > self.x2:
+ x = 2
+ else:
+ x = 1
+ if b.maxy < self.y2 and b.miny < self.y1:
+ y = 0
+ elif b.miny > self.y1 and b.maxy > self.y2:
+ y = 2
+ else:
+ y = 1
+ return y*3 + x
+
+
+def different(*args):
+ cur = 0
+ for i in args:
+ if cur != 0 and i != 0 and cur != i:
+ return True
+ if cur == 0:
+ cur = i
+ return False
+
+def maxcurve(*args):
+ for i in args:
+ if i != 0:
+ return i
+ return 0
+
+class PPath:
+ # a PPath refines a list of x,y points into a list of Points
+ # The Points mark out segments which end at significant Points
+ # such as inflections and reversals.
+
+ def __init__(self, x,y):
+
+ self.start = Point(x,y)
+ self.mid = Point(x,y)
+ self.curr = Point(x,y)
+ self.list = [ self.start ]
+
+ def add(self, x, y):
+ self.curr.add(x,y)
+
+ if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+ (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+ (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+ pass
+ else:
+ self.mid = self.curr.copy()
+
+ if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+ self.start = self.mid.copy()
+ self.list.append(self.start)
+ self.mid = self.curr.copy()
+
+ def close(self):
+ self.list.append(self.curr)
+
+ def get_sectlist(self):
+ if len(self.list) <= 2:
+ return [[0,self.list]]
+ l = []
+ A = self.list[0]
+ B = self.list[1]
+ s = [A,B]
+ curcurve = B.curve(A)
+ for C in self.list[2:]:
+ cabc = C.curve(A)
+ cab = B.curve(A)
+ cbc = C.curve(B)
+ if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+ # B is too pointy, must break here
+ l.append([curcurve, s])
+ s = [B, C]
+ curcurve = cbc
+ elif not different(cabc, cab, cbc, curcurve):
+ # all happy
+ s.append(C)
+ if curcurve == 0:
+ curcurve = maxcurve(cab, cbc, cabc)
+ elif not different(cabc, cab, cbc) :
+ # gentle inflection along AB
+ # was: AB goes in old and new section
+ # now: AB only in old section, but curcurve
+ # preseved.
+ l.append([curcurve,s])
+ s = [A, B, C]
+ curcurve =maxcurve(cab, cbc, cabc)
+ else:
+ # Change of direction at B
+ l.append([curcurve,s])
+ s = [B, C]
+ curcurve = cbc
+
+ A = B
+ B = C
+ l.append([curcurve,s])
+
+ return l
+
+ def remove_shorts(self, bbox):
+ # in self.list, if a point is close to the previous point,
+ # remove it.
+ if len(self.list) <= 2:
+ return
+ w = bbox.width()/10
+ h = bbox.height()/10
+ n = [self.list[0]]
+ leng = w*h*2*2
+ for p in self.list[1:]:
+ l = p.sqlen(n[-1])
+ if l > leng:
+ n.append(p)
+ self.list = n
+
+ def text(self):
+ # OK, we have a list of points with curvature between.
+ # want to divide this into sections.
+ # for each 3 consectutive points ABC curve of ABC and AB and BC
+ # If all the same, they are all in a section.
+ # If not B starts a new section and the old ends on B or C...
+ BB = BBox(self.list[0])
+ for p in self.list:
+ BB.add(p)
+ BB.finish()
+ self.bbox = BB
+ self.remove_shorts(BB)
+ sectlist = self.get_sectlist()
+ t = ""
+ for c, s in sectlist:
+ if c > 0:
+ dr = "R" # clockwise is to the Right
+ elif c < 0:
+ dr = "L" # counterclockwise to the Left
+ else:
+ dr = "S" # straight
+ bb = BBox(s[0])
+ for p in s:
+ bb.add(p)
+ bb.finish()
+ # If all points are in some row or column, then
+ # line is S
+ rwdiff = False; cldiff = False
+ rw = bb.row(s[0]); cl=bb.col(s[0])
+ for p in s:
+ if bb.row(p) != rw: rwdiff = True
+ if bb.col(p) != cl: cldiff = True
+ if not rwdiff or not cldiff: dr = "S"
+
+ t1 = dr
+ t1 += "(%d)" % BB.relpos(bb)
+ t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+ t += t1 + '.'
+ return t[:-1]
+
+
+
+def page_cmp(a,b):
+ if a.lower() < b.lower():
+ return -1
+ if a.lower() > b.lower():
+ return 1
+ if a < b:
+ return -1
+ if a > b:
+ return 1
+ return 0
+
+def inc_name(a):
+ l = len(a)
+ while l > 0 and a[l-1] >= '0' and a[l-1] <= '9':
+ l -= 1
+ # a[l:] is the last number
+ if l == len(a):
+ # there is no number
+ return a + ".1"
+ num = 0 + int(a[l:])
+ return a[0:l] + ("%d" % (num+1))
+
+class ScribblePad:
+
+ def __init__(self):
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.connect("destroy", self.close_application)
+ window.set_title("ScribblePad")
+ #window.set_size_request(480,640)
+ self.window = window
+
+ vb = gtk.VBox()
+ vb.show()
+ self.draw_box = vb
+
+ bar = gtk.HBox(True)
+ bar.set_size_request(-1, 60)
+ vb.pack_end(bar, expand=False)
+ bar.show()
+
+ l = gtk.Label('Page Name')
+ vb.pack_start(l, expand=False)
+ l.show()
+ self.name = l
+
+ page = gtk.DrawingArea()
+ page.set_size_request(480,540)
+ vb.add(page)
+ page.show()
+ ctx = page.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(25*pango.SCALE)
+ page.modify_font(fd)
+
+ l.modify_font(fd)
+
+ dflt = gtk.widget_get_default_style()
+ fd = dflt.font_desc
+ fd.set_absolute_size(25*pango.SCALE)
+
+ self.pixbuf = None
+ self.width = 0
+ self.height = 0
+ self.need_redraw = True
+ self.zoomx = None; self.zoomy = None
+
+ # Now the widgets:
+
+ done = gtk.Button("Done"); done.show()
+ colbtn = gtk.Button("colour"); colbtn.show()
+ undo = gtk.Button('Undo') ; undo.show()
+ redo = gtk.Button('Redo') ; redo.show()
+
+ done.child.modify_font(fd)
+ colbtn.child.modify_font(fd)
+ undo.child.modify_font(fd)
+ redo.child.modify_font(fd)
+
+ bar.add(done)
+ bar.add(colbtn)
+ bar.add(undo)
+ bar.add(redo)
+
+ colbtn.connect("clicked", self.colour_change)
+ undo.connect("clicked", self.undo)
+ redo.connect("clicked", self.redo)
+ done.connect("clicked", self.done)
+
+ self.col_align = colbtn
+ self.undo_join = undo
+ self.redo_rename = redo
+
+ self.page = page
+ self.colbtn = colbtn
+ self.line = None
+ self.lines = []
+ self.hist = [] # undo history
+ self.selecting = False
+ self.textcurs = 0
+ self.textstr = None
+ self.textpos = None
+
+
+ page.connect("button_press_event", self.press)
+ page.connect("button_release_event", self.release)
+ page.connect("motion_notify_event", self.motion)
+ page.connect("expose-event", self.refresh)
+ page.connect("configure-event", self.reconfigure)
+ page.connect("key_press_event", self.type)
+ page.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.STRUCTURE_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.KEY_PRESS_MASK
+ | gtk.gdk.KEY_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
+ page.set_property('can-focus', True)
+
+
+ # Now create the index window
+ # A listselect of all the pages, with a row of buttons:
+ # open delete new undelete
+ ls = listselect.ListSelect(center = False)
+ self.listsel = ls
+
+ ls.connect('selected', self.page_select)
+ ls.set_colour('page','blue')
+ ls.set_zoom(38)
+ ls.show()
+
+ vb = gtk.VBox()
+ vb.show()
+ self.list_box = vb
+
+ vb.add(ls)
+ bar = gtk.HBox(True); bar.show()
+ bar.set_size_request(-1, 60)
+ b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page)
+ b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page)
+ b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page)
+ b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages)
+
+ vb.pack_end(bar, expand=False)
+
+ window.add(self.draw_box)
+
+ window.set_default_size(480,640)
+
+ window.show()
+
+
+ if 'HOME' in os.environ:
+ home = os.environ['HOME']
+ else:
+ home = ""
+ if home == "" or home == "/":
+ home = "/home/root"
+ self.page_dir = home + '/Pages'
+ self.page_dir = '/data/Pages'
+ self.load_pages()
+
+ colourmap = page.get_colormap()
+ self.colourmap = {}
+ self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ]
+ for col in self.colnames:
+ c = gtk.gdk.color_parse(col)
+ gc = page.window.new_gc()
+ gc.line_width = 2
+ gc.set_foreground(colourmap.alloc_color(c))
+ self.colourmap[col] = gc
+ self.reset_colour = False
+
+ self.colour_textmode = self.colourmap['blue']
+
+ self.colourname = "black"
+ self.colour = self.colourmap['black']
+ self.colbtn.child.set_text('black')
+ self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL]
+
+ self.dict = Dictionary()
+ LoadDict(self.dict)
+ self.textstr = None
+
+
+ ctx = page.get_pango_context()
+ fd = ctx.get_font_description()
+ met = ctx.get_metrics(fd)
+ self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+ self.lineascent = met.get_ascent() / pango.SCALE
+
+ self.timeout = None
+
+ window.remove(self.draw_box)
+ window.add(self.list_box)
+
+
+ def close_application(self, widget):
+ self.save_page()
+ gtk.main_quit()
+
+ def load_pages(self):
+ try:
+ os.mkdir(self.page_dir)
+ except:
+ pass
+ self.names = os.listdir(self.page_dir)
+ if len(self.names) == 0:
+ self.names.append("1")
+ self.names.sort(page_cmp)
+ self.pages = {}
+ self.pagenum = 0
+ self.load_page()
+ self.update_list()
+
+ def update_list(self):
+ l = []
+ for p in self.names:
+ l.append([p,'page'])
+ self.listsel.list = l
+ self.listsel.list_changed()
+ self.listsel.select(self.pagenum)
+ return
+
+ def page_select(self, list, item):
+ if item == None:
+ return
+ self.pagenum = item
+
+ def open_page(self, b):
+ self.load_page()
+ self.window.remove(self.list_box)
+ self.window.add(self.draw_box)
+
+ def done(self, b):
+ self.flush_text()
+ self.save_page()
+ self.window.remove(self.draw_box)
+ self.window.add(self.list_box)
+
+ def del_page(self, b):
+ pass
+
+ def new_page(self, b):
+ newname = self.choose_unique(self.names[self.pagenum])
+ self.names = self.names[0:self.pagenum+1] + [ newname ] + \
+ self.names[self.pagenum+1:]
+ self.pagenum += 1;
+ self.update_list()
+ self.listsel.select(self.pagenum)
+
+ return
+
+ def undelete_pages(self, b):
+ pass
+
+
+ def type(self, c, ev):
+ if ev.keyval == 65288:
+ self.add_sym('<BS>')
+ elif ev.string == '\r':
+ self.add_sym('<newline>')
+ else:
+ self.add_sym(ev.string)
+
+ def press(self, c, ev):
+ # Start a new line
+ if self.timeout:
+ gobject.source_remove(self.timeout)
+ self.timeout = None
+ self.taptime = time.time()
+ self.movetext = None
+ c.grab_focus()
+ self.movetimeout = None
+ self.selecting = False
+ if self.selection:
+ self.selection = None
+ self.need_redraw = True
+ self.name_buttons()
+ self.redraw()
+
+ self.line = [ self.colourname, [int(ev.x), int(ev.y)] ]
+ return
+ def release(self, c, ev):
+ if self.movetimeout:
+ gobject.source_remove(self.movetimeout)
+ self.movetimeout = None
+
+ if self.movetext:
+ self.movetext = None
+ self.line = None
+ self.need_redraw = True
+ self.redraw()
+ return
+ if self.line == None:
+ return
+
+ if self.selecting:
+ self.selecting = False
+ self.line = None
+ return
+ if self.timeout == None:
+ self.timeout = gobject.timeout_add(20*1000, self.tick)
+
+ if len(self.line) == 2:
+ # just set a cursor
+ need_redraw = ( self.textstr != None)
+ oldpos = None
+ if not self.textstr:
+ oldpos = self.textpos
+ self.flush_text()
+ if need_redraw:
+ self.redraw()
+ (lineno,index) = self.find_text(self.line[1])
+
+ if lineno == None:
+ # new text,
+ pos = self.align_text(self.line[1])
+ if oldpos and abs(pos[0]-oldpos[0]) < 40 and \
+ abs(pos[1] - oldpos[1]) < 40:
+ # turn of text mode
+ self.flush_text()
+ self.line = None
+ self.need_redraw = True
+ self.setlabel()
+ return
+ self.textpos = pos
+ self.textstr = ""
+ self.textcurs = 0
+ # draw the cursor
+ self.draw_text(pos, self.colour, "", 0)
+ self.line = None
+ else:
+ # clicked inside an old text.
+ # shuffle it to the top, open it, edit.
+ ln = self.lines[lineno]
+ self.lines = self.lines[:lineno] + self.lines[lineno+1:]
+ self.textpos = ln[1]
+ self.textstr = ln[2]
+ if ln[0] in self.colourmap:
+ self.colourname = ln[0]
+ else:
+ self.colourname = "black"
+ self.colbtn.child.set_text(self.colourname)
+ self.colour = self.colourmap[self.colourname]
+ self.textcurs = index + 1
+ self.need_redraw = True
+ self.redraw()
+ self.setlabel()
+ self.line = None
+ return
+ if self.textstr != None:
+ sym = self.getsym()
+ if sym:
+ self.add_sym(sym)
+ else:
+ self.redraw()
+ self.line = None
+ self.reset_colour = True
+ return
+
+ self.lines.append(self.line)
+ self.line = None
+ self.reset_colour = True
+ self.need_redraw = True
+ return
+ def motion(self, c, ev):
+ if self.line:
+ if ev.is_hint:
+ x, y, state = ev.window.get_pointer()
+ else:
+ x = ev.x
+ y = ev.y
+ x = int(x)
+ y = int(y)
+ prev = self.line[-1]
+ if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+ return
+ if not self.movetext and len(self.line) == 2 and time.time() - self.taptime > 0.5:
+ self.flush_text()
+ (lineno, index) = self.find_text(prev)
+ if lineno != None:
+ self.movetext = self.lines[lineno]
+ self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]]
+ if self.movetext:
+ self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]]
+ self.need_redraw = True
+ self.redraw()
+ return
+
+ if self.movetimeout:
+ gobject.source_remove(self.movetimeout)
+ self.movetimeout = gobject.timeout_add(650, self.movetick)
+
+ if self.textstr != None:
+ c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
+ else:
+ c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+ self.line.append([x,y])
+ return
+
+ def movetick(self):
+ # longish pause while drawing
+ self.flush_text()
+ self.selecting = True
+ self.need_redraw = True
+ self.redraw()
+
+ def tick(self):
+ # nothing for 20 seconds, flush the page
+ self.save_page()
+ gobject.source_remove(self.timeout)
+ self.timeout = None
+
+ def find_text(self, pos):
+ x = pos[0]; y = pos[1] + self.lineascent
+ for i in range(0, len(self.lines)):
+ p = self.lines[i]
+ if type(p[2]) != str:
+ continue
+ if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
+ # could be this line - check more precisely
+ layout = self.page.create_pango_layout(p[2]+' ')
+ (ink, log) = layout.get_pixel_extents()
+ (ex,ey,ew,eh) = log
+ if x < p[1][0] + ex or x > p[1][0] + ex + ew or \
+ y < p[1][1] + ey or \
+ y > p[1][1] + ey + self.lineheight :
+ continue
+ # OK, it is in this one. Find out where.
+ (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
+ (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
+ if index >= len(p[2]):
+ index = len(p[2])-1
+ return (i, index)
+ return (None, None)
+
+ def align_text(self, pos):
+ # align pos to existing text.
+ # if pos is near one-line-past a previous text, move the exactly
+ # one-line-past
+ x = pos[0]; y = pos[1] + self.lineascent
+ for l in self.lines:
+ if type(l[2]) != str:
+ continue
+ if abs(x - l[1][0]) > self.lineheight:
+ continue
+ if abs(y - (l[1][1] + self.lineheight)) > self.lineheight:
+ continue
+ return [ l[1][0], l[1][1] + self.lineheight ]
+ return pos
+
+ def flush_text(self):
+ if self.textstr == None:
+ self.textpos = None
+ return
+ self.setlabel()
+ if len(self.textstr) == 0:
+ self.textstr = None
+ self.textpos = None
+ return
+ l = [self.colourname, self.textpos, self.textstr]
+ self.lines.append(l)
+ self.need_redraw = True
+ self.textstr = None
+ self.textpos = None
+
+ def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False):
+ if drawable == None:
+ drawable = self.page.window
+ layout = self.page.create_pango_layout(str)
+ if self.zoomx != None:
+ xs = 2; xo = -self.zoomx
+ ys = 2; yo = -self.zoomy
+ else:
+ xs = 1 ; xo = 0
+ ys = 1 ; yo = 0
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if len(pos) == 2 or pos[2] != ew or pos[3] != eh:
+ while len(pos) > 2:
+ pos.pop()
+ pos.append(ew)
+ pos.append(eh)
+ if selected:
+ drawable.draw_rectangle(self.colourmap['yellow'], True,
+ ex+pos[0], ey+pos[1]-self.lineascent, ew, eh)
+ drawable.draw_layout(colour, pos[0]*xs+xo,
+ (pos[1] - self.lineascent)*ys + yo,
+ layout)
+ if cursor != None:
+ (strong,weak) = layout.get_cursor_pos(cursor)
+ (x,y,width,height) = strong
+ x = pos[0] + x / pango.SCALE
+ y = pos[1]
+ drawable.draw_line(self.colour_textmode,
+ x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo)
+ drawable.draw_line(self.colour_textmode,
+ x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo)
+
+ def add_sym(self, sym):
+ if self.textstr == None:
+ return
+ if sym == "<BS>":
+ if self.textcurs > 0:
+ self.textstr = self.textstr[0:self.textcurs-1]+ \
+ self.textstr[self.textcurs:]
+ self.textcurs -= 1
+ elif sym == "<left>":
+ if self.textcurs > 0:
+ self.textcurs -= 1
+ elif sym == "<right>":
+ if self.textcurs < len(self.textstr):
+ self.textcurs += 1
+ elif sym == "<newline>":
+ tail = self.textstr[self.textcurs:]
+ self.textstr = self.textstr[:self.textcurs]
+ oldpos = self.textpos
+ self.flush_text()
+ self.textcurs = len(tail)
+ self.textstr = tail
+ self.textpos = [ oldpos[0], oldpos[1] +
+ self.lineheight ]
+ else:
+ self.textstr = self.textstr[0:self.textcurs] + sym + \
+ self.textstr[self.textcurs:]
+ self.textcurs += 1
+ self.redraw()
+
+
+ def getsym(self):
+ alloc = self.page.get_allocation()
+ pagebb = BBox(Point(0,0))
+ pagebb.add(Point(alloc.width, alloc.height))
+ pagebb.finish(div = 2)
+
+ p = PPath(self.line[1][0], self.line[1][1])
+ for pp in self.line[1:]:
+ p.add(pp[0], pp[1])
+ p.close()
+ patn = p.text()
+ pos = pagebb.relpos(p.bbox)
+ tpos = "mid"
+ if pos < 3:
+ tpos = "top"
+ if pos >= 6:
+ tpos = "bot"
+ sym = self.dict.match(patn, tpos)
+ if sym == None:
+ print "Failed to match pattern:", patn
+ return sym
+
+ def refresh(self, area, ev):
+ self.redraw()
+
+ def setlabel(self):
+ if self.textstr == None:
+ self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)')
+ else:
+ self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)')
+
+ def name_buttons(self):
+ if self.selection:
+ self.col_align.child.set_text('Align')
+ self.undo_join.child.set_text('Join')
+ self.redo_rename.child.set_text('Rename')
+ else:
+ self.col_align.child.set_text(self.colourname)
+ self.undo_join.child.set_text('Undo')
+ self.redo_rename.child.set_text('Redo')
+
+ def redraw(self):
+ self.setlabel()
+
+ if self.need_redraw:
+ self.draw_buf()
+ self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+ self.width, self.height)
+
+ if self.textstr != None:
+ self.draw_text(self.textpos, self.colour, self.textstr,
+ self.textcurs)
+
+ return
+ def draw_buf(self):
+ if self.pixbuf == None:
+ alloc = self.page.get_allocation()
+ self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height)
+ self.width = alloc.width
+ self.height = alloc.height
+
+ self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+ self.width, self.height)
+ if self.zoomx != None:
+ xs = 2; xo = -self.zoomx
+ ys = 2; yo = -self.zoomy
+ else:
+ xs = 1 ; xo = 0
+ ys = 1 ; yo = 0
+ self.selection = []
+ for l in self.lines:
+ if l[0] in self.colourmap:
+ col = self.colourmap[l[0]]
+ else:
+ col = self.colourmap['black']
+ st = l[1]
+ if type(l[2]) == list:
+ for p in l[2:]:
+ self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo,
+ p[0]*xs + xo,p[1]* ys + yo)
+ st = p
+ if type(l[2]) == str:
+ # if this text is 'near' the current line, make it red
+ selected = False
+ if self.selecting and self.line and len(st) == 4:
+ for p in self.line[1:]:
+ if p[0] > st[0] and \
+ p[0] < st[0] + st[2] and \
+ p[1] > st[1] - self.lineascent and \
+ p[1] < st[1] - self.lineascent + st[3]:
+ selected = True
+ break
+ if selected:
+ self.selection.append(l)
+ self.draw_text(st, col, l[2], drawable = self.pixbuf,
+ selected = selected)
+ self.need_redraw = False
+ self.name_buttons()
+
+
+ def reconfigure(self, w, ev):
+ alloc = w.get_allocation()
+ if self.pixbuf == None:
+ return
+ if alloc.width != self.width or alloc.height != self.height:
+ self.pixbuf = None
+ self.need_redraw = True
+
+
+ def colour_change(self,t):
+ if self.selection:
+ # button is 'join' not 'colour'
+ return self.realign(t)
+
+ if self.reset_colour and self.colourname != 'black':
+ next = 'black'
+ else:
+ next = 'black'
+ prev = ''
+ for c in self.colnames:
+ if self.colourname == prev:
+ next = c
+ prev = c
+ self.reset_colour = False
+ self.colourname = next
+ self.colour = self.colourmap[next]
+ t.child.set_text(next)
+ if self.textstr:
+ self.draw_text(self.textpos, self.colour, self.textstr,
+ self.textcurs)
+
+ return
+ def text_change(self,t):
+ self.flush_text()
+ return
+ def undo(self,b):
+ if self.selection:
+ return self.join(b)
+
+ if len(self.lines) == 0:
+ return
+ self.hist.append(self.lines.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+ def redo(self,b):
+ if self.selection:
+ self.rename(self.selection[0][2])
+ return
+ if len(self.hist) == 0:
+ return
+ self.lines.append(self.hist.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+ def choose_unique(self, newname):
+ while newname in self.names:
+ new2 = inc_name(newname)
+ if new2 not in self.names:
+ newname = new2
+ elif (newname + ".1") not in self.names:
+ newname = newname + ".1"
+ else:
+ newname = newname + ".0.1"
+
+ return newname
+ def delete(self,b):
+ # hack
+ if self.selection:
+ return self.join(b)
+ self.flush_text()
+ if len(self.names) <= 1:
+ return
+ if len(self.lines) > 0:
+ return
+ self.save_page()
+ nm = self.names[self.pagenum]
+ if nm in self.pages:
+ del self.pages[nm]
+ self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
+ if self.pagenum >= len(self.names):
+ self.pagenum -= 1
+ self.load_page()
+ self.need_redraw = True
+ self.redraw()
+
+ return
+
+ def cmplines(self, a,b):
+ pa = a[1]
+ pb = b[1]
+ if pa[1] != pb[1]:
+ return pa[1] - pb[1]
+ return pa[0] - pb[0]
+
+ def realign(self, b):
+ self.selection.sort(self.cmplines)
+ x = self.selection[0][1][0]
+ y = self.selection[0][1][1]
+ for i in range(len(self.selection)):
+ self.selection[i][1][0] = x
+ self.selection[i][1][1] = y
+ y += self.lineheight
+ self.need_redraw = True
+ self.redraw()
+
+ def join(self, b):
+ self.selection.sort(self.cmplines)
+ txt = ""
+ for i in range(len(self.selection)):
+ if txt:
+ txt = txt + ' ' + self.selection[i][2]
+ else:
+ txt = self.selection[i][2]
+ self.selection[i][2] = None
+ self.selection[0][2] = txt
+ i = 0;
+ while i < len(self.lines):
+ if len(self.lines[i]) > 2 and self.lines[i][2] == None:
+ self.lines = self.lines[:i] + self.lines[i+1:]
+ else:
+ i += 1
+ self.need_redraw = True
+ self.redraw()
+
+
+ def rename(self, newname):
+ # Rename current page and rename the file
+ if self.names[self.pagenum] == newname:
+ return
+ self.save_page()
+ newname = self.choose_unique(newname)
+ oldpath = self.page_dir + "/" + self.names[self.pagenum]
+ newpath = self.page_dir + "/" + newname
+ try :
+ os.rename(oldpath, newpath)
+ self.names[self.pagenum] = newname
+ self.names.sort(page_cmp)
+ self.pagenum = self.names.index(newname)
+ self.setlabel()
+ except:
+ pass
+ self.update_list()
+
+ def setname(self,b):
+ if self.textstr:
+ if len(self.textstr) > 0:
+ self.rename(self.textstr)
+
+ def clear(self,b):
+ while len(self.lines) > 0:
+ self.hist.append(self.lines.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+
+ def parseline(self, l):
+ # string in "", or num,num. ':' separates words
+ words = l.strip().split(':')
+ line = []
+ for w in words:
+ if w[0] == '"':
+ w = w[1:-1]
+ elif w.find(',') >= 0:
+ n = w.find(',')
+ x = int(w[:n])
+ y = int(w[n+1:])
+ w = [x,y]
+ line.append(w)
+ return line
+
+ def load_page(self):
+ self.need_redraw = True
+ nm = self.names[self.pagenum]
+ if nm in self.pages:
+ self.lines = self.pages[nm]
+ return
+ self.lines = [];
+ try:
+ f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
+ except:
+ f = None
+ if f:
+ l = f.readline()
+ while len(l) > 0:
+ self.lines.append(self.parseline(l))
+ l = f.readline()
+ f.close()
+ return
+
+ def save_page(self):
+ t = self.textstr; tc = self.textcurs
+ self.flush_text()
+ self.pages[self.names[self.pagenum]] = self.lines
+ tosave = self.lines
+ if t and len(t):
+ # restore the text
+ ln = self.lines[-1]
+ self.lines = self.lines[:-1]
+ self.textpos = ln[1]
+ self.textstr = ln[2]
+ self.textcurs = tc
+
+ fn = self.page_dir + "/" + self.names[self.pagenum]
+ if len(tosave) == 0:
+ try:
+ os.unlink(fn)
+ except:
+ pass
+ return
+ f = open(fn, "w")
+ for l in tosave:
+ start = True
+ if not l:
+ continue
+ for w in l:
+ if not start:
+ f.write(":")
+ start = False
+ if isinstance(w, str):
+ f.write('"%s"' % w)
+ elif isinstance(w, list):
+ f.write("%d,%d" %( w[0],w[1]))
+ f.write("\n")
+ f.close()
+
+def main():
+ gtk.main()
+ return 0
+if __name__ == "__main__":
+ ScribblePad()
+ main()
--- /dev/null
+
+oldinstall:
+ cp scribble.py /usr/bin
+ chmod a+rx /usr/bin/scribble.py
+ cp scribble.desktop /usr/share/applications
+ chmod a+r /usr/share/applications/scribble.desktop
+ cp scribble.png /usr/share/pixmaps/scribble.png
+ chmod a+r /usr/share/pixmaps/scribble.png
+ if [ -d $$HOME/Pages ] ; then : ; else cp -r Sample-Pages $$HOME/Pages; fi
+
+install:
+ chmod +x scribble.py
+ $(CP) scribble.py $(DEST)/usr/local/bin/scribble
--- /dev/null
+"black":64,81:"Welcome to"
+"black":72,159:62,162:45,170:34,178:36,192:48,201:63,207:79,214:92,226:97,245:95,263:83,278:65,287:51,289:50,278
+"black":146,200:136,204:122,212:120,241:131,246:144,249:159,243:169,235
+"black":177,206:188,221:193,231:186,211:186,199:199,187:209,183:219,182:234,181:238,191
+"black":255,186:261,196:270,213
+"black":263,111:270,142:276,170:280,186:284,199:286,209:292,224:292,213:293,203:294,192:299,178:317,187:317,201:307,210:292,202
+"black":321,103:321,119:327,156:330,176:332,193:333,207:337,194:345,175:357,171:365,182:360,197:333,194
+"black":371,96:371,108:374,124:376,138:380,154:382,171:387,195
+"black":416,182:421,171:433,161:429,151:417,155:407,168:405,183:410,193:422,196:454,183
+"red":433,211:423,212:405,214:393,215:380,218:367,221:355,225:344,230:332,233:319,237:307,241:295,245:283,250:258,258:235,264:224,267:214,270:201,273:189,276:177,279:153,285:142,289:129,293:117,296:106,300:96,303:79,310:63,316
+"black":92,384:91,396:92,417:91,429:89,417:88,400:90,385:94,371:102,360:113,357:123,359:131,371:129,383:123,394:112,402:100,403
+"black":133,403:139,414:137,401:148,390:162,390:173,395
+"black":174,411:189,407:200,404:208,394:198,388:186,391:178,403:182,418:195,426:208,426:221,422
+"black":253,398:240,394:227,396:232,406:243,410:225,421
+"black":288,405:264,404:275,413:286,418:276,428:259,431:249,431
+"red":359,349:370,359:381,370:391,377:404,391:399,401:389,407:379,411:366,418:356,425:346,431
+"red":94,506:"to continue"
+"red":65,48:71,36:70,23:69,12:59,23
+"red":68,7:76,17:85,29
--- /dev/null
+"black":31,42:"Here is a page number"
+"black":350,36:361,38:372,40:383,36:395,35:405,33:416,29:426,25:441,18:450,7:440,8:451,7:454,17:453,27
+"black":33,103:"Use"
+"black":202,82:192,80:180,90:168,101:158,106:171,118:189,129:199,135
+"black":241,113:"To go back"
+"black":92,161:106,168:116,174:128,183:140,193:134,204:124,214:113,225:103,234:93,242
+"black":176,198:"for next"
+"black":58,259:54,274:58,291:65,303:79,308:91,303:101,264:101,254
+"black":112,276:117,289:120,300:124,289:138,284:149,295
+"black":192,283:182,282:172,285:170,296:180,298:190,290:192,279:191,265:186,246:183,234:186,251:189,262:190,273:192,284:194,294:197,304
+"black":217,291:226,280:236,278:247,287:245,299:226,305:213,294:215,284:225,280:241,280
+"black":58,349:67,359:71,371:70,360:71,350:77,338:90,335:102,337:112,342
+"black":123,352:134,355:146,355:156,354:163,343:151,338:135,340:129,356:141,371:167,375:184,372
+"black":224,336:222,346:208,350:201,364:208,376:218,372:225,357:225,337:222,321:224,342:227,353:230,363:235,373
+"black":261,361:262,349:274,343:285,348:288,359:272,374:258,369:258,359:264,349
+"black":65,406:68,395:69,409:71,425:73,436
+"black":86,412:76,414:58,416:46,416
+"black":110,436:"add page"
+"black":96,499:84,499:73,500:63,500:53,502
+"black":121,510:"remove page"
+"black":365,503:375,500:378,510:368,514:360,504:371,503
+"black":402,510:402,500:408,510:397,515:390,504:407,499
+"black":429,507:440,504:444,516:430,515:422,503:435,502
--- /dev/null
+"black":117,45:122,57:108,51:96,48:82,53:70,62:57,74:47,86:41,100:40,117:41,128:48,146:56,156:66,165:78,171:110,177:133,169
+"black":158,119:"Clear page"
+"black":195,159:"Before removal"
+"black":83,235:84,224:80,250:79,270:78,289:78,319:79,330
+"black":165,229:149,223:128,220:105,218:60,213:46,213:36,213
+"black":120,308:130,303:143,298:156,306:154,320:139,329:129,326:127,308
+"black":186,297:175,305:168,315:178,318:189,315:199,304:201,321:203,332:169,367:162,357:165,344
+"black":241,304:229,298:219,306:216,319:230,325:241,320:251,310:251,299:252,310:257,340:256,362:250,376:236,379:224,372:221,361
+"black":271,227:271,240:274,255:275,271:277,291:278,308:279,318
+"black":299,322:309,320:328,305:318,300:303,313:333,335:361,322
+"black":219,392:222,409:224,426:226,437
+"black":273,405:261,398:217,395:204,394:193,393:179,394
+"black":249,426:261,424:271,424:268,413:252,418:247,431:263,439:285,441:297,441
+"black":295,424:311,427:322,434
+"black":324,420:312,441:302,455
+"black":354,373:349,388:350,400:351,414:353,427:354,444
+"black":380,414:366,405:355,401:343,400:332,398:322,397
+"black":66,492:"Tap to enable"
--- /dev/null
+[Desktop Entry]
+Name=Scribble pad
+Comment=Note pad for scibbles and note taking
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Exec=scribble.py
+Icon=scribble
+Terminal=false
+Categories=GTK;Application;PIM;Office
+SingleInstance=true
+StartupNotify=true
--- /dev/null
+#!/usr/bin/env python
+
+
+# scribble - scribble pad designed for Neo Freerunner
+#
+# Copyright (C) 2008 Neil Brown <neil@brown.name>
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# The GNU General Public License, version 2, is available at
+# http://www.fsf.org/licensing/licenses/info/GPLv2.html
+# Or you can write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Author: Neil Brown
+# Email: <neil@brown.name>
+
+
+# TODO
+# - index page
+# list of pages
+# Buttons: select, delete, new
+# - bigger buttons - fewer
+# Need: undo, redo, colour, text/line
+# When text is selected, these change to:
+# align, join, make-title
+
+import pygtk
+import gtk
+import os
+import pango
+import gobject
+import time
+import listselect
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+def LoadDict(dict):
+ # Upper case.
+ # Where they are like lowercase, we either double
+ # the last stroke (L, J, I) or draw backwards (S, Z, X)
+ # U V are a special case
+
+ dict.add('A', "R(4)6,8")
+ dict.add('B', "R(4)6,4.R(7)1,6")
+ dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+ dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+ dict.add('C', "R(4)8,2")
+ dict.add('D', "R(4)6,6")
+ dict.add('E', "L(1)2,8.L(7)2,8")
+ # double the stem for F
+ dict.add('F', "L(4)2,6.S(3)7,1")
+ dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+ dict.add('G', "L(4)2,5.S(8)1,7")
+ dict.add('G', "L(4)2,5.R(8)6,8")
+ # FIXME I need better straight-curve alignment
+ dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+ dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+ # capital I is down/up
+ dict.add('I', "S(4)1,7.S(4)7,1")
+
+ # Capital J has a left/right tail
+ dict.add('J', "R(4)1,6.S(7)3,5")
+
+ dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+ # Capital L, like J, doubles the foot
+ dict.add('L', "L(4)0,8.S(7)4,3")
+
+ dict.add('M', "R(3)6,5.R(5)3,8")
+ dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+ dict.add('N', "R(3)6,8.L(5)0,2")
+
+ # Capital O is CW, but can be CCW in special dict
+ dict.add('O', "R(4)1,1", bot='0')
+
+ dict.add('P', "R(4)6,3")
+ dict.add('Q', "R(4)7,7.S(8)0,8")
+
+ dict.add('R', "R(4)6,4.S(8)0,8")
+
+ # S is drawn bottom to top.
+ dict.add('S', "L(7)6,1.R(1)7,2")
+
+ # Double the stem for capital T
+ dict.add('T', "R(4)0,8.S(5)7,1")
+
+ # U is L to R, V is R to L for now
+ dict.add('U', "L(4)0,2")
+ dict.add('V', "R(4)2,0")
+
+ dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+ dict.add('W', "R(5)2,3.R(3)5,0")
+
+ dict.add('X', "R(4)6,0")
+
+ dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+ dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+ dict.add('Z', "R(4)8,2.L(4)6,0")
+
+ # Lower case
+ dict.add('a', "L(4)2,2.L(5)1,7")
+ dict.add('a', "L(4)2,2.L(5)0,8")
+ dict.add('a', "L(4)2,2.S(5)0,8")
+ dict.add('b', "S(3)1,7.R(7)6,3")
+ dict.add('c', "L(4)2,8", top='C')
+ dict.add('d', "L(4)5,2.S(5)1,7")
+ dict.add('d', "L(4)5,2.L(5)0,8")
+ dict.add('e', "S(4)3,5.L(4)5,8")
+ dict.add('e', "L(4)3,8")
+ dict.add('f', "L(4)2,6", top='F')
+ dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+ dict.add('g', "L(1)2,2.R(4)1,6")
+ dict.add('h', "S(3)1,7.R(7)6,8")
+ dict.add('h', "L(3)0,5.R(7)6,8")
+ dict.add('i', "S(4)1,7", top='I', bot='1')
+ dict.add('j', "R(4)1,6", top='J')
+ dict.add('k', "L(3)0,5.L(7)2,8")
+ dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+ dict.add('l', "L(4)0,8", top='L')
+ dict.add('l', "S(4)0,8", top='L')
+ dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+ dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+ dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+ dict.add('n', "S(3)1,7.R(4)6,8")
+ dict.add('o', "L(4)1,1", top='O', bot='0')
+ dict.add('p', "S(3)1,7.R(4)6,3")
+ dict.add('q', "L(1)2,2.L(5)1,5")
+ dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+ dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+ # FIXME this double 1,7 is due to a gentle where the
+ # second looks like a line because it is narrow.??
+ dict.add('r', "S(3)1,7.R(4)6,2")
+ dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+ dict.add('s', "L(5)1,8.R(7)2,3", top='S', bot='5')
+ dict.add('t', "R(4)0,8", top='T', bot='7')
+ dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+ dict.add('u', "L(4)0,2.S(5)1,7")
+ dict.add('v', "L(4)0,2.L(2)0,2")
+ dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+ dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+ dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+ dict.add('x', "L(4)0,6", top='X')
+ dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+ dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+ dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+ # Digits
+ dict.add('0', "L(4)7,7")
+ dict.add('0', "R(4)7,7")
+ dict.add('1', "S(4)7,1")
+ dict.add('2', "R(4)0,6.S(7)3,5")
+ dict.add('2', "R(4)3,6.L(4)2,8")
+ dict.add('3', "R(1)0,6.R(7)1,6")
+ dict.add('4', "L(4)7,5")
+ dict.add('5', "L(1)2,6.R(7)0,3")
+ dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+ dict.add('6', "L(4)2,3")
+ dict.add('7', "S(1)3,5.R(4)1,6")
+ dict.add('7', "R(4)0,6")
+ dict.add('7', "R(4)0,7")
+ dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+ dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+ dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+ dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+ dict.add('9', "L(1)2,2.S(5)1,7")
+
+ dict.add(' ', "S(4)3,5")
+ dict.add('<BS>', "S(4)5,3")
+ dict.add('-', "S(4)3,5.S(4)5,3")
+ dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+ dict.add("<left>", "S(4)5,3.S(3)3,5")
+ dict.add("<right>","S(4)3,5.S(5)5,3")
+ dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+ # Each segment has four elements:
+ # direction: Right Straight Left (R=cw, L=ccw)
+ # location: 0-8.
+ # start: 0-8
+ # finish: 0-8
+ # Segments match if the difference at each element
+ # is 0, 1, or 3 (RSL coded as 012)
+ # A difference of 1 required both to be same / 3
+ # On a match, return number of 0s
+ # On non-match, return -1
+ def __init__(self, str):
+ # D(L)S,R
+ # 0123456
+ self.e = [0,0,0,0]
+ if len(str) != 7:
+ raise ValueError
+ if str[1] != '(' or str[3] != ')' or str[5] != ',':
+ raise ValueError
+ if str[0] == 'R':
+ self.e[0] = 0
+ elif str[0] == 'L':
+ self.e[0] = 2
+ elif str[0] == 'S':
+ self.e[0] = 1
+ else:
+ raise ValueError
+
+ self.e[1] = int(str[2])
+ self.e[2] = int(str[4])
+ self.e[3] = int(str[6])
+
+ def match(self, other):
+ cnt = 0
+ for i in range(0,4):
+ diff = abs(self.e[i] - other.e[i])
+ if diff == 0:
+ cnt += 1
+ elif diff == 3:
+ pass
+ elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+ pass
+ else:
+ return -1
+ return cnt
+
+class DictPattern:
+ # A Dict Pattern is a list of segments.
+ # A parsed pattern matches a dict pattern if
+ # the are the same nubmer of segments and they
+ # all match. The value of the match is the sum
+ # of the individual matches.
+ # A DictPattern is printers as segments joined by periods.
+ #
+ def __init__(self, str):
+ self.segs = map(DictSegment, str.split("."))
+ def match(self,other):
+ if len(self.segs) != len(other.segs):
+ return -1
+ cnt = 0
+ for i in range(0,len(self.segs)):
+ m = self.segs[i].match(other.segs[i])
+ if m < 0:
+ return m
+ cnt += m
+ return cnt
+
+
+class Dictionary:
+ # The dictionary hold all the pattern for symbols and
+ # performs lookup
+ # Each pattern in the directionary can be associated
+ # with 3 symbols. One when drawing in middle of screen,
+ # one for top of screen, one for bottom.
+ # Often these will all be the same.
+ # This allows e.g. s and S to have the same pattern in different
+ # location on the touchscreen.
+ # A match requires a unique entry with a match that is better
+ # than any other entry.
+ #
+ def __init__(self):
+ self.dict = []
+ def add(self, sym, pat, top = None, bot = None):
+ if top == None: top = sym
+ if bot == None: bot = sym
+ self.dict.append((DictPattern(pat), sym, top, bot))
+
+ def _match(self, p):
+ max = -1
+ val = None
+ for (ptn, sym, top, bot) in self.dict:
+ cnt = ptn.match(p)
+ if cnt > max:
+ max = cnt
+ val = (sym, top, bot)
+ elif cnt == max:
+ val = None
+ return val
+
+ def match(self, str, pos = "mid"):
+ p = DictPattern(str)
+ m = self._match(p)
+ if m == None:
+ return m
+ (mid, top, bot) = self._match(p)
+ if pos == "top": return top
+ if pos == "bot": return bot
+ return mid
+
+
+class Point:
+ # This represents a point in the path and all the points leading
+ # up to it. It allows us to find the direction and curvature from
+ # one point to another
+ # We store x,y, and sum/cnt of points so far
+ def __init__(self,x,y) :
+ self.xsum = x
+ self.ysum = y
+ self.x = x
+ self.y = y
+ self.cnt = 1
+
+ def copy(self):
+ n = Point(0,0)
+ n.xsum = self.xsum
+ n.ysum = self.ysum
+ n.x = self.x
+ n.y = self.y
+ n.cnt = self.cnt
+ return n
+
+ def add(self,x,y):
+ if self.x == x and self.y == y:
+ return
+ self.x = x
+ self.y = y
+ self.xsum += x
+ self.ysum += y
+ self.cnt += 1
+
+ def xlen(self,p):
+ return abs(self.x - p.x)
+ def ylen(self,p):
+ return abs(self.y - p.y)
+ def sqlen(self,p):
+ x = self.x - p.x
+ y = self.y - p.y
+ return x*x + y*y
+
+ def xdir(self,p):
+ if self.x > p.x:
+ return 1
+ if self.x < p.x:
+ return -1
+ return 0
+ def ydir(self,p):
+ if self.y > p.y:
+ return 1
+ if self.y < p.y:
+ return -1
+ return 0
+ def curve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ if curve > 6:
+ return 1
+ if curve < -6:
+ return -1
+ return 0
+
+ def Vcurve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ return curve
+
+ def meanpoint(self,p):
+ x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+ y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+ return (x,y)
+
+ def is_sharp(self,A,C):
+ # Measure the cosine at self between A and C
+ # as A and C could be curve, we take the mean point on
+ # self.A and self.C as the points to find cosine between
+ (ax,ay) = self.meanpoint(A)
+ (cx,cy) = self.meanpoint(C)
+ a = ax-self.x; b=ay-self.y
+ c = cx-self.x; d=cy-self.y
+ x = a*c + b*d
+ y = a*d - b*c
+ h = math.sqrt(x*x+y*y)
+ if h > 0:
+ cs = x*1000/h
+ else:
+ cs = 0
+ return (cs > 900)
+
+class BBox:
+ # a BBox records min/max x/y of some Points and
+ # can subsequently report row, column, pos of each point
+ # can also locate one bbox in another
+
+ def __init__(self, p):
+ self.minx = p.x
+ self.maxx = p.x
+ self.miny = p.y
+ self.maxy = p.y
+
+ def width(self):
+ return self.maxx - self.minx
+ def height(self):
+ return self.maxy - self.miny
+
+ def add(self, p):
+ if p.x > self.maxx:
+ self.maxx = p.x
+ if p.x < self.minx:
+ self.minx = p.x
+
+ if p.y > self.maxy:
+ self.maxy = p.y
+ if p.y < self.miny:
+ self.miny = p.y
+ def finish(self, div = 3):
+ # if aspect ratio is bad, we adjust max/min accordingly
+ # before setting [xy][12]. We don't change self.min/max
+ # as they are used to place stroke in bigger bbox.
+ # Normally divisions are at 1/3 and 2/3. They can be moved
+ # by setting div e.g. 2 = 1/2 and 1/2
+ (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+ if (maxx - minx) * 3 < (maxy - miny) * 2:
+ # too narrow
+ mid = int((maxx + minx)/2)
+ halfwidth = int ((maxy - miny)/3)
+ minx = mid - halfwidth
+ maxx = mid + halfwidth
+ if (maxy - miny) * 3 < (maxx - minx) * 2:
+ # too wide
+ mid = int((maxy + miny)/2)
+ halfheight = int ((maxx - minx)/3)
+ miny = mid - halfheight
+ maxy = mid + halfheight
+
+ div1 = div - 1
+ self.x1 = int((div1*minx + maxx)/div)
+ self.x2 = int((minx + div1*maxx)/div)
+ self.y1 = int((div1*miny + maxy)/div)
+ self.y2 = int((miny + div1*maxy)/div)
+
+ def row(self, p):
+ # 0, 1, 2 - top to bottom
+ if p.y <= self.y1:
+ return 0
+ if p.y < self.y2:
+ return 1
+ return 2
+ def col(self, p):
+ if p.x <= self.x1:
+ return 0
+ if p.x < self.x2:
+ return 1
+ return 2
+ def box(self, p):
+ # 0 to 9
+ return self.row(p) * 3 + self.col(p)
+
+ def relpos(self,b):
+ # b is a box within self. find location 0-8
+ if b.maxx < self.x2 and b.minx < self.x1:
+ x = 0
+ elif b.minx > self.x1 and b.maxx > self.x2:
+ x = 2
+ else:
+ x = 1
+ if b.maxy < self.y2 and b.miny < self.y1:
+ y = 0
+ elif b.miny > self.y1 and b.maxy > self.y2:
+ y = 2
+ else:
+ y = 1
+ return y*3 + x
+
+
+def different(*args):
+ cur = 0
+ for i in args:
+ if cur != 0 and i != 0 and cur != i:
+ return True
+ if cur == 0:
+ cur = i
+ return False
+
+def maxcurve(*args):
+ for i in args:
+ if i != 0:
+ return i
+ return 0
+
+class PPath:
+ # a PPath refines a list of x,y points into a list of Points
+ # The Points mark out segments which end at significant Points
+ # such as inflections and reversals.
+
+ def __init__(self, x,y):
+
+ self.start = Point(x,y)
+ self.mid = Point(x,y)
+ self.curr = Point(x,y)
+ self.list = [ self.start ]
+
+ def add(self, x, y):
+ self.curr.add(x,y)
+
+ if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+ (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+ (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+ pass
+ else:
+ self.mid = self.curr.copy()
+
+ if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+ self.start = self.mid.copy()
+ self.list.append(self.start)
+ self.mid = self.curr.copy()
+
+ def close(self):
+ self.list.append(self.curr)
+
+ def get_sectlist(self):
+ if len(self.list) <= 2:
+ return [[0,self.list]]
+ l = []
+ A = self.list[0]
+ B = self.list[1]
+ s = [A,B]
+ curcurve = B.curve(A)
+ for C in self.list[2:]:
+ cabc = C.curve(A)
+ cab = B.curve(A)
+ cbc = C.curve(B)
+ if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+ # B is too pointy, must break here
+ l.append([curcurve, s])
+ s = [B, C]
+ curcurve = cbc
+ elif not different(cabc, cab, cbc, curcurve):
+ # all happy
+ s.append(C)
+ if curcurve == 0:
+ curcurve = maxcurve(cab, cbc, cabc)
+ elif not different(cabc, cab, cbc) :
+ # gentle inflection along AB
+ # was: AB goes in old and new section
+ # now: AB only in old section, but curcurve
+ # preseved.
+ l.append([curcurve,s])
+ s = [A, B, C]
+ curcurve =maxcurve(cab, cbc, cabc)
+ else:
+ # Change of direction at B
+ l.append([curcurve,s])
+ s = [B, C]
+ curcurve = cbc
+
+ A = B
+ B = C
+ l.append([curcurve,s])
+
+ return l
+
+ def remove_shorts(self, bbox):
+ # in self.list, if a point is close to the previous point,
+ # remove it.
+ if len(self.list) <= 2:
+ return
+ w = bbox.width()/10
+ h = bbox.height()/10
+ n = [self.list[0]]
+ leng = w*h*2*2
+ for p in self.list[1:]:
+ l = p.sqlen(n[-1])
+ if l > leng:
+ n.append(p)
+ self.list = n
+
+ def text(self):
+ # OK, we have a list of points with curvature between.
+ # want to divide this into sections.
+ # for each 3 consectutive points ABC curve of ABC and AB and BC
+ # If all the same, they are all in a section.
+ # If not B starts a new section and the old ends on B or C...
+ BB = BBox(self.list[0])
+ for p in self.list:
+ BB.add(p)
+ BB.finish()
+ self.bbox = BB
+ self.remove_shorts(BB)
+ sectlist = self.get_sectlist()
+ t = ""
+ for c, s in sectlist:
+ if c > 0:
+ dr = "R" # clockwise is to the Right
+ elif c < 0:
+ dr = "L" # counterclockwise to the Left
+ else:
+ dr = "S" # straight
+ bb = BBox(s[0])
+ for p in s:
+ bb.add(p)
+ bb.finish()
+ # If all points are in some row or column, then
+ # line is S
+ rwdiff = False; cldiff = False
+ rw = bb.row(s[0]); cl=bb.col(s[0])
+ for p in s:
+ if bb.row(p) != rw: rwdiff = True
+ if bb.col(p) != cl: cldiff = True
+ if not rwdiff or not cldiff: dr = "S"
+
+ t1 = dr
+ t1 += "(%d)" % BB.relpos(bb)
+ t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+ t += t1 + '.'
+ return t[:-1]
+
+
+
+def page_cmp(a,b):
+ if a.lower() < b.lower():
+ return -1
+ if a.lower() > b.lower():
+ return 1
+ if a < b:
+ return -1
+ if a > b:
+ return 1
+ return 0
+
+def inc_name(a):
+ l = len(a)
+ while l > 0 and a[l-1] >= '0' and a[l-1] <= '9':
+ l -= 1
+ # a[l:] is the last number
+ if l == len(a):
+ # there is no number
+ return a + ".1"
+ num = 0 + int(a[l:])
+ return a[0:l] + ("%d" % (num+1))
+
+class ScribblePad:
+
+ def __init__(self):
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.connect("destroy", self.close_application)
+ window.set_title("ScribblePad")
+ #window.set_size_request(480,640)
+ self.window = window
+
+ vb = gtk.VBox()
+ vb.show()
+ self.draw_box = vb
+
+ bar = gtk.HBox(True)
+ bar.set_size_request(-1, 60)
+ vb.pack_end(bar, expand=False)
+ bar.show()
+
+ l = gtk.Label('Page Name')
+ vb.pack_start(l, expand=False)
+ l.show()
+ self.name = l
+
+ page = gtk.DrawingArea()
+ page.set_size_request(480,540)
+ vb.add(page)
+ page.show()
+ ctx = page.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(25*pango.SCALE)
+ page.modify_font(fd)
+
+ l.modify_font(fd)
+
+ dflt = gtk.widget_get_default_style()
+ fd = dflt.font_desc
+ fd.set_absolute_size(25*pango.SCALE)
+
+ self.pixbuf = None
+ self.width = 0
+ self.height = 0
+ self.need_redraw = True
+ self.zoomx = None; self.zoomy = None
+
+ # Now the widgets:
+
+ done = gtk.Button("Done"); done.show()
+ colbtn = gtk.Button("colour"); colbtn.show()
+ undo = gtk.Button('Undo') ; undo.show()
+ redo = gtk.Button('Redo') ; redo.show()
+
+ done.child.modify_font(fd)
+ colbtn.child.modify_font(fd)
+ undo.child.modify_font(fd)
+ redo.child.modify_font(fd)
+
+ bar.add(done)
+ bar.add(colbtn)
+ bar.add(undo)
+ bar.add(redo)
+
+ colbtn.connect("clicked", self.colour_change)
+ undo.connect("clicked", self.undo)
+ redo.connect("clicked", self.redo)
+ done.connect("clicked", self.done)
+
+ self.col_align = colbtn
+ self.undo_join = undo
+ self.redo_rename = redo
+
+ self.page = page
+ self.colbtn = colbtn
+ self.line = None
+ self.lines = []
+ self.hist = [] # undo history
+ self.selecting = False
+ self.textcurs = 0
+ self.textstr = None
+ self.textpos = None
+
+
+ page.connect("button_press_event", self.press)
+ page.connect("button_release_event", self.release)
+ page.connect("motion_notify_event", self.motion)
+ page.connect("expose-event", self.refresh)
+ page.connect("configure-event", self.reconfigure)
+ page.connect("key_press_event", self.type)
+ page.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.STRUCTURE_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.KEY_PRESS_MASK
+ | gtk.gdk.KEY_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
+ page.set_property('can-focus', True)
+
+
+ # Now create the index window
+ # A listselect of all the pages, with a row of buttons:
+ # open delete new undelete
+ ls = listselect.ListSelect(center = False)
+ self.listsel = ls
+
+ ls.connect('selected', self.page_select)
+ ls.set_colour('page','blue')
+ ls.set_zoom(38)
+ ls.show()
+
+ vb = gtk.VBox()
+ vb.show()
+ self.list_box = vb
+
+ vb.add(ls)
+ bar = gtk.HBox(True); bar.show()
+ bar.set_size_request(-1, 60)
+ b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page)
+ b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page)
+ b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page)
+ b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages)
+
+ vb.pack_end(bar, expand=False)
+
+ window.add(self.draw_box)
+
+ window.set_default_size(480,640)
+
+ window.show()
+
+
+ if 'HOME' in os.environ:
+ home = os.environ['HOME']
+ else:
+ home = ""
+ if home == "" or home == "/":
+ home = "/home/root"
+ self.page_dir = home + '/Pages'
+ self.page_dir = '/data/Pages'
+ self.load_pages()
+
+ colourmap = page.get_colormap()
+ self.colourmap = {}
+ self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ]
+ for col in self.colnames:
+ c = gtk.gdk.color_parse(col)
+ gc = page.window.new_gc()
+ gc.line_width = 2
+ gc.set_foreground(colourmap.alloc_color(c))
+ self.colourmap[col] = gc
+ self.reset_colour = False
+
+ self.colour_textmode = self.colourmap['blue']
+
+ self.colourname = "black"
+ self.colour = self.colourmap['black']
+ self.colbtn.child.set_text('black')
+ self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL]
+
+ self.dict = Dictionary()
+ LoadDict(self.dict)
+ self.textstr = None
+
+
+ ctx = page.get_pango_context()
+ fd = ctx.get_font_description()
+ met = ctx.get_metrics(fd)
+ self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+ self.lineascent = met.get_ascent() / pango.SCALE
+
+ self.timeout = None
+
+ window.remove(self.draw_box)
+ window.add(self.list_box)
+
+
+ def close_application(self, widget):
+ self.save_page()
+ gtk.main_quit()
+
+ def load_pages(self):
+ try:
+ os.mkdir(self.page_dir)
+ except:
+ pass
+ self.names = os.listdir(self.page_dir)
+ if len(self.names) == 0:
+ self.names.append("1")
+ self.names.sort(page_cmp)
+ self.pages = {}
+ self.pagenum = 0
+ self.load_page()
+ self.update_list()
+
+ def update_list(self):
+ l = []
+ for p in self.names:
+ l.append([p,'page'])
+ self.listsel.list = l
+ self.listsel.list_changed()
+ self.listsel.select(self.pagenum)
+ return
+
+ def page_select(self, list, item):
+ if item == None:
+ return
+ self.pagenum = item
+
+ def open_page(self, b):
+ self.load_page()
+ self.window.remove(self.list_box)
+ self.window.add(self.draw_box)
+
+ def done(self, b):
+ self.flush_text()
+ self.save_page()
+ self.window.remove(self.draw_box)
+ self.window.add(self.list_box)
+
+ def del_page(self, b):
+ pass
+
+ def new_page(self, b):
+ newname = self.choose_unique(self.names[self.pagenum])
+ self.names = self.names[0:self.pagenum+1] + [ newname ] + \
+ self.names[self.pagenum+1:]
+ self.pagenum += 1;
+ self.update_list()
+ self.listsel.select(self.pagenum)
+
+ return
+
+ def undelete_pages(self, b):
+ pass
+
+
+ def type(self, c, ev):
+ if ev.keyval == 65288:
+ self.add_sym('<BS>')
+ elif ev.string == '\r':
+ self.add_sym('<newline>')
+ else:
+ self.add_sym(ev.string)
+
+ def press(self, c, ev):
+ # Start a new line
+ if self.timeout:
+ gobject.source_remove(self.timeout)
+ self.timeout = None
+ self.taptime = time.time()
+ self.movetext = None
+ c.grab_focus()
+ self.movetimeout = None
+ self.selecting = False
+ if self.selection:
+ self.selection = None
+ self.need_redraw = True
+ self.name_buttons()
+ self.redraw()
+
+ self.line = [ self.colourname, [int(ev.x), int(ev.y)] ]
+ return
+ def release(self, c, ev):
+ if self.movetimeout:
+ gobject.source_remove(self.movetimeout)
+ self.movetimeout = None
+
+ if self.movetext:
+ self.movetext = None
+ self.line = None
+ self.need_redraw = True
+ self.redraw()
+ return
+ if self.line == None:
+ return
+
+ if self.selecting:
+ self.selecting = False
+ self.line = None
+ return
+ if self.timeout == None:
+ self.timeout = gobject.timeout_add(20*1000, self.tick)
+
+ if len(self.line) == 2:
+ # just set a cursor
+ need_redraw = ( self.textstr != None)
+ oldpos = None
+ if not self.textstr:
+ oldpos = self.textpos
+ self.flush_text()
+ if need_redraw:
+ self.redraw()
+ (lineno,index) = self.find_text(self.line[1])
+
+ if lineno == None:
+ # new text,
+ pos = self.align_text(self.line[1])
+ if oldpos and abs(pos[0]-oldpos[0]) < 40 and \
+ abs(pos[1] - oldpos[1]) < 40:
+ # turn of text mode
+ self.flush_text()
+ self.line = None
+ self.need_redraw = True
+ self.setlabel()
+ return
+ self.textpos = pos
+ self.textstr = ""
+ self.textcurs = 0
+ # draw the cursor
+ self.draw_text(pos, self.colour, "", 0)
+ self.line = None
+ else:
+ # clicked inside an old text.
+ # shuffle it to the top, open it, edit.
+ ln = self.lines[lineno]
+ self.lines = self.lines[:lineno] + self.lines[lineno+1:]
+ self.textpos = ln[1]
+ self.textstr = ln[2]
+ if ln[0] in self.colourmap:
+ self.colourname = ln[0]
+ else:
+ self.colourname = "black"
+ self.colbtn.child.set_text(self.colourname)
+ self.colour = self.colourmap[self.colourname]
+ self.textcurs = index + 1
+ self.need_redraw = True
+ self.redraw()
+ self.setlabel()
+ self.line = None
+ return
+ if self.textstr != None:
+ sym = self.getsym()
+ if sym:
+ self.add_sym(sym)
+ else:
+ self.redraw()
+ self.line = None
+ self.reset_colour = True
+ return
+
+ self.lines.append(self.line)
+ self.line = None
+ self.reset_colour = True
+ self.need_redraw = True
+ return
+ def motion(self, c, ev):
+ if self.line:
+ if ev.is_hint:
+ x, y, state = ev.window.get_pointer()
+ else:
+ x = ev.x
+ y = ev.y
+ x = int(x)
+ y = int(y)
+ prev = self.line[-1]
+ if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+ return
+ if not self.movetext and len(self.line) == 2 and time.time() - self.taptime > 0.5:
+ self.flush_text()
+ (lineno, index) = self.find_text(prev)
+ if lineno != None:
+ self.movetext = self.lines[lineno]
+ self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]]
+ if self.movetext:
+ self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]]
+ self.need_redraw = True
+ self.redraw()
+ return
+
+ if self.movetimeout:
+ gobject.source_remove(self.movetimeout)
+ self.movetimeout = gobject.timeout_add(650, self.movetick)
+
+ if self.textstr != None:
+ c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y)
+ else:
+ c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+ self.line.append([x,y])
+ return
+
+ def movetick(self):
+ # longish pause while drawing
+ self.flush_text()
+ self.selecting = True
+ self.need_redraw = True
+ self.redraw()
+
+ def tick(self):
+ # nothing for 20 seconds, flush the page
+ self.save_page()
+ gobject.source_remove(self.timeout)
+ self.timeout = None
+
+ def find_text(self, pos):
+ x = pos[0]; y = pos[1] + self.lineascent
+ for i in range(0, len(self.lines)):
+ p = self.lines[i]
+ if type(p[2]) != str:
+ continue
+ if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight:
+ # could be this line - check more precisely
+ layout = self.page.create_pango_layout(p[2]+' ')
+ (ink, log) = layout.get_pixel_extents()
+ (ex,ey,ew,eh) = log
+ if x < p[1][0] + ex or x > p[1][0] + ex + ew or \
+ y < p[1][1] + ey or \
+ y > p[1][1] + ey + self.lineheight :
+ continue
+ # OK, it is in this one. Find out where.
+ (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE,
+ (y - p[1][1] - ey - self.lineheight) * pango.SCALE)
+ if index >= len(p[2]):
+ index = len(p[2])-1
+ return (i, index)
+ return (None, None)
+
+ def align_text(self, pos):
+ # align pos to existing text.
+ # if pos is near one-line-past a previous text, move the exactly
+ # one-line-past
+ x = pos[0]; y = pos[1] + self.lineascent
+ for l in self.lines:
+ if type(l[2]) != str:
+ continue
+ if abs(x - l[1][0]) > self.lineheight:
+ continue
+ if abs(y - (l[1][1] + self.lineheight)) > self.lineheight:
+ continue
+ return [ l[1][0], l[1][1] + self.lineheight ]
+ return pos
+
+ def flush_text(self):
+ if self.textstr == None:
+ self.textpos = None
+ return
+ self.setlabel()
+ if len(self.textstr) == 0:
+ self.textstr = None
+ self.textpos = None
+ return
+ l = [self.colourname, self.textpos, self.textstr]
+ self.lines.append(l)
+ self.need_redraw = True
+ self.textstr = None
+ self.textpos = None
+
+ def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False):
+ if drawable == None:
+ drawable = self.page.window
+ layout = self.page.create_pango_layout(str)
+ if self.zoomx != None:
+ xs = 2; xo = -self.zoomx
+ ys = 2; yo = -self.zoomy
+ else:
+ xs = 1 ; xo = 0
+ ys = 1 ; yo = 0
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if len(pos) == 2 or pos[2] != ew or pos[3] != eh:
+ while len(pos) > 2:
+ pos.pop()
+ pos.append(ew)
+ pos.append(eh)
+ if selected:
+ drawable.draw_rectangle(self.colourmap['yellow'], True,
+ ex+pos[0], ey+pos[1]-self.lineascent, ew, eh)
+ drawable.draw_layout(colour, pos[0]*xs+xo,
+ (pos[1] - self.lineascent)*ys + yo,
+ layout)
+ if cursor != None:
+ (strong,weak) = layout.get_cursor_pos(cursor)
+ (x,y,width,height) = strong
+ x = pos[0] + x / pango.SCALE
+ y = pos[1]
+ drawable.draw_line(self.colour_textmode,
+ x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo)
+ drawable.draw_line(self.colour_textmode,
+ x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo)
+
+ def add_sym(self, sym):
+ if self.textstr == None:
+ return
+ if sym == "<BS>":
+ if self.textcurs > 0:
+ self.textstr = self.textstr[0:self.textcurs-1]+ \
+ self.textstr[self.textcurs:]
+ self.textcurs -= 1
+ elif sym == "<left>":
+ if self.textcurs > 0:
+ self.textcurs -= 1
+ elif sym == "<right>":
+ if self.textcurs < len(self.textstr):
+ self.textcurs += 1
+ elif sym == "<newline>":
+ tail = self.textstr[self.textcurs:]
+ self.textstr = self.textstr[:self.textcurs]
+ oldpos = self.textpos
+ self.flush_text()
+ self.textcurs = len(tail)
+ self.textstr = tail
+ self.textpos = [ oldpos[0], oldpos[1] +
+ self.lineheight ]
+ else:
+ self.textstr = self.textstr[0:self.textcurs] + sym + \
+ self.textstr[self.textcurs:]
+ self.textcurs += 1
+ self.redraw()
+
+
+ def getsym(self):
+ alloc = self.page.get_allocation()
+ pagebb = BBox(Point(0,0))
+ pagebb.add(Point(alloc.width, alloc.height))
+ pagebb.finish(div = 2)
+
+ p = PPath(self.line[1][0], self.line[1][1])
+ for pp in self.line[1:]:
+ p.add(pp[0], pp[1])
+ p.close()
+ patn = p.text()
+ pos = pagebb.relpos(p.bbox)
+ tpos = "mid"
+ if pos < 3:
+ tpos = "top"
+ if pos >= 6:
+ tpos = "bot"
+ sym = self.dict.match(patn, tpos)
+ if sym == None:
+ print "Failed to match pattern:", patn
+ return sym
+
+ def refresh(self, area, ev):
+ self.redraw()
+
+ def setlabel(self):
+ if self.textstr == None:
+ self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)')
+ else:
+ self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)')
+
+ def name_buttons(self):
+ if self.selection:
+ self.col_align.child.set_text('Align')
+ self.undo_join.child.set_text('Join')
+ self.redo_rename.child.set_text('Rename')
+ else:
+ self.col_align.child.set_text(self.colourname)
+ self.undo_join.child.set_text('Undo')
+ self.redo_rename.child.set_text('Redo')
+
+ def redraw(self):
+ self.setlabel()
+
+ if self.need_redraw:
+ self.draw_buf()
+ self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+ self.width, self.height)
+
+ if self.textstr != None:
+ self.draw_text(self.textpos, self.colour, self.textstr,
+ self.textcurs)
+
+ return
+ def draw_buf(self):
+ if self.pixbuf == None:
+ alloc = self.page.get_allocation()
+ self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height)
+ self.width = alloc.width
+ self.height = alloc.height
+
+ self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+ self.width, self.height)
+ if self.zoomx != None:
+ xs = 2; xo = -self.zoomx
+ ys = 2; yo = -self.zoomy
+ else:
+ xs = 1 ; xo = 0
+ ys = 1 ; yo = 0
+ self.selection = []
+ for l in self.lines:
+ if l[0] in self.colourmap:
+ col = self.colourmap[l[0]]
+ else:
+ col = self.colourmap['black']
+ st = l[1]
+ if type(l[2]) == list:
+ for p in l[2:]:
+ self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo,
+ p[0]*xs + xo,p[1]* ys + yo)
+ st = p
+ if type(l[2]) == str:
+ # if this text is 'near' the current line, make it red
+ selected = False
+ if self.selecting and self.line and len(st) == 4:
+ for p in self.line[1:]:
+ if p[0] > st[0] and \
+ p[0] < st[0] + st[2] and \
+ p[1] > st[1] - self.lineascent and \
+ p[1] < st[1] - self.lineascent + st[3]:
+ selected = True
+ break
+ if selected:
+ self.selection.append(l)
+ self.draw_text(st, col, l[2], drawable = self.pixbuf,
+ selected = selected)
+ self.need_redraw = False
+ self.name_buttons()
+
+
+ def reconfigure(self, w, ev):
+ alloc = w.get_allocation()
+ if self.pixbuf == None:
+ return
+ if alloc.width != self.width or alloc.height != self.height:
+ self.pixbuf = None
+ self.need_redraw = True
+
+
+ def colour_change(self,t):
+ if self.selection:
+ # button is 'join' not 'colour'
+ return self.realign(t)
+
+ if self.reset_colour and self.colourname != 'black':
+ next = 'black'
+ else:
+ next = 'black'
+ prev = ''
+ for c in self.colnames:
+ if self.colourname == prev:
+ next = c
+ prev = c
+ self.reset_colour = False
+ self.colourname = next
+ self.colour = self.colourmap[next]
+ t.child.set_text(next)
+ if self.textstr:
+ self.draw_text(self.textpos, self.colour, self.textstr,
+ self.textcurs)
+
+ return
+ def text_change(self,t):
+ self.flush_text()
+ return
+ def undo(self,b):
+ if self.selection:
+ return self.join(b)
+
+ if len(self.lines) == 0:
+ return
+ self.hist.append(self.lines.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+ def redo(self,b):
+ if self.selection:
+ self.rename(self.selection[0][2])
+ return
+ if len(self.hist) == 0:
+ return
+ self.lines.append(self.hist.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+ def choose_unique(self, newname):
+ while newname in self.names:
+ new2 = inc_name(newname)
+ if new2 not in self.names:
+ newname = new2
+ elif (newname + ".1") not in self.names:
+ newname = newname + ".1"
+ else:
+ newname = newname + ".0.1"
+
+ return newname
+ def delete(self,b):
+ # hack
+ if self.selection:
+ return self.join(b)
+ self.flush_text()
+ if len(self.names) <= 1:
+ return
+ if len(self.lines) > 0:
+ return
+ self.save_page()
+ nm = self.names[self.pagenum]
+ if nm in self.pages:
+ del self.pages[nm]
+ self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:]
+ if self.pagenum >= len(self.names):
+ self.pagenum -= 1
+ self.load_page()
+ self.need_redraw = True
+ self.redraw()
+
+ return
+
+ def cmplines(self, a,b):
+ pa = a[1]
+ pb = b[1]
+ if pa[1] != pb[1]:
+ return pa[1] - pb[1]
+ return pa[0] - pb[0]
+
+ def realign(self, b):
+ self.selection.sort(self.cmplines)
+ x = self.selection[0][1][0]
+ y = self.selection[0][1][1]
+ for i in range(len(self.selection)):
+ self.selection[i][1][0] = x
+ self.selection[i][1][1] = y
+ y += self.lineheight
+ self.need_redraw = True
+ self.redraw()
+
+ def join(self, b):
+ self.selection.sort(self.cmplines)
+ txt = ""
+ for i in range(len(self.selection)):
+ if txt:
+ txt = txt + ' ' + self.selection[i][2]
+ else:
+ txt = self.selection[i][2]
+ self.selection[i][2] = None
+ self.selection[0][2] = txt
+ i = 0;
+ while i < len(self.lines):
+ if len(self.lines[i]) > 2 and self.lines[i][2] == None:
+ self.lines = self.lines[:i] + self.lines[i+1:]
+ else:
+ i += 1
+ self.need_redraw = True
+ self.redraw()
+
+
+ def rename(self, newname):
+ # Rename current page and rename the file
+ if self.names[self.pagenum] == newname:
+ return
+ self.save_page()
+ newname = self.choose_unique(newname)
+ oldpath = self.page_dir + "/" + self.names[self.pagenum]
+ newpath = self.page_dir + "/" + newname
+ try :
+ os.rename(oldpath, newpath)
+ self.names[self.pagenum] = newname
+ self.names.sort(page_cmp)
+ self.pagenum = self.names.index(newname)
+ self.setlabel()
+ except:
+ pass
+ self.update_list()
+
+ def setname(self,b):
+ if self.textstr:
+ if len(self.textstr) > 0:
+ self.rename(self.textstr)
+
+ def clear(self,b):
+ while len(self.lines) > 0:
+ self.hist.append(self.lines.pop())
+ self.need_redraw = True
+ self.redraw()
+ return
+
+ def parseline(self, l):
+ # string in "", or num,num. ':' separates words
+ words = l.strip().split(':')
+ line = []
+ for w in words:
+ if w[0] == '"':
+ w = w[1:-1]
+ elif w.find(',') >= 0:
+ n = w.find(',')
+ x = int(w[:n])
+ y = int(w[n+1:])
+ w = [x,y]
+ line.append(w)
+ return line
+
+ def load_page(self):
+ self.need_redraw = True
+ nm = self.names[self.pagenum]
+ if nm in self.pages:
+ self.lines = self.pages[nm]
+ return
+ self.lines = [];
+ try:
+ f = open(self.page_dir + "/" + self.names[self.pagenum], "r")
+ except:
+ f = None
+ if f:
+ l = f.readline()
+ while len(l) > 0:
+ self.lines.append(self.parseline(l))
+ l = f.readline()
+ f.close()
+ return
+
+ def save_page(self):
+ t = self.textstr; tc = self.textcurs
+ self.flush_text()
+ self.pages[self.names[self.pagenum]] = self.lines
+ tosave = self.lines
+ if t and len(t):
+ # restore the text
+ ln = self.lines[-1]
+ self.lines = self.lines[:-1]
+ self.textpos = ln[1]
+ self.textstr = ln[2]
+ self.textcurs = tc
+
+ fn = self.page_dir + "/" + self.names[self.pagenum]
+ if len(tosave) == 0:
+ try:
+ os.unlink(fn)
+ except:
+ pass
+ return
+ f = open(fn, "w")
+ for l in tosave:
+ start = True
+ if not l:
+ continue
+ for w in l:
+ if not start:
+ f.write(":")
+ start = False
+ if isinstance(w, str):
+ f.write('"%s"' % w)
+ elif isinstance(w, list):
+ f.write("%d,%d" %( w[0],w[1]))
+ f.write("\n")
+ f.close()
+
+def main():
+ gtk.main()
+ return 0
+if __name__ == "__main__":
+ ScribblePad()
+ main()
--- /dev/null
+#!/usr/bin/env python
+
+#
+# TO FIX
+# - document
+# - use separate hand-writing code
+# - use separate list-select code
+
+
+import sys, os, time
+import pygtk, gtk, pango
+import gobject
+
+###########################################################
+# Writing recognistion code
+import math
+
+global place
+place = 0
+
+def LoadDict(dict):
+ # Upper case.
+ # Where they are like lowercase, we either double
+ # the last stroke (L, J, I) or draw backwards (S, Z, X)
+ # U V are a special case
+
+ dict.add('A', "R(4)6,8")
+ dict.add('B', "R(4)6,4.R(7)1,6")
+ dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+ dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+ dict.add('C', "R(4)8,2")
+ dict.add('D', "R(4)6,6")
+ dict.add('E', "L(1)2,8.L(7)2,8")
+ # double the stem for F
+ dict.add('F', "L(4)2,6.S(3)7,1")
+ dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+ dict.add('G', "L(4)2,5.S(8)1,7")
+ dict.add('G', "L(4)2,5.R(8)6,8")
+ # FIXME I need better straight-curve alignment
+ dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+ dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+ # capital I is down/up
+ dict.add('I', "S(4)1,7.S(4)7,1")
+
+ # Capital J has a left/right tail
+ dict.add('J', "R(4)1,6.S(7)3,5")
+
+ dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+ # Capital L, like J, doubles the foot
+ dict.add('L', "L(4)0,8.S(7)4,3")
+
+ dict.add('M', "R(3)6,5.R(5)3,8")
+ dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+ dict.add('N', "R(3)6,8.L(5)0,2")
+
+ # Capital O is CW, but can be CCW in special dict
+ dict.add('O', "R(4)1,1", bot='0')
+
+ dict.add('P', "R(4)6,3")
+ dict.add('Q', "R(4)7,7.S(8)0,8")
+
+ dict.add('R', "R(4)6,4.S(8)0,8")
+
+ # S is drawn bottom to top.
+ dict.add('S', "L(7)6,1.R(1)7,2")
+
+ # Double the stem for capital T
+ dict.add('T', "R(4)0,8.S(5)7,1")
+
+ # U is L to R, V is R to L for now
+ dict.add('U', "L(4)0,2")
+ dict.add('V', "R(4)2,0")
+
+ dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+ dict.add('W', "R(5)2,3.R(3)5,0")
+
+ dict.add('X', "R(4)6,0")
+
+ dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+ dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+ dict.add('Z', "R(4)8,2.L(4)6,0")
+
+ # Lower case
+ dict.add('a', "L(4)2,2.L(5)1,7")
+ dict.add('a', "L(4)2,2.L(5)0,8")
+ dict.add('a', "L(4)2,2.S(5)0,8")
+ dict.add('b', "S(3)1,7.R(7)6,3")
+ dict.add('c', "L(4)2,8", top='C')
+ dict.add('d', "L(4)5,2.S(5)1,7")
+ dict.add('d', "L(4)5,2.L(5)0,8")
+ dict.add('e', "S(4)3,5.L(4)5,8")
+ dict.add('e', "L(4)3,8")
+ dict.add('f', "L(4)2,6", top='F')
+ dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+ dict.add('g', "L(1)2,2.R(4)1,6")
+ dict.add('h', "S(3)1,7.R(7)6,8")
+ dict.add('h', "L(3)0,5.R(7)6,8")
+ dict.add('i', "S(4)1,7", top='I', bot='1')
+ dict.add('j', "R(4)1,6", top='J')
+ dict.add('k', "L(3)0,5.L(7)2,8")
+ dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+ dict.add('l', "L(4)0,8", top='L')
+ dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+ dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+ dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+ dict.add('n', "S(3)1,7.R(4)6,8")
+ dict.add('o', "L(4)1,1", top='O', bot='0')
+ dict.add('p', "S(3)1,7.R(4)6,3")
+ dict.add('q', "L(1)2,2.L(5)1,5")
+ dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+ dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+ # FIXME this double 1,7 is due to a gentle where the
+ # second looks like a line because it is narrow.??
+ dict.add('r', "S(3)1,7.R(4)6,2")
+ dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+ dict.add('t', "R(4)0,8", top='T', bot='7')
+ dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+ dict.add('u', "L(4)0,2.S(5)1,7")
+ dict.add('v', "L(4)0,2.L(2)0,2")
+ dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+ dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+ dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+ dict.add('x', "L(4)0,6", top='X')
+ dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+ dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+ dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+ # Digits
+ dict.add('0', "L(4)7,7")
+ dict.add('0', "R(4)7,7")
+ dict.add('1', "S(4)7,1")
+ dict.add('2', "R(4)0,6.S(7)3,5")
+ dict.add('2', "R(4)3,6.L(4)2,8")
+ dict.add('3', "R(1)0,6.R(7)1,6")
+ dict.add('4', "L(4)7,5")
+ dict.add('5', "L(1)2,6.R(7)0,3")
+ dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+ dict.add('6', "L(4)2,3")
+ dict.add('7', "S(1)3,5.R(4)1,6")
+ dict.add('7', "R(4)0,6")
+ dict.add('7', "R(4)0,7")
+ dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+ dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+ dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+ dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+ dict.add('9', "L(1)2,2.S(5)1,7")
+
+ dict.add(' ', "S(4)3,5")
+ dict.add('<BS>', "S(4)5,3")
+ dict.add('-', "S(4)3,5.S(4)5,3")
+ dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+ dict.add("<left>", "S(4)5,3.S(3)3,5")
+ dict.add("<right>","S(4)3,5.S(5)5,3")
+ dict.add("<left>", "S(4)7,1.S(1)1,7") # "<up>"
+ dict.add("<right>","S(4)1,7.S(7)7,1") # "<down>"
+ dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+ # Each segment has for elements:
+ # direction: Right Straight Left (R=cw, L=ccw)
+ # location: 0-8.
+ # start: 0-8
+ # finish: 0-8
+ # Segments match if there difference at each element
+ # is 0, 1, or 3 (RSL coded as 012)
+ # A difference of 1 required both to be same / 3
+ # On a match, return number of 0s
+ # On non-match, return -1
+ def __init__(self, str):
+ # D(L)S,R
+ # 0123456
+ self.e = [0,0,0,0]
+ if len(str) != 7:
+ raise ValueError
+ if str[1] != '(' or str[3] != ')' or str[5] != ',':
+ raise ValueError
+ if str[0] == 'R':
+ self.e[0] = 0
+ elif str[0] == 'L':
+ self.e[0] = 2
+ elif str[0] == 'S':
+ self.e[0] = 1
+ else:
+ raise ValueError
+
+ self.e[1] = int(str[2])
+ self.e[2] = int(str[4])
+ self.e[3] = int(str[6])
+
+ def match(self, other):
+ cnt = 0
+ for i in range(0,4):
+ diff = abs(self.e[i] - other.e[i])
+ if diff == 0:
+ cnt += 1
+ elif diff == 3:
+ pass
+ elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+ pass
+ else:
+ return -1
+ return cnt
+
+class DictPattern:
+ # A Dict Pattern is a list of segments.
+ # A parsed pattern matches a dict pattern if
+ # the are the same nubmer of segments and they
+ # all match. The value of the match is the sum
+ # of the individual matches.
+ # A DictPattern is printers as segments joined by periods.
+ #
+ def __init__(self, str):
+ self.segs = map(DictSegment, str.split("."))
+ def match(self,other):
+ if len(self.segs) != len(other.segs):
+ return -1
+ cnt = 0
+ for i in range(0,len(self.segs)):
+ m = self.segs[i].match(other.segs[i])
+ if m < 0:
+ return m
+ cnt += m
+ return cnt
+
+
+class Dictionary:
+ # The dictionary hold all the pattern for symbols and
+ # performs lookup
+ # Each pattern in the directionary can be associated
+ # with 3 symbols. One when drawing in middle of screen,
+ # one for top of screen, one for bottom.
+ # Often these will all be the same.
+ # This allows e.g. s and S to have the same pattern in different
+ # location on the touchscreen.
+ # A match requires a unique entry with a match that is better
+ # than any other entry.
+ #
+ def __init__(self):
+ self.dict = []
+ def add(self, sym, pat, top = None, bot = None):
+ if top == None: top = sym
+ if bot == None: bot = sym
+ self.dict.append((DictPattern(pat), sym, top, bot))
+
+ def _match(self, p):
+ max = -1
+ val = None
+ for (ptn, sym, top, bot) in self.dict:
+ cnt = ptn.match(p)
+ if cnt > max:
+ max = cnt
+ val = (sym, top, bot)
+ elif cnt == max:
+ val = None
+ return val
+
+ def match(self, str, pos = "mid"):
+ p = DictPattern(str)
+ m = self._match(p)
+ if m == None:
+ return m
+ (mid, top, bot) = self._match(p)
+ if pos == "top": return top
+ if pos == "bot": return bot
+ return mid
+
+
+class Point:
+ # This represents a point in the path and all the points leading
+ # up to it. It allows us to find the direction and curvature from
+ # one point to another
+ # We store x,y, and sum/cnt of points so far
+ def __init__(self,x,y) :
+ self.xsum = x
+ self.ysum = y
+ self.x = x
+ self.y = y
+ self.cnt = 1
+
+ def copy(self):
+ n = Point(0,0)
+ n.xsum = self.xsum
+ n.ysum = self.ysum
+ n.x = self.x
+ n.y = self.y
+ n.cnt = self.cnt
+ return n
+
+ def add(self,x,y):
+ if self.x == x and self.y == y:
+ return
+ self.x = x
+ self.y = y
+ self.xsum += x
+ self.ysum += y
+ self.cnt += 1
+
+ def xlen(self,p):
+ return abs(self.x - p.x)
+ def ylen(self,p):
+ return abs(self.y - p.y)
+ def sqlen(self,p):
+ x = self.x - p.x
+ y = self.y - p.y
+ return x*x + y*y
+
+ def xdir(self,p):
+ if self.x > p.x:
+ return 1
+ if self.x < p.x:
+ return -1
+ return 0
+ def ydir(self,p):
+ if self.y > p.y:
+ return 1
+ if self.y < p.y:
+ return -1
+ return 0
+ def curve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ if curve > 6:
+ return 1
+ if curve < -6:
+ return -1
+ return 0
+
+ def Vcurve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ return curve
+
+ def meanpoint(self,p):
+ x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+ y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+ return (x,y)
+
+ def is_sharp(self,A,C):
+ # Measure the cosine at self between A and C
+ # as A and C could be curve, we take the mean point on
+ # self.A and self.C as the points to find cosine between
+ (ax,ay) = self.meanpoint(A)
+ (cx,cy) = self.meanpoint(C)
+ a = ax-self.x; b=ay-self.y
+ c = cx-self.x; d=cy-self.y
+ x = a*c + b*d
+ y = a*d - b*c
+ h = math.sqrt(x*x+y*y)
+ if h > 0:
+ cs = x*1000/h
+ else:
+ cs = 0
+ return (cs > 900)
+
+class BBox:
+ # a BBox records min/max x/y of some Points and
+ # can subsequently report row, column, pos of each point
+ # can also locate one bbox in another
+
+ def __init__(self, p):
+ self.minx = p.x
+ self.maxx = p.x
+ self.miny = p.y
+ self.maxy = p.y
+
+ def width(self):
+ return self.maxx - self.minx
+ def height(self):
+ return self.maxy - self.miny
+
+ def add(self, p):
+ if p.x > self.maxx:
+ self.maxx = p.x
+ if p.x < self.minx:
+ self.minx = p.x
+
+ if p.y > self.maxy:
+ self.maxy = p.y
+ if p.y < self.miny:
+ self.miny = p.y
+ def finish(self, div = 3):
+ # if aspect ratio is bad, we adjust max/min accordingly
+ # before setting [xy][12]. We don't change self.min/max
+ # as they are used to place stroke in bigger bbox.
+ # Normally divisions are at 1/3 and 2/3. They can be moved
+ # by setting div e.g. 2 = 1/2 and 1/2
+ (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+ if (maxx - minx) * 3 < (maxy - miny) * 2:
+ # too narrow
+ mid = int((maxx + minx)/2)
+ halfwidth = int ((maxy - miny)/3)
+ minx = mid - halfwidth
+ maxx = mid + halfwidth
+ if (maxy - miny) * 3 < (maxx - minx) * 2:
+ # too wide
+ mid = int((maxy + miny)/2)
+ halfheight = int ((maxx - minx)/3)
+ miny = mid - halfheight
+ maxy = mid + halfheight
+
+ div1 = div - 1
+ self.x1 = int((div1*minx + maxx)/div)
+ self.x2 = int((minx + div1*maxx)/div)
+ self.y1 = int((div1*miny + maxy)/div)
+ self.y2 = int((miny + div1*maxy)/div)
+
+ def row(self, p):
+ # 0, 1, 2 - top to bottom
+ if p.y <= self.y1:
+ return 0
+ if p.y < self.y2:
+ return 1
+ return 2
+ def col(self, p):
+ if p.x <= self.x1:
+ return 0
+ if p.x < self.x2:
+ return 1
+ return 2
+ def box(self, p):
+ # 0 to 9
+ return self.row(p) * 3 + self.col(p)
+
+ def relpos(self,b):
+ # b is a box within self. find location 0-8
+ if b.maxx < self.x2 and b.minx < self.x1:
+ x = 0
+ elif b.minx > self.x1 and b.maxx > self.x2:
+ x = 2
+ else:
+ x = 1
+ if b.maxy < self.y2 and b.miny < self.y1:
+ y = 0
+ elif b.miny > self.y1 and b.maxy > self.y2:
+ y = 2
+ else:
+ y = 1
+ return y*3 + x
+
+
+def different(*args):
+ cur = 0
+ for i in args:
+ if cur != 0 and i != 0 and cur != i:
+ return True
+ if cur == 0:
+ cur = i
+ return False
+
+def maxcurve(*args):
+ for i in args:
+ if i != 0:
+ return i
+ return 0
+
+class PPath:
+ # a PPath refines a list of x,y points into a list of Points
+ # The Points mark out segments which end at significant Points
+ # such as inflections and reversals.
+
+ def __init__(self, x,y):
+
+ self.start = Point(x,y)
+ self.mid = Point(x,y)
+ self.curr = Point(x,y)
+ self.list = [ self.start ]
+
+ def add(self, x, y):
+ self.curr.add(x,y)
+
+ if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+ (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+ (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+ pass
+ else:
+ self.mid = self.curr.copy()
+
+ if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+ self.start = self.mid.copy()
+ self.list.append(self.start)
+ self.mid = self.curr.copy()
+
+ def close(self):
+ self.list.append(self.curr)
+
+ def get_sectlist(self):
+ if len(self.list) <= 2:
+ return [[0,self.list]]
+ l = []
+ A = self.list[0]
+ B = self.list[1]
+ s = [A,B]
+ curcurve = B.curve(A)
+ for C in self.list[2:]:
+ cabc = C.curve(A)
+ cab = B.curve(A)
+ cbc = C.curve(B)
+ if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+ # B is too pointy, must break here
+ l.append([curcurve, s])
+ s = [B, C]
+ curcurve = cbc
+ elif not different(cabc, cab, cbc, curcurve):
+ # all happy
+ s.append(C)
+ if curcurve == 0:
+ curcurve = maxcurve(cab, cbc, cabc)
+ elif not different(cabc, cab, cbc) :
+ # gentle inflection along AB
+ # was: AB goes in old and new section
+ # now: AB only in old section, but curcurve
+ # preseved.
+ l.append([curcurve,s])
+ s = [A, B, C]
+ curcurve =maxcurve(cab, cbc, cabc)
+ else:
+ # Change of direction at B
+ l.append([curcurve,s])
+ s = [B, C]
+ curcurve = cbc
+
+ A = B
+ B = C
+ l.append([curcurve,s])
+
+ return l
+
+ def remove_shorts(self, bbox):
+ # in self.list, if a point is close to the previous point,
+ # remove it.
+ if len(self.list) <= 2:
+ return
+ w = bbox.width()/10
+ h = bbox.height()/10
+ n = [self.list[0]]
+ leng = w*h*2*2
+ for p in self.list[1:]:
+ l = p.sqlen(n[-1])
+ if l > leng:
+ n.append(p)
+ self.list = n
+
+ def text(self):
+ # OK, we have a list of points with curvature between.
+ # want to divide this into sections.
+ # for each 3 consectutive points ABC curve of ABC and AB and BC
+ # If all the same, they are all in a section.
+ # If not B starts a new section and the old ends on B or C...
+ BB = BBox(self.list[0])
+ for p in self.list:
+ BB.add(p)
+ BB.finish()
+ self.bbox = BB
+ self.remove_shorts(BB)
+ sectlist = self.get_sectlist()
+ t = ""
+ for c, s in sectlist:
+ if c > 0:
+ dr = "R" # clockwise is to the Right
+ elif c < 0:
+ dr = "L" # counterclockwise to the Left
+ else:
+ dr = "S" # straight
+ bb = BBox(s[0])
+ for p in s:
+ bb.add(p)
+ bb.finish()
+ # If all points are in some row or column, then
+ # line is S
+ rwdiff = False; cldiff = False
+ rw = bb.row(s[0]); cl=bb.col(s[0])
+ for p in s:
+ if bb.row(p) != rw: rwdiff = True
+ if bb.col(p) != cl: cldiff = True
+ if not rwdiff or not cldiff: dr = "S"
+
+ t1 = dr
+ t1 += "(%d)" % BB.relpos(bb)
+ t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+ t += t1 + '.'
+ return t[:-1]
+
+
+
+
+
+class text_input:
+ def __init__(self, page, callout):
+
+ self.page = page
+ self.callout = callout
+ self.colour = None
+ self.line = None
+ self.dict = Dictionary()
+ LoadDict(self.dict)
+
+ page.connect("button_press_event", self.press)
+ page.connect("button_release_event", self.release)
+ page.connect("motion_notify_event", self.motion)
+ page.set_events(page.get_events()
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+ def set_colour(self, col):
+ self.colour = col
+
+ def press(self, c, ev):
+ # Start a new line
+ self.line = [ [int(ev.x), int(ev.y)] ]
+ return
+ def release(self, c, ev):
+ if self.line == None:
+ return
+ if len(self.line) == 1:
+ self.callout('click', ev)
+ self.line = None
+ return
+
+ sym = self.getsym()
+ if sym:
+ self.callout('sym', sym)
+ self.callout('redraw', None)
+ self.line = None
+ return
+
+ def motion(self, c, ev):
+ if self.line:
+ if ev.is_hint:
+ x, y, state = ev.window.get_pointer()
+ else:
+ x = ev.x
+ y = ev.y
+ x = int(x)
+ y = int(y)
+ prev = self.line[-1]
+ if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+ return
+ if self.colour:
+ c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+ self.line.append([x,y])
+ return
+
+ def getsym(self):
+ alloc = self.page.get_allocation()
+ pagebb = BBox(Point(0,0))
+ pagebb.add(Point(alloc.width, alloc.height))
+ pagebb.finish(div = 2)
+
+ p = PPath(self.line[1][0], self.line[1][1])
+ for pp in self.line[1:]:
+ p.add(pp[0], pp[1])
+ p.close()
+ patn = p.text()
+ pos = pagebb.relpos(p.bbox)
+ tpos = "mid"
+ if pos < 3:
+ tpos = "top"
+ if pos >= 6:
+ tpos = "bot"
+ sym = self.dict.match(patn, tpos)
+ if sym == None:
+ print "Failed to match pattern:", patn
+ return sym
+
+
+
+
+
+########################################################################
+
+
+def extend_array(ra, leng, val=None):
+ while len(ra) <= leng:
+ ra.append(val)
+
+
+class Prod:
+ # A product that might be purchased
+ # These are stored in a list index by product number
+ def __init__(self, num, line):
+ # line is read from file, or string typed in for new
+ # product in which case it contains no comma.
+ # otherwise "Name,[R|I]{,Ln:m}"
+ self.num = num
+ words = line.split(',')
+ self.name = words[0]
+ self.regular = (len(words) > 1 and words[1] == 'R')
+ self.loc = []
+ for loc in words[2:]:
+ if len(loc) == 0:
+ continue
+ n = loc[1:].split(':')
+ pl = int(n[0])
+ lc = int(n[1])
+ extend_array(self.loc, pl, -1)
+ self.loc[pl] = lc
+
+ def format(self,f):
+ str = "I%d," % self.num
+ str += self.name + ','
+ if self.regular:
+ str += 'R'
+ else:
+ str += 'I'
+ for i in range(len(self.loc)):
+ if self.loc[i] >= 0:
+ str += ",L%d:%d"%(i, self.loc[i])
+ str += '\n'
+ f.write(str)
+
+
+class Purch:
+ # A purchase that could be made
+ # A list of these is the current shopping list.
+ def __init__(self,source):
+ # source is a string read from a file, or
+ # a product being added to the list.
+ if source.__class__ == Prod:
+ self.prod = source.num
+ self.state = 'X'
+ self.comment = ""
+ elif source.__class__ == str:
+ l = source.split(',', 2)
+ self.prod = int(l[0])
+ self.state = l[1]
+ self.comment = l[2]
+ else:
+ raise ValueError
+
+ def format(self, f):
+ str = '%d,%s,%s\n' % (self.prod, self.state, self.comment)
+ f.write(str)
+
+ def loc(self):
+ global place
+ p = products[self.prod]
+ if len(p.loc) <= place:
+ return -1
+ if p.loc[place] == None:
+ return -1
+ return p.loc[place]
+
+ def locord(self):
+ global place
+ p = products[self.prod]
+ if len(p.loc) <= place:
+ return -1
+ if p.loc[place] == -1 or p.loc[place] == None:
+ return -1
+ return locorder[place].index(p.loc[place])
+
+def purch_cmp(a,b):
+ pa = products[a.prod]
+ pb = products[b.prod]
+ la = a.locord()
+ lb = b.locord()
+
+ if la < lb:
+ return -1
+ if la > lb:
+ return 1
+ # same location
+ return cmp(pa.name, pb.name)
+
+
+def parse_places(l):
+ # P,n:name,...
+ w = l.split(',')
+ if w[0] != 'P':
+ return
+ for p in w[1:]:
+ w2 = p.split(':',1)
+ pos = int(w2[0])
+ extend_array(places, pos,0)
+ places[pos] = w2[1]
+
+def parse_locations(l):
+ # Ln,m:loc,m2:loc2,
+ w = l.split(',')
+ if w[0][0] != 'L':
+ return
+ lnum = int(w[0][1:])
+ loc = []
+ order = []
+ for l in w[1:]:
+ w2 = l.split(':',1)
+ pos = int(w2[0])
+ extend_array(loc, pos)
+ loc[pos] = w2[1]
+ order.append(pos)
+ extend_array(locations, lnum)
+ extend_array(locorder, lnum)
+ locations[lnum] = loc
+ locorder[lnum] = order
+
+def parse_item(l):
+ # In,rest
+ w = l.split(',',1)
+ if w[0][0] != 'I':
+ return
+ lnum = int(w[0][1:])
+ itm = Prod(lnum, w[1])
+ extend_array(products, lnum)
+ products[lnum] = itm
+
+def load_table(f):
+ # read P L and I lines
+ l = f.readline()
+ while len(l) > 0:
+ l = l.strip()
+ if l[0] == 'P':
+ parse_places(l)
+ elif l[0] == 'L':
+ parse_locations(l)
+ elif l[0] == 'I':
+ parse_item(l)
+ l = f.readline()
+
+def save_table(name):
+ try:
+ f = open(name+".new", "w")
+ except:
+ return
+ f.write("P")
+ for i in range(len(places)):
+ f.write(",%d:%s" % (i, places[i]))
+ f.write("\n")
+
+ for i in range(len(places)):
+ f.write("L%d" % i)
+ for j in locorder[i]:
+ f.write(",%d:%s" % (j, locations[i][j]))
+ f.write("\n")
+ for p in products:
+ if p:
+ p.format(f)
+ f.close()
+ os.rename(name+".new", name)
+
+table_timeout = None
+def table_changed():
+ global table_timeout
+ if table_timeout:
+ gobject.source_remove(table_timeout)
+ table_timeout = None
+ table_timeout = gobject.timeout_add(15*1000, table_tick)
+
+def table_tick():
+ global table_timeout
+ if table_timeout:
+ gobject.source_remove(table_timeout)
+ table_timeout = None
+ save_table("Products")
+
+def load_list(f):
+ # Read item,state,comment from file to 'purch' list
+ l = f.readline()
+ while len(l) > 0:
+ l = l.strip()
+ purch.append(Purch(l))
+ l = f.readline()
+
+def save_list(name):
+ try:
+ f = open(name+".new", "w")
+ except:
+ return
+ for p in purch:
+ if p.state != 'X':
+ p.format(f)
+ f.close()
+ os.rename(name+".new", name)
+
+
+list_timeout = None
+def list_changed():
+ global list_timeout
+ if list_timeout:
+ gobject.source_remove(list_timeout)
+ list_timeout = None
+ list_timeout = gobject.timeout_add(15*1000, list_tick)
+
+def list_tick():
+ global list_timeout
+ if list_timeout:
+ gobject.source_remove(list_timeout)
+ list_timeout = None
+ save_list("Purchases")
+
+def merge_list(purch, prod):
+ # add to purch any products not already there
+ have = []
+ for p in purch:
+ extend_array(have, p.prod, False)
+ have[p.prod] = True
+ for p in prod:
+ if p and (p.num >= len(have) or not have[p.num]) :
+ purch.append(Purch(p))
+
+def locname(purch):
+ if purch.loc() < 0:
+ return "Unknown"
+ else:
+ return locations[place][purch.loc()]
+
+class PurchView:
+ # A PurchView is the view on the list of possible purchases.
+ # We draw the names in a DrawingArea
+ # When a name is tapped, we call-out to possibly update it.
+ # We get a callback when:
+ # item state changes, so we need to change colour
+ # list (or sort-order) changes so complete redraw is needed
+ # zoom changes
+ #
+
+ def __init__(self, zoom, callout, entry):
+ p = gtk.DrawingArea()
+ p.show()
+ self.widget = p
+
+ fd = p.get_pango_context().get_font_description()
+ self.fd = fd
+
+ self.callout = callout
+ self.zoom = 0
+ self.set_zoom(zoom)
+ self.pixbuf = None
+ self.width = self.height = 0
+ self.need_redraw = True
+
+ self.colours = None
+
+ self.plist = None
+ self.search = None
+ self.current = None
+ self.gonext = False
+ self.top = None
+ self.all_headers = False
+
+ p.connect("expose-event", self.redraw)
+ p.connect("configure-event", self.reconfig)
+
+ #p.connect("button_release_event", self.click)
+ p.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.STRUCTURE_MASK)
+
+ self.entry = entry
+ self.writing = text_input(p, self.stylus)
+
+ def stylus(self, cmd, info):
+ if cmd == "click":
+ self.click(None, info)
+ return
+ if cmd == "redraw":
+ self.widget.queue_draw()
+ return
+ if cmd == "sym":
+
+ if info == "<BS>":
+ self.entry.emit("backspace")
+ elif info == "<newline>":
+ self.entry.emit("activate")
+ else:
+ self.entry.emit("insert-at-cursor",info)
+ #print "Got Sym ", info
+
+ def add_col(self, sym, col):
+ c = gtk.gdk.color_parse(col)
+ gc = self.widget.window.new_gc()
+ gc.set_foreground(self.widget.get_colormap().alloc_color(c))
+ self.colours[sym] = gc
+
+ def set_zoom(self, zoom):
+ if zoom > 50:
+ zoom = 50
+ if zoom < 20:
+ zoom = 20
+ if zoom == self.zoom:
+ return
+ self.need_redraw = True
+ self.zoom = zoom
+ s = pango.SCALE
+ for i in range(zoom):
+ s = s * 11 / 10
+ self.fd.set_absolute_size(s)
+ self.widget.modify_font(self.fd)
+ met = self.widget.get_pango_context().get_metrics(self.fd)
+
+ self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+ self.lineascent = met.get_ascent() / pango.SCALE
+ self.widget.queue_draw()
+
+ def set_search(self, str):
+ self.search = str
+ self.need_redraw = True
+ self.widget.queue_draw()
+
+ def reconfig(self, w, ev):
+ alloc = w.get_allocation()
+ if not self.pixbuf:
+ return
+ if alloc.width != self.width or alloc.height != self.height:
+ self.pixbuf = None
+ self.need_redraw = True
+
+ def redraw(self, w, ev):
+ if self.colours == None:
+ self.colours = {}
+ self.add_col('N', "blue") # need
+ self.add_col('F', "darkgreen") # found
+ self.add_col('C', "red") # Cannot find
+ self.add_col('R', "orange")# Regular
+ self.add_col('X', "black") # No Need
+ self.add_col(' ', "white") # selected background
+ self.add_col('_', "black") # location separator
+ self.add_col('title', "cyan")
+ self.bg = self.widget.get_style().bg_gc[gtk.STATE_NORMAL]
+ self.writing.set_colour(self.colours['_'])
+
+
+ if self.need_redraw:
+ self.draw_buf()
+
+ self.widget.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+ self.width, self.height)
+
+ def draw_buf(self):
+ self.need_redraw = False
+ p = self.widget
+ if self.pixbuf == None:
+ alloc = p.get_allocation()
+ self.pixbuf = gtk.gdk.Pixmap(p.window, alloc.width, alloc.height)
+ self.width = alloc.width
+ self.height = alloc.height
+ self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+ self.width, self.height)
+
+ if self.plist == None:
+ # Empty list, say so.
+ layout = self.widget.create_pango_layout("List Is Empty")
+ self.cols = 1
+ (ink, log) = layout.get_pixel_extents()
+ (ex,ey,ew,eh) = log
+ self.pixbuf.draw_layout(self.colours['X'], (self.width-ew)/2,
+ (self.height-eh)/2,
+ layout)
+ return
+
+ # find max width and height
+ maxw = 1; maxh = 1; longest = "nothing"
+ curr = None; top = 0
+ visible = []
+ curloc = None
+ for p in self.plist:
+
+ if self.search == None and p.state == 'X':
+ # Don't normally show "noneed" entries
+ if p.prod == self.current and self.gonext:
+ curr = len(visible)
+ continue
+ if self.search != None and products[p.prod].name.lower().find(self.search.lower()) < 0:
+ # doesn't contain search string
+ continue
+ while p.loc() != curloc:
+ if not self.all_headers:
+ curloc = p.loc()
+ elif curloc == None:
+ curloc = -1
+ else:
+ if curloc < 0:
+ i = -1
+ else:
+ i = locorder[place].index(curloc)
+
+ if i < len(locorder[place]) - 1:
+ curloc = locorder[place][i+1]
+ else:
+ break
+ if curloc < 0:
+ locstr = "Unknown"
+ elif curloc < len(locations[place]):
+ locstr = locations[place][curloc]
+ else:
+ locstr = None
+
+ if locstr == None:
+ break
+
+ if self.top == locstr:
+ top = len(visible)
+ visible.append(locstr)
+
+ layout = self.widget.create_pango_layout(locstr)
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if ew > maxw: maxw = ew; longest = products[p.prod].name
+ if eh > maxh: maxh = eh
+
+ if p.prod == self.top:
+ top = len(visible)
+ if curr != None and self.gonext and self.gonext == p.state:
+ self.gonext = False
+ self.current = p.prod
+ self.callout(p, 'auto')
+ if p.prod == self.current:
+ curr = len(visible)
+ visible.append(p)
+ layout = self.widget.create_pango_layout(products[p.prod].name)
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if ew > maxw: maxw = ew; longest = products[p.prod].name
+ if eh > maxh: maxh = eh
+ # print "mw=%d mh=%d lh=%d la=%d" % (maxw, maxh, self.lineheight, self.lineascent)
+
+ if self.all_headers:
+ # any following headers with no items visible
+ while True:
+ if curloc == None:
+ curloc = -1
+ else:
+ if curloc < 0:
+ i = -1
+ else:
+ i = locorder[place].index(curloc)
+
+ if i < len(locorder[place]) - 1:
+ curloc = locorder[place][i+1]
+ else:
+ break
+ if curloc < 0:
+ locstr = "Unknown"
+ elif curloc < len(locations[place]):
+ locstr = locations[place][curloc]
+ else:
+ locstr = None
+
+ if locstr == None:
+ break
+
+ if self.top == locstr:
+ top = len(visible)
+ visible.append(locstr)
+
+ layout = self.widget.create_pango_layout(locstr)
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if ew > maxw: maxw = ew; longest = products[p.prod].name
+ if eh > maxh: maxh = eh
+
+ self.gonext = False
+ truemaxw = maxw
+ maxw = int(maxw * 11/10)
+ if maxh > self.lineheight:
+ self.lineheight = maxh
+ # Find max rows/columns
+ rows = int(self.height / self.lineheight)
+ cols = int(self.width / maxw)
+ if rows < 1 or cols < 1:
+ if self.zoom > 10:
+ self.set_zoom(self.zoom - 1)
+ self.need_redraw = True
+ self.widget.queue_draw()
+ return
+ #print "rows=%d cols=%s" % (rows,cols)
+ colw = int(self.width / cols)
+ offset = (colw - truemaxw)/2
+ self.offset = offset
+
+ # check 'curr' is in appropriate range and
+ # possibly adjust 'top'. Then trim visible to top.
+ # Allow one blank line at the end.
+ cells = rows * cols
+ if cells >= len(visible):
+ # display everything
+ top = 0
+ elif curr != None:
+ # make sure curr is in good range
+ if curr - top < rows/3:
+ top = curr - (cells - rows/3)
+ if top < 0:
+ top = 0
+ if (cells - (curr - top)) < rows/3:
+ top = curr - rows / 3
+ if len(visible) - top < cells-1:
+ top = len(visible) - (cells-1)
+ if top < 0:
+ top = 0
+ else:
+ if len(visible) - top < cells-1:
+ top = len(visible) - (cells-1)
+ if top < 0:
+ top = 0
+
+ visible = visible[top:]
+ self.top = None
+
+ self.visible = visible
+ self.rows = rows
+ self.cols = cols
+
+ for r in range(rows):
+ for c in range(cols):
+ pos = c*rows+r
+ uline = False
+ if pos < len(visible):
+ if type(visible[pos]) == str:
+ strng = visible[pos]
+ state = 'title'
+ comment = False
+ if self.top == None:
+ self.top = visible[pos]
+ else:
+ strng = products[visible[pos].prod].name
+ uline = products[visible[pos].prod].regular
+ state = visible[pos].state
+ if self.top == None:
+ self.top = visible[pos].prod
+ comment = (not not visible[pos].comment)
+ else:
+ break
+ layout = self.widget.create_pango_layout(strng)
+ if uline:
+ a = pango.AttrList()
+ a.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE,0,len(strng)))
+ layout.set_attributes(a)
+ if curr != None and c*rows+r == curr - top:
+ self.pixbuf.draw_rectangle(self.colours[' '], True,
+ c*colw, r*self.lineheight,
+ colw, self.lineheight)
+ if state == 'title':
+ self.pixbuf.draw_rectangle(self.colours['_'], True,
+ c*colw+2, r*self.lineheight,
+ colw-4, self.lineheight)
+ self.pixbuf.draw_rectangle(self.colours['_'], False,
+ c*colw+2, r*self.lineheight,
+ colw-4-1, self.lineheight-1)
+
+ if comment:
+ if offset > self.lineheight * 0.8:
+ w = int (self.lineheight * 0.8 * 0.9)
+ o = (offset - w) / 2
+ else:
+ w = int(offset*0.9)
+ o = int((offset-w)/2)
+ vo = int((self.lineheight-w)/2)
+ self.pixbuf.draw_rectangle(self.colours[state], True,
+ c * colw + o, r * self.lineheight + vo,
+ w, w)
+
+
+ self.pixbuf.draw_layout(self.colours[state],
+ c * colw + offset,
+ r * self.lineheight,
+ layout)
+
+
+ def click(self, w, ev):
+ cw = self.width / self.cols
+ rh = self.lineheight
+ col = int(ev.x/cw)
+ row = int(ev.y/rh)
+ cell = col * self.rows + row
+ cellpos = ev.x - col * cw
+ pos = 0
+ if cellpos < self.lineheight or cellpos < self.offset:
+ pos = -1
+ elif cellpos > cw - self.lineheight:
+ pos = 1
+
+ if cell < len(self.visible) and type(self.visible[cell]) != str:
+ if pos == 0:
+ prod = self.visible[cell].prod
+ layout = self.widget.create_pango_layout(products[prod].name)
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ if cellpos > self.offset + ew:
+ pos = 1
+ self.callout(self.visible[cell], pos)
+ if cell < len(self.visible) and type(self.visible[cell]) == str:
+ self.callout(self.visible[cell], 'heading')
+
+
+ def set_purch(self, purch):
+ if purch:
+ self.plist = purch
+ self.need_redraw = True
+ self.widget.queue_draw()
+
+ def select(self, item = None, gonext = False):
+ if gonext:
+ self.gonext = gonext
+ elif item != None:
+ self.current = item
+ self.need_redraw = True
+ self.widget.queue_draw()
+
+ def show_headers(self, all):
+ self.all_headers = all
+ self.need_redraw = True
+ self.widget.queue_draw()
+
+def swap_place(a,b):
+ places[a], places[b] = places[b], places[a]
+ locorder[a], locorder[b] = locorder[b], locorder[a]
+ locations[a], locations[b] = locations[b], locations[a]
+ for p in products:
+ if p:
+ extend_array(p.loc, a, -1)
+ extend_array(p.loc, b, -1)
+ p.loc[a], p.loc[b] = p.loc[b], p.loc[a]
+ table_changed()
+
+class ShoppingList:
+
+ def __init__(self):
+ window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ window.connect("destroy", self.close_application)
+ window.set_title("Shopping List")
+
+ self.purch = None
+ self.isize = gtk.icon_size_register("mine", 40, 40)
+
+ # try to guess where user is so when he tried to change the
+ # location of something, we try 'here' first.
+ # Update this whenever we find something, or set a location.
+ # We clear to -1 when we change place
+ self.current_loc = -1
+
+ vb = gtk.VBox()
+ window.add(vb)
+ vb.show()
+
+ top = gtk.HBox()
+ top.set_size_request(-1,40)
+ vb.pack_start(top, expand=False)
+ top.show()
+
+ glob = gtk.HBox()
+ glob.set_size_request(-1,80)
+ glob.set_homogeneous(True)
+ vb.pack_end(glob, expand=False)
+ glob.show()
+ self.glob_control = glob
+
+ locedit = gtk.HBox()
+ locedit.set_size_request(-1, 80)
+ vb.pack_end(locedit, expand=False)
+ locedit.hide()
+ self.locedit = locedit
+
+ loc = gtk.HBox()
+ loc.set_size_request(-1,80)
+ vb.pack_end(loc, expand=False)
+ loc.hide()
+ self.loc = loc
+
+ placeb = gtk.HBox()
+ placeb.set_size_request(-1,80)
+ vb.pack_end(placeb, expand=False)
+ placeb.hide()
+ self.place = placeb
+
+ filemenu = gtk.HBox()
+ filemenu.set_homogeneous(True)
+ filemenu.set_size_request(-1,80)
+ vb.pack_end(filemenu, expand=False)
+ filemenu.hide()
+ self.filemenu = filemenu
+
+ curr = gtk.HBox()
+ curr.set_size_request(-1,80)
+ curr.set_homogeneous(True)
+ vb.pack_end(curr, expand=False)
+ curr.show()
+ self.curr = curr
+ self.mode = 'curr'
+
+ e = gtk.Entry()
+
+ l = PurchView(34, self.item_selected, e)
+ vb.add(l.widget)
+ self.lview = l
+
+ # multi use text-entry body
+ # used for search-string, comment-entry, item-entry/rename
+ #
+ ctx = e.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(25*pango.SCALE)
+ e.modify_font(fd)
+ e.show()
+ top.add(e)
+ self.button(gtk.STOCK_OK, top, self.enter_change, expand = False)
+ self.entry = e
+ global XX
+ XX = e
+ self.entry_ignore = True
+ self.entry_mode = "comment"
+ e.connect("changed", self.entry_changed)
+ self.ecol_search = None
+ self.ecol_comment = None
+ self.ecol_item = None
+ self.ecol_loc = None
+
+ # global control buttons
+ self.button(gtk.STOCK_REFRESH, glob, self.choose_place)
+ self.button(gtk.STOCK_PREFERENCES, glob, self.show_controls)
+ self.search_toggle = self.button(gtk.STOCK_FIND, glob, self.toggle_search, toggle=True)
+ self.button(gtk.STOCK_ADD, glob, self.add_item)
+ self.button(gtk.STOCK_EDIT, glob, self.change_name)
+
+ # buttons to control current entry
+ self.button(gtk.STOCK_APPLY, curr, self.tick)
+ self.button(gtk.STOCK_CANCEL, curr, self.cross)
+ self.button(gtk.STOCK_JUMP_TO, curr, self.choose_loc)
+ self.button(gtk.STOCK_ZOOM_IN, curr, self.zoomin)
+ self.button(gtk.STOCK_ZOOM_OUT, curr, self.zoomout)
+
+ # buttons for whole-list operations
+ self.button(gtk.STOCK_SAVE_AS, filemenu, self.record)
+ self.button(gtk.STOCK_HOME, filemenu, self.reset)
+ a = self.button(gtk.STOCK_CONVERT, filemenu, self.add_regulars)
+ self.add_reg_clicked = False
+ a.connect('leave',self.clear_regulars)
+ self.save_button = self.button(gtk.STOCK_SAVE, filemenu, self.save)
+ self.revert_button = self.button(gtk.STOCK_REVERT_TO_SAVED, filemenu, self.revert_to_saved)
+ self.revert_button.hide()
+ self.button(gtk.STOCK_QUIT, filemenu, self.close_application)
+
+ # Buttons to change location of current entry
+ self.button(gtk.STOCK_GO_BACK, loc, self.prevloc, expand = False)
+ self.curr_loc = self.button("here", loc, self.curr_switch)
+ self.button(gtk.STOCK_GO_FORWARD, loc, self.nextloc, expand = False)
+ l = self.curr_loc.child
+ ctx = l.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(25*pango.SCALE)
+ l.modify_font(fd)
+
+ # buttons to edit the current location or place
+ self.button(gtk.STOCK_DELETE, locedit, self.locdelete)
+ self.button(gtk.STOCK_GO_UP, locedit, self.loc_move_up)
+ self.button(gtk.STOCK_GO_DOWN, locedit, self.loc_move_down)
+ self.button(gtk.STOCK_ADD, locedit, self.add_item)
+ self.button(gtk.STOCK_EDIT, locedit, self.change_name)
+
+
+ # Buttons to change current 'place'
+ self.button(gtk.STOCK_MEDIA_REWIND, placeb, self.prevplace, expand = False)
+ self.curr_place = self.button("HOME", placeb, self.curr_switch)
+ self.button(gtk.STOCK_MEDIA_FORWARD, placeb, self.nextplace, expand = False)
+ l = self.curr_place.child
+ ctx = l.get_pango_context()
+ fd = ctx.get_font_description()
+ fd.set_absolute_size(25*pango.SCALE)
+ l.modify_font(fd)
+
+
+ window.set_default_size(480, 640)
+ window.show()
+
+
+ def close_application(self, widget):
+ gtk.main_quit()
+ if table_timeout:
+ table_tick()
+ if list_timeout:
+ list_tick()
+
+ def item_selected(self, purch, pos):
+ if pos == 'heading':
+ if self.mode != 'loc':
+ return
+ newloc = None
+ i = None
+ for i in range(len(locations[place])):
+ if locations[place][i] == purch:
+ newloc = i
+ if i == None:
+ return
+ extend_array(products[self.purch.prod].loc, place, -1)
+ products[self.purch.prod].loc[place] = newloc
+ table_changed()
+ self.lview.plist.sort(purch_cmp)
+ self.set_loc()
+ self.lview.select()
+ return
+
+ if self.entry_mode == "comment":
+ self.lview.select(purch.prod)
+ self.purch = purch
+ self.entry_ignore = True
+ self.entry.set_text(purch.comment)
+ self.entry_ignore = False
+ self.set_loc()
+ #if pos < 0:
+ # self.tick(None)
+ #if pos > 0:
+ # self.cross(None)
+ if self.entry_mode == "search":
+ if pos != 'auto':
+ if purch.state == 'X' or purch.state == 'R':
+ purch.state = 'N'
+ elif purch.state == 'N':
+ purch.state = 'X'
+ self.lview.select(purch.prod)
+ self.purch = purch
+ self.set_loc()
+ list_changed()
+
+
+ def entry_changed(self, widget):
+ if self.entry_ignore:
+ return
+ if self.entry_mode == "search":
+ self.lview.set_search(self.entry.get_text())
+ if self.entry_mode == "comment" and self.purch != None:
+ self.purch.comment = self.entry.get_text()
+ list_changed()
+
+ def set_purch(self, purch):
+ self.lview.set_purch(purch)
+
+ def button(self, name, bar, func, expand = True, toggle = False):
+ if toggle:
+ btn = gtk.ToggleButton()
+ else:
+ btn = gtk.Button()
+ if type(name) == str and name[0:3] != "gtk":
+ if not expand:
+ name = " " + name + " "
+ btn.set_label(name)
+ else:
+ img = gtk.image_new_from_stock(name, self.isize)
+ if not expand:
+ img.set_size_request(80, -1)
+ img.show()
+ btn.add(img)
+ btn.show()
+ bar.pack_start(btn, expand = expand)
+ btn.connect("clicked", func)
+ btn.set_focus_on_click(False)
+ return btn
+
+ def record(self, widget):
+ # Record current purchase file in datestamped storage
+ save_list(time.strftime("purch-%Y%m%d-%H"))
+
+ def reset(self, widget):
+ # Reset the shopping list.
+ # All regular to noneed
+ # if there were no regular, then
+ # found -> noneed
+ # cannot find -> need
+ found = False
+ for p in purch:
+ if p.state == 'R':
+ p.state = 'X'
+ found = True
+ if not found:
+ for p in purch:
+ if p.state == 'F':
+ p.state = 'X'
+ if p.state == 'C':
+ p.state = 'N'
+ list_changed()
+ self.lview.select()
+
+ def add_regulars(self, widget):
+ if self.add_reg_clicked:
+ return self.all_regulars(widget)
+ # Mark all regulars (not already selected) as regulars
+ for p in purch:
+ if products[p.prod].regular and p.state == 'X':
+ p.state = 'R'
+ list_changed()
+ self.lview.select()
+ self.add_reg_clicked = True
+
+ def all_regulars(self, widget):
+ # Mark all regulars and don'twant (not already selected) as regulars
+ for p in purch:
+ if p.state == 'X':
+ p.state = 'R'
+ list_changed()
+ self.lview.select()
+ def clear_regulars(self, widget):
+ self.add_reg_clicked = False
+
+ def save(self, widget):
+ table_tick()
+ list_tick()
+ self.curr_switch(widget)
+
+
+ def setecol(self):
+ if self.ecol_search != None:
+ return
+ c = gtk.gdk.color_parse("yellow")
+ self.ecol_search = c
+
+ c = gtk.gdk.color_parse("white")
+ self.ecol_comment = c
+
+ c = gtk.gdk.color_parse("pink")
+ self.ecol_item = c
+
+ c = gtk.gdk.color_parse('grey90')
+ self.ecol_loc = c
+
+ def toggle_search(self, widget):
+ self.setecol()
+ if self.entry_mode == "item":
+ self.search_toggle.set_active(False)
+ return
+ if self.entry_mode == "search":
+ self.entry_ignore = True
+ self.entry.set_text(self.purch.comment)
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment)
+ self.entry_mode = "comment"
+ self.lview.set_search(None)
+ self.entry_ignore = False
+ return
+ self.entry_mode = "search"
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_search)
+ self.entry_ignore = True
+ self.entry.set_text("")
+ self.lview.set_search("")
+ self.entry_ignore = False
+ self.search_toggle.set_active(True)
+
+ def choose_loc(self, widget):
+ # replace 'curr' buttons with 'loc' buttons
+ if self.purch == None:
+ return
+ self.curr.hide()
+ self.filemenu.hide()
+ self.loc.show()
+ self.locedit.show()
+ self.glob_control.hide()
+ self.mode = 'loc'
+ self.set_loc()
+ self.lview.show_headers(True)
+
+ def set_loc(self):
+ loc = locname(self.purch)
+ self.current_loc = self.purch.loc()
+ self.curr_loc.child.set_text(places[place]+" / "+loc)
+
+ def curr_switch(self, widget):
+ # set current item to current location, and switch back
+ self.lview.show_headers(False)
+ self.loc.hide()
+ self.locedit.hide()
+ self.glob_control.show()
+ self.place.hide()
+ self.filemenu.hide()
+ self.curr.show()
+ self.mode = 'curr'
+
+ def show_controls(self, widget):
+ if self.mode == 'filemenu':
+ self.curr_switch(widget)
+ else:
+ self.lview.show_headers(False)
+ self.loc.hide()
+ self.place.hide()
+ self.curr.hide()
+ self.locedit.hide()
+ self.glob_control.show()
+ self.filemenu.show()
+ self.mode = 'filemenu'
+
+
+ def nextloc(self, widget):
+ if self.entry_mode != 'comment':
+ self.enter_change(None)
+ if self.current_loc != -1 and self.current_loc != self.purch.loc():
+ newloc = self.current_loc
+ self.current_loc = -1
+ elif self.purch.loc() < 0:
+ newloc = locorder[place][0]
+ else:
+ i = locorder[place].index(self.purch.loc())
+ if i < len(locorder[place])-1:
+ newloc = locorder[place][i+1]
+ else:
+ return
+
+
+ if newloc < -1 or newloc >= len(locations[place]):
+ return
+ extend_array(products[self.purch.prod].loc, place, -1)
+ products[self.purch.prod].loc[place] = newloc
+ table_changed()
+ self.lview.plist.sort(purch_cmp)
+ self.set_loc()
+ self.lview.select()
+
+ def prevloc(self, widget):
+ if self.entry_mode != 'comment':
+ self.enter_change(None)
+ if self.current_loc != -1 and self.current_loc != self.purch.loc():
+ newloc = self.current_loc
+ self.current_loc = -1
+ elif self.purch.loc() < 0:
+ return
+ else:
+ i = locorder[place].index(self.purch.loc())
+ if i > 0:
+ newloc = locorder[place][i-1]
+ else:
+ newloc = -1
+
+ if newloc < -1:
+ return
+ extend_array(products[self.purch.prod].loc, place, -1)
+ products[self.purch.prod].loc[place] = newloc
+ table_changed()
+ self.lview.plist.sort(purch_cmp)
+ self.set_loc()
+ self.lview.select()
+
+ def locdelete(self, widget):
+ # merge this location with the previous one
+ # So every product with this location needs to be changed,
+ # and the locorder updated.
+ if self.mode != "loc":
+ return
+ l = self.purch.loc()
+ if l < 0:
+ # cannot delete 'Unknown'
+ return
+ i = locorder[place].index(l)
+ if i == 0:
+ # nothing to merge with
+ return
+ self.backup_table()
+ newl = locorder[place][i-1]
+ for p in products:
+ if p != None:
+ if len(p.loc) > place:
+ if p.loc[place] == l:
+ p.loc[place] = newl
+ locorder[place][i:i+1] = []
+
+ table_changed()
+ self.lview.plist.sort(purch_cmp)
+ self.set_loc()
+ self.lview.select()
+
+ def loc_move_up(self, widget):
+ global place
+ if self.mode == "loc":
+ l = self.purch.loc()
+ if l < 0:
+ # Cannot move 'unknown'
+ return
+ i = locorder[place].index(l)
+ if i == 0:
+ # nowhere to move
+ pass
+ else:
+ o = locorder[place][i-1:i+1]
+ locorder[place][i-1:i+1] = [o[1],o[0]]
+ table_changed()
+ self.lview.plist.sort(purch_cmp)
+ self.set_loc()
+ self.lview.select()
+ return
+ if self.mode == 'place':
+ if place > 0:
+ swap_place(place-1, place)
+ self.prevplace(None)
+
+ def loc_move_down(self, widget):
+ global place
+ if self.mode == "loc":
+ l = self.purch.loc()
+ if l < 0:
+ # Cannot move 'unknown'
+ return
+ i = locorder[place].index(l)
+ if i+1 >= len(locorder[place]):
+ # nowhere to move
+ pass
+ else:
+ o = locorder[place][i:i+2]
+ locorder[place][i:i+2] = [o[1],o[0]]
+ table_changed()
+ self.lview.plist.sort(purch_cmp)
+ self.set_loc()
+ self.lview.select()
+
+ if self.mode == 'place':
+ if place < len(places)-1:
+ swap_place(place, place+1)
+ self.nextplace(None)
+
+
+ def choose_place(self, widget):
+ if self.entry_mode != 'comment':
+ self.enter_change(None)
+ if self.mode == 'place':
+ self.curr_switch(widget)
+ return
+ self.pl_visible = True
+ self.lview.show_headers(False)
+ self.loc.hide()
+ self.locedit.show()
+ self.glob_control.hide()
+ self.curr.hide()
+ self.filemenu.hide()
+ self.mode = 'place'
+ self.place.show()
+ self.set_place()
+
+ def set_place(self):
+ global place
+ if place >= len(places):
+ place = len(places) - 1
+ if place < 0:
+ place = 0
+ if place >= len(places):
+ pl = "Unknown Place"
+ else:
+ pl = places[place]
+ if place == 0:
+ prev = ""
+ else:
+ prev = places[place-1]
+ if place+1 >= len(places):
+ next = ""
+ else:
+ next = places[place+1]
+ spaces=" "
+ self.curr_place.child.set_markup(
+ '<span size="10000">'+prev+'\n</span>'
+ +spaces+pl+'\n'
+ +spaces+spaces+'<span size="10000" rise="5000">'+next+'</span>')
+ self.current_loc = -1
+
+ def nextplace(self, widget):
+ global place
+ if place >= len(places)-1:
+ return
+ place += 1
+ if self.lview.plist:
+ self.lview.plist.sort(purch_cmp)
+ self.set_place()
+ if self.purch:
+ self.lview.select()
+ else:
+ self.lview.set_purch(None)
+
+ def prevplace(self, widget):
+ global place
+ if place <= 0:
+ return
+ place -= 1
+ if self.lview.plist:
+ self.lview.plist.sort(purch_cmp)
+ self.set_place()
+ if self.purch:
+ self.lview.select()
+ else:
+ self.lview.set_purch(None)
+
+
+ def zoomin(self, widget):
+ self.lview.set_zoom(self.lview.zoom+1)
+ def zoomout(self, widget):
+ self.lview.set_zoom(self.lview.zoom-1)
+
+ def tick(self, widget):
+ if self.purch != None:
+ if self.entry_mode == "search":
+ # set to regular
+ products[self.purch.prod].regular = True
+ #self.purch.state = 'R'
+ self.lview.select(gonext=self.purch.state)
+ list_changed(); table_changed()
+ return
+ oldstate = self.purch.state
+ if self.purch.state == 'N': self.purch.state = 'F'
+ elif self.purch.state == 'C': self.purch.state = 'F'
+ elif self.purch.state == 'R': self.purch.state = 'N'
+ elif self.purch.state == 'X': self.purch.state = 'N'
+ if self.purch.state == 'F':
+ self.current_loc = self.purch.loc()
+ self.lview.select(gonext = oldstate)
+ list_changed()
+ def cross(self, widget):
+ if self.purch != None:
+ oldstate = self.purch.state
+ if self.entry_mode == "search":
+ # set to regular
+ products[self.purch.prod].regular = False
+ if self.purch.state == 'R':
+ self.purch.state = 'X'
+ self.lview.select(gonext=oldstate)
+ list_changed(); table_changed()
+ return
+
+ if self.purch.state == 'N': self.purch.state = 'C'
+ elif self.purch.state == 'F': self.purch.state = 'N'
+ elif self.purch.state == 'C': self.purch.state = 'X'
+ elif self.purch.state == 'R': self.purch.state = 'X'
+ elif self.purch.state == 'X': self.purch.state = 'X'
+ self.lview.select(gonext = oldstate)
+ list_changed()
+
+ def add_item(self, widget):
+ global place
+ self.setecol()
+ if self.entry_mode == "search":
+ self.search_toggle.set_active(False)
+ if self.entry_mode != "comment":
+ return
+ if self.mode == 'curr':
+ self.entry_mode = "item"
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item)
+ self.entry_ignore = True
+ self.entry.set_text("")
+ self.purch = None
+ self.lview.select()
+ self.entry_ignore = False
+ elif self.mode == 'loc':
+ if self.purch == None:
+ return
+ if None in locations[place]:
+ lnum = locations[place].index(None)
+ else:
+ lnum = len(locations[place])
+ locations[place].append(None)
+ locations[place][lnum] = 'NewLocation'
+ if self.purch.loc() == -1:
+ so = 0
+ else:
+ so = locorder[place].index(self.purch.loc())+1
+ locorder[place][so:so] = [lnum]
+ self.nextloc(None)
+ self.entry_mode = 'location'
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+ self.entry_ignore = True
+ self.entry.set_text('NewLocation')
+ self.entry.select_region(0,-1)
+ self.entry_ignore = False
+ elif self.mode == 'place':
+ if None in places:
+ pnum = places.index(None)
+ else:
+ pnum = len(places)
+ places.append(None)
+ places[pnum] = 'NewPlace'
+ extend_array(locations, pnum)
+ locations[pnum] = []
+ extend_array(locorder, pnum)
+ locorder[pnum] = []
+ place = pnum
+ self.lview.plist.sort(purch_cmp)
+ self.set_place()
+ self.entry_mode = 'place'
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+ self.entry_ignore = True
+ self.entry.set_text('NewPlace')
+ self.entry_ingore = False
+ def change_name(self, widget):
+ self.setecol()
+ if self.entry_mode == "search":
+ self.search_toggle.set_active(False)
+ if self.entry_mode == "item":
+ if self.purch != None:
+ self.entry.set_text(products[self.purch.prod].name)
+ return
+ if self.entry_mode == "location":
+ if self.purch != None:
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+ self.entry.set_text(locname(self.purch))
+ return
+ if self.entry_mode == 'place':
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+ set.entry.set_text(places[place])
+ return
+ if self.entry_mode != "comment":
+ return
+ if self.mode == 'curr':
+ if self.purch == None:
+ return
+ self.entry_mode = "item"
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item)
+ self.entry_ignore = True
+ self.entry.set_text(products[self.purch.prod].name)
+ self.entry_ignore = False
+ elif self.mode == 'loc':
+ if self.purch == None:
+ return
+ if self.purch.loc() < 0:
+ return
+ self.entry_mode = "location"
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+ self.entry_ignore = True
+ self.entry.set_text(locname(self.purch))
+ self.entry_ignore = False
+ elif self.mode == 'place':
+ self.entry_mode = 'place'
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc)
+ self.entry_ignore = True
+ self.entry.set_text(places[place])
+ self.entry_inode = False
+
+ #
+ # An item is being added or renamed. Commit the change
+ # If the new name is empty, that implys a delete.
+ # We only allow the delete if the state is 'X' and not regular
+ def update_item(self, name):
+ if len(name) > 0:
+ if self.purch == None:
+ # check for duplicates FIXME
+ num = len(products)
+ prod = Prod(num, name)
+ products.append(prod)
+ p = Purch(prod)
+ purch.append(p)
+ p.state = "N";
+ self.purch = p
+ self.set_purch(purch)
+ self.lview.select(num)
+ self.lview.plist.sort(purch_cmp)
+ else:
+ products[self.purch.prod].name = name
+ self.lview.select()
+ self.lview.plist.sort(purch_cmp)
+ self.forget_backup()
+ table_changed()
+ list_changed()
+ elif self.purch:
+ # delete?
+ if self.purch.state == 'N':
+ # OK to delete
+ products[self.purch.prod] = None
+ try:
+ i = purch.index(self.purch)
+ except:
+ pass
+ else:
+ if i == 0:
+ new = -1
+ else:
+ new = purch[i-1].prod
+ del purch[i]
+ self.lview.plist.sort(purch_cmp)
+ self.lview.select(new)
+ table_changed()
+ list_changed()
+ self.forget_backup()
+
+ def update_location(self, name):
+ if len(name) > 0:
+ locations[place][self.purch.loc()] = name
+ self.set_loc()
+ self.lview.select()
+ table_changed()
+ return
+ # See if we can delete this location
+ # need to check all products that they aren't 'here'
+ for p in products:
+ if p and p.num != self.purch.prod and place < len(p.loc):
+ if p.loc[place] == self.purch.loc():
+ return
+ # nothing here except 'purch'
+ l = self.purch.loc()
+ self.prevloc(None)
+ locations[place][l] = None
+ locorder[place].remove(l)
+ self.lview.plist.sort(purch_cmp)
+ self.lview.select()
+ table_changed()
+ list_changed()
+
+ def update_place(self, name):
+ global place
+ if len(name) > 0:
+ places[place] = name
+ self.lview.select()
+ self.set_place()
+ table_changed()
+ return
+ if len(places) <= 1:
+ return
+
+ self.backup_table()
+ places[place:place+1] = []
+ locations[place:place+1] = []
+ locorder[place:place+1] = []
+ if place >= len(places):
+ place -= 1
+ table_changed()
+ self.set_place()
+ self.lview.select()
+
+ def backup_table(self):
+ save_table("Products.backup")
+ self.save_button.hide()
+ self.revert_button.show()
+
+ def forget_backup(self):
+ self.save_button.show()
+ self.revert_button.hide()
+
+ def revert_to_saved(self, widget):
+ try:
+ f = open("Products.backup")
+ products = []
+ locations = []
+ locorder = []
+ places = []
+ load_table(f)
+ f.close()
+ except:
+ pass
+
+ self.forget_backup()
+
+
+ def enter_change(self, widget):
+ name = self.entry.get_text()
+ mode = self.entry_mode
+ # This is need to avoid recursion as update_* calls {next,prev}loc
+ self.entry_mode = 'comment'
+ if mode == 'item':
+ self.update_item(name)
+ elif mode == 'location':
+ self.update_location(name)
+ elif mode == 'place':
+ self.update_place(name)
+
+ self.entry_mode = "comment"
+ self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment)
+ self.entry_ignore = True
+ self.entry.set_text("")
+ self.entry_ignore = False
+
+def main():
+ gtk.main()
+ return 0
+
+if __name__ == "__main__":
+
+ home = os.getenv("HOME")
+ #p = os.path.join(home, "shopping")
+ p = "/data/shopping"
+ try:
+ os.mkdir(p)
+ except:
+ pass
+ if os.path.exists(p):
+ os.chdir(p)
+ else:
+ os.chdir(home)
+
+ products = []
+ locations = []
+ locorder = []
+ places = []
+ try:
+ f = open("Products")
+ load_table(f)
+ f.close()
+ except:
+ places = ['Home']
+ locorder = [[]]
+ locations =[[]]
+
+ purch = []
+ try:
+ f = open("Purchases")
+ except:
+ pass
+ else:
+ load_list(f)
+ f.close()
+ merge_list(purch, products)
+
+
+ place = 0
+ purch.sort(purch_cmp)
+
+ sl = ShoppingList()
+ sl.set_purch(purch)
+ ss = gtk.settings_get_default()
+ ss.set_long_property("gtk-cursor-blink", 0, "shop")
+ main()
--- /dev/null
+#!/usr/bin/env python
+
+import urllib, sys, os
+
+def make_url(sender,recipient,mesg):
+ if recipient[0] == '+':
+ recipient = recipient[1:]
+ elif recipient[0:2] != '04':
+ print "Invalid SMS address: " + recipient
+ sys.exit(1)
+
+ return "https://www.exetel.com.au/sendsms/api_sms.php?username=0293169905&password=birtwhistle&mobilenumber=%s&message=%s&sender=%s&messagetype=Text" % (
+ recipient,
+ urllib.quote(mesg),
+ sender
+ )
+
+
+def send(sender, recipient, mesg):
+ try:
+ f = urllib.urlopen(make_url(sender,recipient, mesg))
+ except:
+ rv = 2
+ print "Cannot connect: " + sys.exc_value.strerror
+ else:
+ rv = 0
+ for l in f:
+ l = l.strip()
+ if not l:
+ continue
+ f = l.split('|')
+ if len(f) == 5:
+ if f[0] != '1':
+ rv = 1
+ m = f[4]
+ if m[-4:] == '<br>':
+ m = m[0:-4]
+ print m,
+ else:
+ rv = 1
+ print l,
+ print
+ return rv
+
+os.system("/bin/sh /root/pppon")
+ec = send(sys.argv[1], sys.argv[2], sys.argv[3])
+sys.exit(ec)
+
--- /dev/null
+#!/usr/bin/env python
+
+# Create/edit/send/display/search SMS messages.
+# Two main displays: Create and display
+# Create:
+# Allow entry of recipient and text of SMS message and allow basic editting
+# When entering recipient, text box can show address matches for selection
+# Bottom buttons are "Select"..
+# When entering text, if there is no text, buttom buttons are:
+# "Browse", "Close"
+# If these is some text, bottom buttons are:
+# "Send", Save"
+#
+# Display:
+# We usually display a list of messages which can be selected from
+# There is a 'search' box to restrict message to those with a string
+# Options for selected message are:
+# Delete Reply View Open(for draft)/Forward(for non-draft)
+# In View mode, the whole text is displayed and the 'View' button becomes "Index"
+# or "Show List" or "ReadIt"
+# General options are:
+# New Config List
+# New goes to Edit
+#
+# Delete becomes Undelete and can undelete a whole stack.
+# Delete can become undelete without deleting be press-and-hold
+#
+#
+# Messages are sent using a separate program. e.g. sms-gsm
+# Different recipients can use different programs based on flag in address book.
+# Somehow senders can be configured.
+# e.g. sms-exetel needs username, password, sender strings.
+# press-and-hold on the send button allows a sender to be selected.
+#
+#
+# Send an SMS message using some backend.
+
+#
+#
+# TODO:
+# 'del' to return to 'list' view
+# top buttons: del, view/list, new/open/reply
+# so can only reply when viewing whole message
+# Bottom:
+# all: sent recv
+# send: all draft
+# recv: all new
+# draft: all sent
+# new: all recv
+# DONE handle newline chars in summary
+# DONE cope properly when the month changes.
+# switch-to-'new' on 'expose'
+# 'draft' button becomes 'cancel' when all is empty
+# DONE better display of name/number of destination
+# jump to list mode when change 'list'
+# 'open' becomes 'reply' when current message was received.
+# new message becomes non-new when replied to
+# '<list>' button doesn't select, but just makes choice.
+# 'new' becomes 'select' when <list> has been pressed.
+# DONE Start in 'read', preferrably 'new'
+# DONE always report status from send
+# DONE draft/new/recv/sent/all - 5 groups
+# DONE allow scrolling through list
+# DONE + prefix to work
+# DONE compose as 'GSM' or 'EXE' send
+# DONE somehow do addressbook lookup for compose
+# DONE addressbook lookup for display
+# On 'send' move to 'sent' (not draft) and display list
+# When open 'draft', delete from drafts... or later..
+# When 'reply' to new message, make it not 'new'
+#
+# get 'cut' to work from phone number entry.
+# how to configure sender...
+# need to select 'number only' mode for entry
+# need drop-down of common numbers
+# DONE text wrapping
+# punctuation
+# faster text input!!!
+# DONE status message of transmission
+# DONE maybe go to 'past messages' on send - need to go somewhere
+# cut from other sources??
+# DONE scroll if message is too long!
+#
+# DONE reread sms file when changing view
+# Don't add drafts that have not been changed... or
+# When opening a draft, delete it... or replace when re-add
+# DONE when sending a message, store - as draft if send failed
+# DONE show the 'send' status somewhere
+# DONE add a 'new' button from 'list' to 'send'
+# Need 'reply' button.. Make 'open' show 'reply' when 'to' me.
+# Scroll when near top or bottom
+# hide status line when not needed.
+# searching mesg list
+# 'folder' view - by month or day
+# highlight 'new' and 'draft' messages in different colour
+# support 'sent' and 'received' distinction
+# when return from viewing a 'new' message, clear the 'new' status
+# enable starting in 'listing/New' mode
+
+import gtk, pango
+import sys, time, os, re
+import struct
+from subprocess import Popen, PIPE
+from storesms import SMSstore, SMSmesg
+import dnotify
+
+###########################################################
+# Writing recognistion code
+import math
+
+
+def LoadDict(dict):
+ # Upper case.
+ # Where they are like lowercase, we either double
+ # the last stroke (L, J, I) or draw backwards (S, Z, X)
+ # U V are a special case
+
+ dict.add('A', "R(4)6,8")
+ dict.add('B', "R(4)6,4.R(7)1,6")
+ dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6")
+ dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6")
+ dict.add('C', "R(4)8,2")
+ dict.add('D', "R(4)6,6")
+ dict.add('E', "L(1)2,8.L(7)2,8")
+ # double the stem for F
+ dict.add('F', "L(4)2,6.S(3)7,1")
+ dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1")
+
+ dict.add('G', "L(4)2,5.S(8)1,7")
+ dict.add('G', "L(4)2,5.R(8)6,8")
+ # FIXME I need better straight-curve alignment
+ dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1")
+ dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1")
+ # capital I is down/up
+ dict.add('I', "S(4)1,7.S(4)7,1")
+
+ # Capital J has a left/right tail
+ dict.add('J', "R(4)1,6.S(7)3,5")
+
+ dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8")
+
+ # Capital L, like J, doubles the foot
+ dict.add('L', "L(4)0,8.S(7)4,3")
+
+ dict.add('M', "R(3)6,5.R(5)3,8")
+ dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8")
+
+ dict.add('N', "R(3)6,8.L(5)0,2")
+
+ # Capital O is CW, but can be CCW in special dict
+ dict.add('O', "R(4)1,1", bot='0')
+
+ dict.add('P', "R(4)6,3")
+ dict.add('Q', "R(4)7,7.S(8)0,8")
+
+ dict.add('R', "R(4)6,4.S(8)0,8")
+
+ # S is drawn bottom to top.
+ dict.add('S', "L(7)6,1.R(1)7,2")
+
+ # Double the stem for capital T
+ dict.add('T', "R(4)0,8.S(5)7,1")
+
+ # U is L to R, V is R to L for now
+ dict.add('U', "L(4)0,2")
+ dict.add('V', "R(4)2,0")
+
+ dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0")
+ dict.add('W', "R(5)2,3.R(3)5,0")
+
+ dict.add('X', "R(4)6,0")
+
+ dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2")
+ dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2")
+
+ dict.add('Z', "R(4)8,2.L(4)6,0")
+
+ # Lower case
+ dict.add('a', "L(4)2,2.L(5)1,7")
+ dict.add('a', "L(4)2,2.L(5)0,8")
+ dict.add('a', "L(4)2,2.S(5)0,8")
+ dict.add('b', "S(3)1,7.R(7)6,3")
+ dict.add('c', "L(4)2,8", top='C')
+ dict.add('d', "L(4)5,2.S(5)1,7")
+ dict.add('d', "L(4)5,2.L(5)0,8")
+ dict.add('e', "S(4)3,5.L(4)5,8")
+ dict.add('e', "L(4)3,8")
+ dict.add('f', "L(4)2,6", top='F')
+ dict.add('f', "S(1)5,3.S(3)1,7", top='F')
+ dict.add('g', "L(1)2,2.R(4)1,6")
+ dict.add('h', "S(3)1,7.R(7)6,8")
+ dict.add('h', "L(3)0,5.R(7)6,8")
+ dict.add('i', "S(4)1,7", top='I', bot='1')
+ dict.add('j', "R(4)1,6", top='J')
+ dict.add('k', "L(3)0,5.L(7)2,8")
+ dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8")
+ dict.add('l', "L(4)0,8", top='L')
+ dict.add('l', "S(3)1,7.S(7)3,5", top='L')
+ dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8")
+ dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8")
+ dict.add('n', "S(3)1,7.R(4)6,8")
+ dict.add('o', "L(4)1,1", top='O', bot='0')
+ dict.add('p', "S(3)1,7.R(4)6,3")
+ dict.add('q', "L(1)2,2.L(5)1,5")
+ dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2")
+ dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7")
+ # FIXME this double 1,7 is due to a gentle where the
+ # second looks like a line because it is narrow.??
+ dict.add('r', "S(3)1,7.R(4)6,2")
+ dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5')
+ dict.add('t', "R(4)0,8", top='T', bot='7')
+ dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7')
+ dict.add('u', "L(4)0,2.S(5)1,7")
+ dict.add('v', "L(4)0,2.L(2)0,2")
+ dict.add('w', "L(3)0,2.L(5)0,2", top='W')
+ dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W')
+ dict.add('w', "L(3)0,5.L(5)3,2", top='W')
+ dict.add('x', "L(4)0,6", top='X')
+ dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved
+ dict.add('y', "L(1)0,2.S(5)2,7", top='Y')
+ dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2')
+
+ # Digits
+ dict.add('0', "L(4)7,7")
+ dict.add('0', "R(4)7,7")
+ dict.add('1', "S(4)7,1")
+ dict.add('2', "R(4)0,6.S(7)3,5")
+ dict.add('2', "R(4)3,6.L(4)2,8")
+ dict.add('3', "R(1)0,6.R(7)1,6")
+ dict.add('4', "L(4)7,5")
+ dict.add('5', "L(1)2,6.R(7)0,3")
+ dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3")
+ dict.add('6', "L(4)2,3")
+ dict.add('7', "S(1)3,5.R(4)1,6")
+ dict.add('7', "R(4)0,6")
+ dict.add('7', "R(4)0,7")
+ dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1")
+ dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1")
+ dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0")
+ dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1")
+ dict.add('9', "L(1)2,2.S(5)1,7")
+
+ dict.add(' ', "S(4)3,5")
+ dict.add('<BS>', "S(4)5,3")
+ dict.add('-', "S(4)3,5.S(4)5,3")
+ dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5")
+ dict.add("<left>", "S(4)5,3.S(3)3,5")
+ dict.add("<right>","S(4)3,5.S(5)5,3")
+ dict.add("<left>", "S(4)7,1.S(1)1,7") # "<up>"
+ dict.add("<right>","S(4)1,7.S(7)7,1") # "<down>"
+ dict.add("<newline>", "S(4)2,6")
+
+
+class DictSegment:
+ # Each segment has for elements:
+ # direction: Right Straight Left (R=cw, L=ccw)
+ # location: 0-8.
+ # start: 0-8
+ # finish: 0-8
+ # Segments match if there difference at each element
+ # is 0, 1, or 3 (RSL coded as 012)
+ # A difference of 1 required both to be same / 3
+ # On a match, return number of 0s
+ # On non-match, return -1
+ def __init__(self, str):
+ # D(L)S,R
+ # 0123456
+ self.e = [0,0,0,0]
+ if len(str) != 7:
+ raise ValueError
+ if str[1] != '(' or str[3] != ')' or str[5] != ',':
+ raise ValueError
+ if str[0] == 'R':
+ self.e[0] = 0
+ elif str[0] == 'L':
+ self.e[0] = 2
+ elif str[0] == 'S':
+ self.e[0] = 1
+ else:
+ raise ValueError
+
+ self.e[1] = int(str[2])
+ self.e[2] = int(str[4])
+ self.e[3] = int(str[6])
+
+ def match(self, other):
+ cnt = 0
+ for i in range(0,4):
+ diff = abs(self.e[i] - other.e[i])
+ if diff == 0:
+ cnt += 1
+ elif diff == 3:
+ pass
+ elif diff == 1 and (self.e[i]/3 == other.e[i]/3):
+ pass
+ else:
+ return -1
+ return cnt
+
+class DictPattern:
+ # A Dict Pattern is a list of segments.
+ # A parsed pattern matches a dict pattern if
+ # the are the same nubmer of segments and they
+ # all match. The value of the match is the sum
+ # of the individual matches.
+ # A DictPattern is printers as segments joined by periods.
+ #
+ def __init__(self, str):
+ self.segs = map(DictSegment, str.split("."))
+ def match(self,other):
+ if len(self.segs) != len(other.segs):
+ return -1
+ cnt = 0
+ for i in range(0,len(self.segs)):
+ m = self.segs[i].match(other.segs[i])
+ if m < 0:
+ return m
+ cnt += m
+ return cnt
+
+
+class Dictionary:
+ # The dictionary hold all the pattern for symbols and
+ # performs lookup
+ # Each pattern in the directionary can be associated
+ # with 3 symbols. One when drawing in middle of screen,
+ # one for top of screen, one for bottom.
+ # Often these will all be the same.
+ # This allows e.g. s and S to have the same pattern in different
+ # location on the touchscreen.
+ # A match requires a unique entry with a match that is better
+ # than any other entry.
+ #
+ def __init__(self):
+ self.dict = []
+ def add(self, sym, pat, top = None, bot = None):
+ if top == None: top = sym
+ if bot == None: bot = sym
+ self.dict.append((DictPattern(pat), sym, top, bot))
+
+ def _match(self, p):
+ max = -1
+ val = None
+ for (ptn, sym, top, bot) in self.dict:
+ cnt = ptn.match(p)
+ if cnt > max:
+ max = cnt
+ val = (sym, top, bot)
+ elif cnt == max:
+ val = None
+ return val
+
+ def match(self, str, pos = "mid"):
+ p = DictPattern(str)
+ m = self._match(p)
+ if m == None:
+ return m
+ (mid, top, bot) = self._match(p)
+ if pos == "top": return top
+ if pos == "bot": return bot
+ return mid
+
+
+class Point:
+ # This represents a point in the path and all the points leading
+ # up to it. It allows us to find the direction and curvature from
+ # one point to another
+ # We store x,y, and sum/cnt of points so far
+ def __init__(self,x,y) :
+ self.xsum = x
+ self.ysum = y
+ self.x = x
+ self.y = y
+ self.cnt = 1
+
+ def copy(self):
+ n = Point(0,0)
+ n.xsum = self.xsum
+ n.ysum = self.ysum
+ n.x = self.x
+ n.y = self.y
+ n.cnt = self.cnt
+ return n
+
+ def add(self,x,y):
+ if self.x == x and self.y == y:
+ return
+ self.x = x
+ self.y = y
+ self.xsum += x
+ self.ysum += y
+ self.cnt += 1
+
+ def xlen(self,p):
+ return abs(self.x - p.x)
+ def ylen(self,p):
+ return abs(self.y - p.y)
+ def sqlen(self,p):
+ x = self.x - p.x
+ y = self.y - p.y
+ return x*x + y*y
+
+ def xdir(self,p):
+ if self.x > p.x:
+ return 1
+ if self.x < p.x:
+ return -1
+ return 0
+ def ydir(self,p):
+ if self.y > p.y:
+ return 1
+ if self.y < p.y:
+ return -1
+ return 0
+ def curve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ if curve > 6:
+ return 1
+ if curve < -6:
+ return -1
+ return 0
+
+ def Vcurve(self,p):
+ if self.cnt == p.cnt:
+ return 0
+ x1 = p.x ; y1 = p.y
+ (x2,y2) = self.meanpoint(p)
+ x3 = self.x; y3 = self.y
+
+ curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1)
+ curve = curve * 100 / ((y3-y1)*(y3-y1)
+ + (x3-x1)*(x3-x1))
+ return curve
+
+ def meanpoint(self,p):
+ x = (self.xsum - p.xsum) / (self.cnt - p.cnt)
+ y = (self.ysum - p.ysum) / (self.cnt - p.cnt)
+ return (x,y)
+
+ def is_sharp(self,A,C):
+ # Measure the cosine at self between A and C
+ # as A and C could be curve, we take the mean point on
+ # self.A and self.C as the points to find cosine between
+ (ax,ay) = self.meanpoint(A)
+ (cx,cy) = self.meanpoint(C)
+ a = ax-self.x; b=ay-self.y
+ c = cx-self.x; d=cy-self.y
+ x = a*c + b*d
+ y = a*d - b*c
+ h = math.sqrt(x*x+y*y)
+ if h > 0:
+ cs = x*1000/h
+ else:
+ cs = 0
+ return (cs > 900)
+
+class BBox:
+ # a BBox records min/max x/y of some Points and
+ # can subsequently report row, column, pos of each point
+ # can also locate one bbox in another
+
+ def __init__(self, p):
+ self.minx = p.x
+ self.maxx = p.x
+ self.miny = p.y
+ self.maxy = p.y
+
+ def width(self):
+ return self.maxx - self.minx
+ def height(self):
+ return self.maxy - self.miny
+
+ def add(self, p):
+ if p.x > self.maxx:
+ self.maxx = p.x
+ if p.x < self.minx:
+ self.minx = p.x
+
+ if p.y > self.maxy:
+ self.maxy = p.y
+ if p.y < self.miny:
+ self.miny = p.y
+ def finish(self, div = 3):
+ # if aspect ratio is bad, we adjust max/min accordingly
+ # before setting [xy][12]. We don't change self.min/max
+ # as they are used to place stroke in bigger bbox.
+ # Normally divisions are at 1/3 and 2/3. They can be moved
+ # by setting div e.g. 2 = 1/2 and 1/2
+ (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy)
+ if (maxx - minx) * 3 < (maxy - miny) * 2:
+ # too narrow
+ mid = int((maxx + minx)/2)
+ halfwidth = int ((maxy - miny)/3)
+ minx = mid - halfwidth
+ maxx = mid + halfwidth
+ if (maxy - miny) * 3 < (maxx - minx) * 2:
+ # too wide
+ mid = int((maxy + miny)/2)
+ halfheight = int ((maxx - minx)/3)
+ miny = mid - halfheight
+ maxy = mid + halfheight
+
+ div1 = div - 1
+ self.x1 = int((div1*minx + maxx)/div)
+ self.x2 = int((minx + div1*maxx)/div)
+ self.y1 = int((div1*miny + maxy)/div)
+ self.y2 = int((miny + div1*maxy)/div)
+
+ def row(self, p):
+ # 0, 1, 2 - top to bottom
+ if p.y <= self.y1:
+ return 0
+ if p.y < self.y2:
+ return 1
+ return 2
+ def col(self, p):
+ if p.x <= self.x1:
+ return 0
+ if p.x < self.x2:
+ return 1
+ return 2
+ def box(self, p):
+ # 0 to 9
+ return self.row(p) * 3 + self.col(p)
+
+ def relpos(self,b):
+ # b is a box within self. find location 0-8
+ if b.maxx < self.x2 and b.minx < self.x1:
+ x = 0
+ elif b.minx > self.x1 and b.maxx > self.x2:
+ x = 2
+ else:
+ x = 1
+ if b.maxy < self.y2 and b.miny < self.y1:
+ y = 0
+ elif b.miny > self.y1 and b.maxy > self.y2:
+ y = 2
+ else:
+ y = 1
+ return y*3 + x
+
+
+def different(*args):
+ cur = 0
+ for i in args:
+ if cur != 0 and i != 0 and cur != i:
+ return True
+ if cur == 0:
+ cur = i
+ return False
+
+def maxcurve(*args):
+ for i in args:
+ if i != 0:
+ return i
+ return 0
+
+class PPath:
+ # a PPath refines a list of x,y points into a list of Points
+ # The Points mark out segments which end at significant Points
+ # such as inflections and reversals.
+
+ def __init__(self, x,y):
+
+ self.start = Point(x,y)
+ self.mid = Point(x,y)
+ self.curr = Point(x,y)
+ self.list = [ self.start ]
+
+ def add(self, x, y):
+ self.curr.add(x,y)
+
+ if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or
+ (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or
+ (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))):
+ pass
+ else:
+ self.mid = self.curr.copy()
+
+ if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4:
+ self.start = self.mid.copy()
+ self.list.append(self.start)
+ self.mid = self.curr.copy()
+
+ def close(self):
+ self.list.append(self.curr)
+
+ def get_sectlist(self):
+ if len(self.list) <= 2:
+ return [[0,self.list]]
+ l = []
+ A = self.list[0]
+ B = self.list[1]
+ s = [A,B]
+ curcurve = B.curve(A)
+ for C in self.list[2:]:
+ cabc = C.curve(A)
+ cab = B.curve(A)
+ cbc = C.curve(B)
+ if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve):
+ # B is too pointy, must break here
+ l.append([curcurve, s])
+ s = [B, C]
+ curcurve = cbc
+ elif not different(cabc, cab, cbc, curcurve):
+ # all happy
+ s.append(C)
+ if curcurve == 0:
+ curcurve = maxcurve(cab, cbc, cabc)
+ elif not different(cabc, cab, cbc) :
+ # gentle inflection along AB
+ # was: AB goes in old and new section
+ # now: AB only in old section, but curcurve
+ # preseved.
+ l.append([curcurve,s])
+ s = [A, B, C]
+ curcurve =maxcurve(cab, cbc, cabc)
+ else:
+ # Change of direction at B
+ l.append([curcurve,s])
+ s = [B, C]
+ curcurve = cbc
+
+ A = B
+ B = C
+ l.append([curcurve,s])
+
+ return l
+
+ def remove_shorts(self, bbox):
+ # in self.list, if a point is close to the previous point,
+ # remove it.
+ if len(self.list) <= 2:
+ return
+ w = bbox.width()/10
+ h = bbox.height()/10
+ n = [self.list[0]]
+ leng = w*h*2*2
+ for p in self.list[1:]:
+ l = p.sqlen(n[-1])
+ if l > leng:
+ n.append(p)
+ self.list = n
+
+ def text(self):
+ # OK, we have a list of points with curvature between.
+ # want to divide this into sections.
+ # for each 3 consectutive points ABC curve of ABC and AB and BC
+ # If all the same, they are all in a section.
+ # If not B starts a new section and the old ends on B or C...
+ BB = BBox(self.list[0])
+ for p in self.list:
+ BB.add(p)
+ BB.finish()
+ self.bbox = BB
+ self.remove_shorts(BB)
+ sectlist = self.get_sectlist()
+ t = ""
+ for c, s in sectlist:
+ if c > 0:
+ dr = "R" # clockwise is to the Right
+ elif c < 0:
+ dr = "L" # counterclockwise to the Left
+ else:
+ dr = "S" # straight
+ bb = BBox(s[0])
+ for p in s:
+ bb.add(p)
+ bb.finish()
+ # If all points are in some row or column, then
+ # line is S
+ rwdiff = False; cldiff = False
+ rw = bb.row(s[0]); cl=bb.col(s[0])
+ for p in s:
+ if bb.row(p) != rw: rwdiff = True
+ if bb.col(p) != cl: cldiff = True
+ if not rwdiff or not cldiff: dr = "S"
+
+ t1 = dr
+ t1 += "(%d)" % BB.relpos(bb)
+ t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1]))
+ t += t1 + '.'
+ return t[:-1]
+
+
+class text_input:
+ def __init__(self, page, callout):
+
+ self.page = page
+ self.callout = callout
+ self.colour = None
+ self.line = None
+ self.dict = Dictionary()
+ self.active = True
+ LoadDict(self.dict)
+
+ page.connect("button_press_event", self.press)
+ page.connect("button_release_event", self.release)
+ page.connect("motion_notify_event", self.motion)
+ page.set_events(page.get_events()
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+ def set_colour(self, col):
+ self.colour = col
+
+ def press(self, c, ev):
+ if not self.active:
+ return
+ # Start a new line
+ self.line = [ [int(ev.x), int(ev.y)] ]
+ if not ev.send_event:
+ self.page.stop_emission('button_press_event')
+ return
+ def release(self, c, ev):
+ if self.line == None:
+ return
+ if len(self.line) == 1:
+ self.callout('click', ev)
+ self.line = None
+ return
+
+ sym = self.getsym()
+ if sym:
+ self.callout('sym', sym)
+ self.callout('redraw', None)
+ self.line = None
+ return
+
+ def motion(self, c, ev):
+ if self.line:
+ if ev.is_hint:
+ x, y, state = ev.window.get_pointer()
+ else:
+ x = ev.x
+ y = ev.y
+ x = int(x)
+ y = int(y)
+ prev = self.line[-1]
+ if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10:
+ return
+ if self.colour:
+ c.window.draw_line(self.colour, prev[0],prev[1],x,y)
+ self.line.append([x,y])
+ return
+
+ def getsym(self):
+ alloc = self.page.get_allocation()
+ pagebb = BBox(Point(0,0))
+ pagebb.add(Point(alloc.width, alloc.height))
+ pagebb.finish(div = 2)
+
+ p = PPath(self.line[1][0], self.line[1][1])
+ for pp in self.line[1:]:
+ p.add(pp[0], pp[1])
+ p.close()
+ patn = p.text()
+ pos = pagebb.relpos(p.bbox)
+ tpos = "mid"
+ if pos < 3:
+ tpos = "top"
+ if pos >= 6:
+ tpos = "bot"
+ sym = self.dict.match(patn, tpos)
+ if sym == None:
+ print "Failed to match pattern:", patn
+ return sym
+
+
+
+
+
+########################################################################
+
+
+
+class FingerText(gtk.TextView):
+ def __init__(self):
+ gtk.TextView.__init__(self)
+ self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
+ self.exphan = self.connect('expose-event', self.config)
+ self.input = text_input(self, self.stylus)
+
+ def config(self, *a):
+ self.disconnect(self.exphan)
+ c = gtk.gdk.color_parse('red')
+ gc = self.window.new_gc()
+ gc.set_foreground(self.get_colormap().alloc_color(c))
+ #gc.set_line_attributes(2, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
+ gc.set_subwindow(gtk.gdk.INCLUDE_INFERIORS)
+ self.input.set_colour(gc)
+
+ def stylus(self, cmd, info):
+ if cmd == "sym":
+ tl = self.get_toplevel()
+ w = tl.get_focus()
+ if w == None:
+ w = self
+ ev = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
+ ev.window = w.window
+ if info == '<BS>':
+ ev.keyval = 65288
+ ev.hardware_keycode = 22
+ else:
+ (ev.keyval,) = struct.unpack_from("b", info)
+ w.emit('key_press_event', ev)
+ #self.get_buffer().insert_at_cursor(info)
+ if cmd == 'click':
+ self.grab_focus()
+ if not info.send_event:
+ info.send_event = True
+ ev2 = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
+ ev2.send_event = True
+ ev2.window = info.window
+ ev2.time = info.time
+ ev2.x = info.x
+ ev2.y = info.y
+ ev2.button = info.button
+ self.emit('button_press_event', ev2)
+ self.emit('button_release_event', info)
+ if cmd == 'redraw':
+ self.queue_draw()
+
+ def insert_at_cursor(self, text):
+ self.get_buffer().insert_at_cursor(text)
+
+class FingerEntry(gtk.Entry):
+ def __init__(self):
+ gtk.Entry.__init__(self)
+
+ def insert_at_cursor(self, text):
+ c = self.get_property('cursor-position')
+ t = self.get_text()
+ t = t[0:c]+text+t[c:]
+ self.set_text(t)
+
+class SMSlist(gtk.DrawingArea):
+ def __init__(self, getlist):
+ gtk.DrawingArea.__init__(self)
+ self.pixbuf = None
+ self.width = self.height = 0
+ self.need_redraw = True
+ self.colours = None
+ self.collist = {}
+ self.get_list = getlist
+
+ self.connect("expose-event", self.redraw)
+ self.connect("configure-event", self.reconfig)
+
+ self.connect("button_release_event", self.release)
+ self.connect("button_press_event", self.press)
+ self.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.STRUCTURE_MASK)
+
+ # choose a font
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(25 * pango.SCALE)
+ self.font = fd
+ met = self.get_pango_context().get_metrics(fd)
+ self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE
+ fd = pango.FontDescription('sans 5')
+ fd.set_absolute_size(15 * pango.SCALE)
+ self.smallfont = fd
+ self.selected = 0
+ self.top = 0
+ self.book = None
+
+ self.smslist = []
+
+ self.queue_draw()
+
+
+ def set_book(self, book):
+ self.book = book
+
+ def lines(self):
+ alloc = self.get_allocation()
+ lines = alloc.height / self.lineheight
+ return lines
+
+ def reset_list(self):
+ self.selected = 0
+ self.smslist = None
+ self.size_requested = 0
+ self.refresh()
+
+ def refresh(self):
+ self.need_redraw = True
+ self.queue_draw()
+
+ def assign_colour(self, purpose, name):
+ self.collist[purpose] = name
+
+ def reconfig(self, w, ev):
+ alloc = w.get_allocation()
+ if not self.pixbuf:
+ return
+ if alloc.width != self.width or alloc.height != self.height:
+ self.pixbuf = None
+ self.need_redraw = True
+
+ def add_col(self, sym, col):
+ c = gtk.gdk.color_parse(col)
+ gc = self.window.new_gc()
+ gc.set_foreground(self.get_colormap().alloc_color(c))
+ self.colours[sym] = gc
+
+ def redraw(self, w, ev):
+ if self.colours == None:
+ self.colours = {}
+ for p in self.collist:
+ self.add_col(p, self.collist[p])
+ self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
+
+ if self.need_redraw:
+ self.draw_buf()
+
+ self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0,
+ self.width, self.height)
+
+ def draw_buf(self):
+ self.need_redraw = False
+ if self.pixbuf == None:
+ alloc = self.get_allocation()
+ self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height)
+ self.width = alloc.width
+ self.height = alloc.height
+ self.pixbuf.draw_rectangle(self.bg, True, 0, 0,
+ self.width, self.height)
+
+ if self.top > self.selected:
+ self.top = 0
+ max = self.lines()
+ if self.smslist == None or \
+ (self.top + max > len(self.smslist) and self.size_requested < self.top + max):
+ self.size_requested = self.top + max
+ self.smslist = self.get_list(self.top + max)
+ for i in range(len(self.smslist)):
+ if i < self.top:
+ continue
+ if i > self.top + max:
+ break
+ if i == self.selected:
+ col = self.colours['bg-selected']
+ else:
+ col = self.colours['bg-%d'%(i%2)]
+
+ self.pixbuf.draw_rectangle(col,
+ True, 0, (i-self.top)*self.lineheight,
+ self.width, self.lineheight)
+ self.draw_sms(self.smslist[i], (i - self.top) * self.lineheight)
+
+
+ def draw_sms(self, sms, yoff):
+
+ self.modify_font(self.smallfont)
+ tm = time.strftime("%Y-%m-%d %H:%M:%S", sms.time[0:6]+(0,0,0))
+ then = time.mktime(sms.time[0:6]+(0,0,-1))
+ now = time.time()
+ if now > then:
+ diff = now - then
+ if diff < 99:
+ delta = "%02d sec ago" % diff
+ elif diff < 99*60:
+ delta = "%02d min ago" % (diff/60)
+ elif diff < 48*60*60:
+ delta = "%02dh%02dm ago" % ((diff/60/60), (diff/60)%60)
+ else:
+ delta = tm[0:10]
+ tm = delta + tm[10:]
+
+ l = self.create_pango_layout(tm)
+ self.pixbuf.draw_layout(self.colours['time'],
+ 0, yoff, l)
+ co = sms.correspondent
+ if self.book:
+ cor = book_name(self.book, co)
+ if cor:
+ co = cor[0]
+ if sms.source == 'LOCAL':
+ col = self.colours['recipient']
+ co = 'To ' + co
+ else:
+ col = self.colours['sender']
+ co = 'From '+co
+ l = self.create_pango_layout(co)
+ self.pixbuf.draw_layout(col,
+ 0, yoff + self.lineheight/2, l)
+ self.modify_font(self.font)
+ t = sms.text.replace("\n", " ")
+ t = t.replace("\n", " ")
+ l = self.create_pango_layout(t)
+ if sms.state in ['DRAFT', 'NEW']:
+ col = self.colours['mesg-new']
+ else:
+ col = self.colours['mesg']
+ self.pixbuf.draw_layout(col,
+ 180, yoff, l)
+
+ def press(self,w,ev):
+ row = int(ev.y / self.lineheight)
+ self.selected = self.top + row
+ if self.selected >= len(self.smslist):
+ self.selected = len(self.smslist) - 1
+ if self.selected < 0:
+ self.selected = 0
+
+ l = self.lines()
+ self.top += row - l / 2
+ if self.top >= len(self.smslist) - l:
+ self.top = len(self.smslist) - l + 1
+ if self.top < 0:
+ self.top = 0
+
+ self.refresh()
+
+ def release(self,w,ev):
+ pass
+
+def load_book(file):
+ try:
+ f = open(file)
+ except:
+ f = open('/home/neilb/home/mobile-numbers-jan-08')
+ rv = []
+ for l in f:
+ x = l.split(';')
+ rv.append([x[0],x[1]])
+ rv.sort(lambda x,y: cmp(x[0],y[0]))
+ return rv
+
+def book_lookup(book, name, num):
+ st=[]; mid=[]
+ for l in book:
+ if name.lower() == l[0][0:len(name)].lower():
+ st.append(l)
+ elif l[0].lower().find(name.lower()) >= 0:
+ mid.append(l)
+ st += mid
+ if len(st) == 0:
+ return [None, None]
+ if num >= len(st):
+ num = -1
+ return st[num]
+
+def book_parse(book, name):
+ if not book:
+ return None
+ cnt = 0
+ while len(name) and name[-1] == '.':
+ cnt += 1
+ name = name[0:-1]
+ return book_lookup(book, name, cnt)
+
+
+
+def book_name(book, num):
+ if len(num) < 8:
+ return None
+ for ad in book:
+ if len(ad[1]) >= 8 and num[-8:] == ad[1][-8:]:
+ return ad
+ return None
+
+def book_speed(book, sym):
+ i = book_lookup(book, sym, 0)
+ if i[0] == None or i[0] != sym:
+ return None
+ j = book_lookup(book, i[1], 0)
+ if j[0] == None:
+ return (i[1], i[0])
+ return (j[1], j[0])
+
+def name_lookup(book, str):
+ # We need to report
+ # - a number - to dial
+ # - optionally a name that is associated with that number
+ # - optionally a new name to save the number as
+ # The name is normally alpha, but can be a single digit for
+ # speed-dial
+ # Dots following a name allow us to stop through multiple matches.
+ # So input can be:
+ # A single symbol.
+ # This is a speed dial. It maps to name, then number
+ # A string of >1 digits
+ # This is a literal number, we look up name if we can
+ # A string of dots
+ # This is a look up against recent incoming calls
+ # We look up name in phone book
+ # A string starting with alpha, possibly ending with dots
+ # This is a regular lookup in the phone book
+ # A number followed by a string
+ # This provides the string as a new name for saving
+ # A string of dots followed by a string
+ # This also provides the string as a newname
+ # An alpha string, with dots, followed by '+'then a single symbol
+ # This saves the match as a speed dial
+ #
+ # We return a triple of (number,oldname,newname)
+ if re.match('^[A-Za-z0-9]$', str):
+ # Speed dial lookup
+ s = book_speed(book, str)
+ if s:
+ return (s[0], s[1], None)
+ return None
+ m = re.match('^(\+?\d+)([A-Za-z][A-Za-z0-9 ]*)?$', str)
+ if m:
+ # Number and possible newname
+ s = book_name(book, m.group(1))
+ if s:
+ return (m.group(1), s[0], m.group(2))
+ else:
+ return (m.group(1), None, m.group(2))
+ m = re.match('^([A-Za-z][A-Za-z0-9 ]*)(\.*)(\+[A-Za-z0-9])?$', str)
+ if m:
+ # name and dots
+ speed = None
+ if m.group(3):
+ speed = m.group(3)[1]
+ i = book_lookup(book, m.group(1), len(m.group(2)))
+ if i[0]:
+ return (i[1], i[0], speed)
+ return None
+
+class SendSMS(gtk.Window):
+ def __init__(self, store):
+ gtk.Window.__init__(self)
+ self.set_default_size(480,640)
+ self.set_title("SendSMS")
+ self.store = store
+ self.connect('destroy', self.close_win)
+
+ self.selecting = False
+ self.viewing = False
+ self.book = None
+ self.create_ui()
+
+ self.show()
+ self.reload_book = True
+ self.number = None
+ self.cutbuffer = None
+
+ d = dnotify.dir(store.dirname)
+ self.watcher = d.watch('newmesg', lambda f : self.got_new())
+
+ self.watch_clip('sms-new')
+
+ self.connect('property-notify-event', self.newprop)
+ self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK)
+ def newprop(self, w, ev):
+ if ev.atom == '_INPUT_TEXT':
+ str = self.window.property_get('_INPUT_TEXT')
+ self.numentry.set_text(str[2])
+
+ def close_win(self, *a):
+ # FIXME save draft
+ gtk.main_quit()
+
+ def create_ui(self):
+
+ fd = pango.FontDescription("sans 10")
+ fd.set_absolute_size(25*pango.SCALE)
+ self.button_font = fd
+ v = gtk.VBox() ;v.show() ; self.add(v)
+
+ self.sender = self.send_ui()
+ v.add(self.sender)
+ self.sender.hide()
+
+ self.listing = self.list_ui()
+ v.add(self.listing)
+ self.listing.show()
+
+ self.book = load_book("/data/address-book")
+ self.listview.set_book(self.book)
+
+ self.rotate_list(self, target='All')
+
+ def send_ui(self):
+ v = gtk.VBox()
+ main = v
+
+ h = gtk.HBox()
+ h.show()
+ v.pack_start(h, expand=False)
+ l = gtk.Label('To:')
+ l.modify_font(self.button_font)
+ l.show()
+ h.pack_start(l, expand=False)
+
+ self.numentry = FingerEntry()
+ h.pack_start(self.numentry)
+ self.numentry.modify_font(self.button_font)
+ self.numentry.show()
+ self.numentry.connect('changed', self.update_to);
+
+ h = gtk.HBox()
+ l = gtk.Label('')
+ l.modify_font(self.button_font)
+ l.show()
+ h.pack_start(l)
+ h.show()
+ v.pack_start(h, expand=False)
+ h = gtk.HBox()
+ self.to_label = l
+ l = gtk.Label('0 chars')
+ l.modify_font(self.button_font)
+ l.show()
+ self.cnt_label = l
+ h.pack_end(l)
+ h.show()
+ v.pack_start(h, expand=False)
+
+ h = gtk.HBox()
+ h.set_size_request(-1,80)
+ h.set_homogeneous(True)
+ h.show()
+ v.pack_start(h, expand=False)
+
+ self.add_button(h, 'select', self.select)
+ self.add_button(h, 'clear', self.clear)
+ self.add_button(h, 'paste', self.paste)
+
+ self.message = FingerText()
+ self.message.show()
+ self.message.modify_font(self.button_font)
+ sw = gtk.ScrolledWindow() ; sw.show()
+ sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ #v.add(self.message)
+ v.add(sw)
+ sw.add(self.message)
+ self.message.get_buffer().connect('changed', self.buff_changed)
+
+ h = gtk.HBox()
+ h.set_size_request(-1,80)
+ h.set_homogeneous(True)
+ h.show()
+ v.pack_end(h, expand=False)
+
+ self.add_button(h, 'Send GSM', self.send, 'GSM')
+ self.draft_button = self.add_button(h, 'Draft', self.draft)
+ self.add_button(h, 'Send EXE', self.send, 'EXE')
+
+ return main
+
+ def list_ui(self):
+ v = gtk.VBox() ; main = v
+
+ h = gtk.HBox() ; h.show()
+ h.set_size_request(-1,80)
+ h.set_homogeneous(True)
+ v.pack_start(h, expand = False)
+ self.add_button(h, 'Del', self.delete)
+ self.view_button = self.add_button(h, 'View', self.view)
+ self.reply = self.add_button(h, 'New', self.open)
+
+ h = gtk.HBox() ; h.show()
+ h.set_size_request(-1,80)
+ h.set_homogeneous(True)
+ v.pack_end(h, expand=False)
+ self.buttonA = self.add_button(h, 'Sent', self.rotate_list, 'A')
+ self.buttonB = self.add_button(h, 'Recv', self.rotate_list, 'B')
+
+
+ self.last_response = gtk.Label('')
+ v.pack_end(self.last_response, expand = False)
+
+ h = gtk.HBox() ; h.show()
+ v.pack_start(h, expand=False)
+ b = gtk.Button("clr") ; b.show()
+ b.connect('clicked', self.clear_search)
+ h.pack_end(b, expand=False)
+ l = gtk.Label('search:') ; l.show()
+ h.pack_start(l, expand=False)
+
+ e = gtk.Entry() ; e.show()
+ self.search_entry = e
+ h.pack_start(e)
+
+ self.listview = SMSlist(self.load_list)
+ self.listview.show()
+ self.listview.assign_colour('time', 'blue')
+ self.listview.assign_colour('sender', 'red')
+ self.listview.assign_colour('recipient', 'black')
+ self.listview.assign_colour('mesg', 'black')
+ self.listview.assign_colour('mesg-new', 'red')
+ self.listview.assign_colour('bg-0', 'yellow')
+ self.listview.assign_colour('bg-1', 'pink')
+ self.listview.assign_colour('bg-selected', 'white')
+
+ v.add(self.listview)
+
+ self.singleview = gtk.TextView()
+ self.singleview.modify_font(self.button_font)
+ self.singleview.show()
+ self.singleview.set_wrap_mode(gtk.WRAP_WORD_CHAR)
+ sw = gtk.ScrolledWindow()
+ sw.add(self.singleview)
+ sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ sw.hide()
+ v.add(sw)
+ self.singlescroll = sw
+
+
+ main.show()
+ return main
+
+ def add_button(self, parent, label, func, *args):
+ b = gtk.Button(label)
+ b.child.modify_font(self.button_font)
+ b.connect('clicked', func, *args)
+ b.set_property('can-focus', False)
+ parent.add(b)
+ b.show()
+ return b
+
+ def update_to(self, w):
+ n = w.get_text()
+ if n == '':
+ self.reload_book = True
+ self.to_label.set_text('')
+ else:
+ if self.reload_book:
+ self.reload_book = False
+ self.book = load_book("/data/address-book")
+ self.listview.set_book(self.book)
+ e = name_lookup(self.book, n)
+ if e and e[1]:
+ self.to_label.set_text(e[1] +
+ ' '+
+ e[0])
+ self.number = e[0]
+ else:
+ self.to_label.set_text('??')
+ self.number = n
+ self.buff_changed(None)
+
+ def buff_changed(self, w):
+ if self.numentry.get_text() == '' and self.message.get_buffer().get_property('text') == '':
+ self.draft_button.child.set_text('Cancel')
+ else:
+ self.draft_button.child.set_text('SaveDraft')
+ l = len(self.message.get_buffer().get_property('text'))
+ if l <= 160:
+ m = 1
+ else:
+ m = (l+152)/153
+ self.cnt_label.set_text('%d chars / %d msgs' % (l, m))
+
+ def select(self, w, *a):
+ if not self.selecting:
+ self.message.input.active = False
+ w.child.set_text('Cut')
+ self.selecting = True
+ else:
+ self.message.input.active = True
+ w.child.set_text('Select')
+ self.selecting = False
+ b = self.message.get_buffer()
+ bound = b.get_selection_bounds()
+ if bound:
+ (s,e) = bound
+ t = b.get_text(s,e)
+ self.cutbuffer = t
+ b.delete_selection(True, True)
+
+ def clear(self, *a):
+ w = self.get_toplevel().get_focus()
+ if w == None:
+ w = self.message
+ if w == self.message:
+ self.cutbuffer = self.message.get_buffer().get_property('text')
+ b = self.message.get_buffer()
+ b.set_text('')
+ else:
+ self.cutbuffer = w.get_text()
+ w.set_text('')
+
+ def paste(self, *a):
+ w = self.get_toplevel().get_focus()
+ if w == None:
+ w = self.message
+ if self.cutbuffer:
+ w.insert_at_cursor(self.cutbuffer)
+ pass
+
+ def watch_clip(self, board):
+ self.cb = gtk.Clipboard(selection=board)
+ self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+ self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+
+ def got_clip(self, clipb, data):
+ a = clipb.wait_for_text()
+ print "sms got clip", a
+ self.numentry.set_text(a)
+ self.listing.hide()
+ self.sender.show()
+ self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+ self.present()
+
+ def get(self, sel, info, data):
+ sel.set_text("Number Please")
+
+ def send(self, w, style):
+ sender = '0403463349'
+ recipient = self.number
+ mesg = self.message.get_buffer().get_property('text')
+ if not mesg or not recipient:
+ return
+ try:
+ if style == 'EXE':
+ p = Popen(['exesms', sender, recipient, mesg], stdout = PIPE)
+ else:
+ p = Popen(['gsm-sms', sender, recipient, mesg], stdout = PIPE)
+ except:
+ rv = 1
+ line = 'Fork Failed'
+ else:
+ line = 'no response'
+ rv = p.wait()
+ for l in p.stdout:
+ if l:
+ line = l
+
+ s = SMSmesg(to = recipient, text = mesg)
+
+ if rv or line[0:2] != 'OK':
+ s.state = 'DRAFT'
+ target = 'Draft'
+ else:
+ target = 'All'
+ self.store.store(s)
+ self.last_response.set_text('Mess Send: '+ line.strip())
+ self.last_response.show()
+
+ self.sender.hide()
+ self.listing.show()
+ self.rotate_list(target=target)
+
+ def draft(self, *a):
+ sender = '0403463349'
+ recipient = self.numentry.get_text()
+ if recipient:
+ rl = [recipient]
+ else:
+ rl = []
+ mesg = self.message.get_buffer().get_property('text')
+ if mesg:
+ s = SMSmesg(to = recipient, text = mesg, state = 'DRAFT')
+ self.store.store(s)
+ self.sender.hide()
+ self.listing.show()
+ self.rotate_list(target='Draft')
+ def config(self, *a):
+ pass
+ def delete(self, *a):
+ if len(self.listview.smslist ) < 1:
+ return
+ s = self.listview.smslist[self.listview.selected]
+ self.store.delete(s)
+ sel = self.listview.selected
+ self.rotate_list(target=self.display_list)
+ self.listview.selected = sel
+ if self.viewing:
+ self.view(self.view_button)
+
+ def view(self, w, *a):
+ if self.viewing:
+ w.child.set_text('View')
+ self.viewing = False
+ self.singlescroll.hide()
+ self.listview.show()
+ if self.listview.smslist and len(self.listview.smslist ) >= 1:
+ s = self.listview.smslist[self.listview.selected]
+ if s.state == 'NEW':
+ self.store.setstate(s, None)
+ if self.display_list == 'New':
+ self.rotate_list(target='New')
+ self.reply.child.set_text('New')
+ else:
+ if not self.listview.smslist or len(self.listview.smslist ) < 1:
+ return
+ s = self.listview.smslist[self.listview.selected]
+ w.child.set_text('List')
+ self.viewing = True
+ self.last_response.hide()
+ self.listview.hide()
+ if self.book:
+ n = book_name(self.book, s.correspondent)
+ if n and n[0]:
+ n = n[0] + ' ['+s.correspondent+']'
+ else:
+ n = s.correspondent
+ else:
+ n = s.correspondent
+ if s.source == 'LOCAL':
+ t = 'To: ' + n + '\n'
+ else:
+ t = 'From: %s (%s)\n' % (n, s.source)
+ tm = time.strftime('%d%b%Y %H:%M:%S', s.time[0:6]+(0,0,0))
+ t += 'Time: ' + tm + '\n'
+ t += '\n'
+ t += s.text
+ self.singleview.get_buffer().set_text(t)
+ self.singlescroll.show()
+
+ if s.source == 'LOCAL':
+ self.reply.child.set_text('Open')
+ else:
+ self.reply.child.set_text('Reply')
+
+ def open(self, *a):
+ if self.viewing:
+ if len(self.listview.smslist) < 1:
+ return
+ s = self.listview.smslist[self.listview.selected]
+ if s.state == 'NEW':
+ self.store.setstate(s, None)
+
+ self.numentry.set_text(s.correspondent)
+ self.message.get_buffer().set_text(s.text)
+ self.draft_button.child.set_text('SaveDraft')
+ else:
+ self.numentry.set_text('')
+ self.message.get_buffer().set_text('')
+ self.draft_button.child.set_text('Cancel')
+ self.listing.hide()
+ self.sender.show()
+
+ def load_list(self, lines):
+ now = time.time()
+ l = []
+ target = self.display_list
+ patn = self.search_entry.get_text()
+ #print 'pattern is', patn
+ if target == 'New':
+ (now, l) = self.store.lookup(now, 'NEW')
+ elif target == 'Draft':
+ (now, l) = self.store.lookup(now, 'DRAFT')
+ else:
+ if lines == 0: lines = 20
+ while now and len(l) < lines:
+ (now, l2) = self.store.lookup(now)
+ for e in l2:
+ if patn and patn not in e.correspondent:
+ continue
+ if target == 'All':
+ l.append(e)
+ elif target == 'Sent' and e.source == 'LOCAL':
+ l.append(e)
+ elif target == 'Recv' and e.source != 'LOCAL':
+ l.append(e)
+ return l
+
+ def rotate_list(self, w=None, ev=None, which = None, target=None):
+ # lists are:
+ # All, Recv, New, Sent, Draft
+ # When one is current, two others can be selected
+
+ if target == None:
+ if w == None:
+ target = self.display_list
+ else:
+ target = w.child.get_text()
+
+ if target == 'All':
+ self.buttonA.child.set_text('Sent')
+ self.buttonB.child.set_text('Recv')
+ if target == 'Sent':
+ self.buttonA.child.set_text('All')
+ self.buttonB.child.set_text('Draft')
+ if target == 'Draft':
+ self.buttonA.child.set_text('All')
+ self.buttonB.child.set_text('Sent')
+ if target == 'Recv':
+ self.buttonA.child.set_text('All')
+ self.buttonB.child.set_text('New')
+ if target == 'New':
+ self.buttonA.child.set_text('All')
+ self.buttonB.child.set_text('Recv')
+
+ self.display_list = target
+ self.listview.reset_list()
+
+ def clear_search(self, *a):
+ pass
+
+ def got_new(self):
+ self.rotate_list(self, target = 'New')
+
+def main(args):
+ for p in ['/data','/media/card','/var/tmp']:
+ if os.path.exists(p):
+ pth = p
+ break
+ w = SendSMS(SMSstore(pth+'/SMS'))
+ gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main")
+
+ gtk.main()
+
+if __name__ == '__main__':
+ main(sys.argv)
--- /dev/null
+#
+# FIXME
+# - trim newmesg and draft when possible.
+# - remove old multipart files
+#
+# Store SMS messages is a bunch of files, one per month.
+# Each message is stored on one line with space separated .
+# URL encoding (%XX) is used to quote white space, unprintables etc
+# We store 5 fields:
+# - time stamp that we first saw the message. This is in UTC.
+# This is the primary key. If a second message is seen in the same second,
+# we quietly add 1 to the second.
+# - Source, one of 'LOCAL' for locally composed, 'GSM' for recieved via GSM
+# or maybe 'EMAIL' if received via email??
+# - Time message was sent, Localtime with -TZ. For GSM messages this comes with the
+# message. For 'LOCAL' it might be '-', or will be the time we succeeded
+# in sending.
+# time is stored as a tupple (Y m d H M S Z) where Z is timezone in multiples
+# of 15 minutes.
+# - The correspondent: sender if GSM, recipient if LOCAL, or '-' if not sent.
+# This might be a comma-separated list of recipients.
+# - The text of the message
+#
+# Times are formatted %Y%m%d-%H%M%S and local time has a GSM TZ suffix.
+# GSM TZ is from +48 to -48 in units of 15 minutes. (0 is +00)
+#
+# We never modify a message once it has been stored.
+# If we have a draft that we edit and send, we delete the draft and
+# create a new sent-message
+# If we forward a message, we will then have two copies.
+#
+# New messages are not distinguished by a flag (which would have to be cleared)
+# but by being in a separate list of new messages.
+# We havea list of 'new' messages and a list of 'draft' messages.
+#
+# Multi-part messages are accumulated as they are received. The quoted message
+# contains <N>text for each part of the message.
+# e.g. <1><2>nd%20so%20no.....<3>
+# if we only have part 2 of 3.
+# For each incomplete message there is a file (like 'draft' and 'newmesg') named
+# for the message which provides an index to each incomplete message.
+# It will be named e.g. 'multipart-1C' when 1C is the message id.
+#
+# This module defines 2 classes:
+# SMSmesg
+# This holds a message and so has timestamp, source, time, correspondent
+# and text fields. These are decoded.
+# SMSmesg also has 'state' which can be one of "NEW", "DRAFT" or None
+# Finally it might have a 'ref' and a 'part' which is a tuple (this,max)
+# This is only used when storing the message to link it up with
+# a partner
+#
+# SMSstore
+# This represents a collection of messages in a directory (one file per month)
+# and provides lists of 'NEW' and 'DRAFT' messages.
+# Operations:
+# store(SMSmesg, NEW|DRAFT|) -> None
+# stores the message and sets the timestamp
+# lookup(latest-time, NEW|DRAFT|ALL) -> (earlytime, [SMSmesg])
+# collects a list of messages in reverse time order with times no later
+# than 'latest-time'. Only consider NEW or DRAFT or ALL messages.
+# The list may not be complete (typically one month at a time are returnned)
+# If you want more, call again with 'earlytime' as 'latest-time').
+# delete(SMSmesg)
+# delete the given message (based on the timestamp only)
+# setstate(SMSmesg, NEW|DRAFT|None)
+# update the 'new' and 'draft' lists or container, or not container, this
+# message.
+#
+#
+
+import os, fcntl, re, time, urllib
+
+def umktime(tm):
+ # like time.mktime, but tm is UTC
+ # So we want a 't' where
+ # time.gmtime(t)[0:6] == tm[0:6]
+ estimate = time.mktime(tm) - time.timezone
+ t2 = time.gmtime(estimate)
+ while t2[0:6] < tm[0:6]:
+ estimate += 15*60
+ t2 = time.gmtime(estimate)
+ while t2[0:6] > tm[0:6]:
+ estimate -= 15*60
+ t2 = time.gmtime(estimate)
+ return estimate
+
+def parse_time(strg):
+ return int(umktime(time.strptime(strg, "%Y%m%d-%H%M%S")))
+def parse_ltime(strg):
+ n=3
+ if strg[-2] == '+' or strg[-2] == '-':
+ n=2
+ z = strg[-n:]
+ return time.strptime(strg[:-n], "%Y%m%d-%H%M%S")[0:6] + (int(z),)
+def format_time(t):
+ return time.strftime("%Y%m%d-%H%M%S", time.gmtime(t))
+def format_ltime(tm):
+ return time.strftime("%Y%m%d-%H%M%S", tm[0:6]+(0,0,0)) + ("%+03d" % tm[6])
+
+
+class SMSmesg:
+ def __init__(self, **a):
+ if len(a) == 1 and 'line' in a:
+ # line read from a file, with 5 fields.
+ # stamp, source, time, correspondent, text
+ line = a['line'].split()
+ self.stamp = parse_time(line[0])
+ self.source = line[1]
+ self.time = parse_ltime(line[2])
+ self.correspondents = []
+ for c in line[3].split(','):
+ if c != '-':
+ self.correspondents.append(urllib.unquote(c))
+ self.set_corresponent()
+ self.state = None
+
+ self.parts = None
+ txt = line[4]
+ if txt[0] != '<':
+ self.text = urllib.unquote(txt)
+ return
+ # multipart: <1>text...<2>text...<3><4>
+ m = re.findall('<(\d+)>([^<]*)', txt)
+ parts = []
+ for (pos, strg) in m:
+ p = int(pos)
+ while len(parts) < p:
+ parts.append(None)
+ if strg:
+ parts[p-1] = urllib.unquote(strg)
+ self.parts = parts
+ self.reduce_parts()
+ else:
+ self.stamp = int(time.time())
+ self.source = None
+ lt = time.localtime()
+ z = time.timezone/15/60
+ if lt[8] == 1:
+ z -= 4
+ self.time = time.localtime()[0:6] + (z,)
+ self.correspondents = []
+ self.text = ""
+ self.state = None
+ self.ref = None
+ self.parts = None
+ part = None
+ for k in a:
+ if k == 'stamp':
+ self.stamp = a[k]
+ elif k == 'source':
+ self.source = a[k]
+ elif k == 'time':
+ # time can be a GSM string: 09/02/09,09:56:28+44 (ymd,HMS+z)
+ # or a tuple (y,m,d,H,M,S,z)
+ if type(a[k]) == str:
+ t = a[k][:-3]
+ z = a[k][-3:]
+ tm = time.strptime(t, "%y/%m/%d,%H:%M:%S")
+ self.time = tm[0:6] + (int(z),)
+ elif k == 'to' or k == 'sender':
+ if self.source == None:
+ if k == 'to':
+ self.source = 'LOCAL'
+ if k == 'sender':
+ self.source = 'GSM'
+ self.correspondents = [ a[k] ]
+ elif k == 'correspondents':
+ self.correspondents = a[k]
+ elif k == 'text':
+ self.text = a[k]
+ elif k == 'state':
+ self.state = a[k]
+ elif k == 'ref':
+ if a[k] != None:
+ self.ref = a[k]
+ elif k == 'part':
+ if a[k]:
+ part = a[k]
+ else:
+ raise ValueError
+ if self.source == None:
+ self.source = 'LOCAL'
+ if part:
+ print 'part', part[0], part[1]
+ self.parts = [None for x in range(part[1])]
+ self.parts[part[0]-1] = self.text
+ self.reduce_parts()
+ self.set_corresponent()
+
+ self.month_re = re.compile("^[0-9]{6}$")
+
+ def reduce_parts(self):
+ def reduce_pair(a,b):
+ if b == None:
+ b = "...part of message missing..."
+ if a == None:
+ return b
+ return a+b
+ self.text = reduce(reduce_pair, self.parts)
+
+
+ def set_corresponent(self):
+ if len(self.correspondents) == 1:
+ self.correspondent = self.correspondents[0]
+ elif len(self.correspondents) == 0:
+ self.correspondent = "Unsent"
+ else:
+ self.correspondent = "Multiple"
+
+ def format(self):
+ fmt = "%s %s %s %s " % (format_time(self.stamp), self.source,
+ format_ltime(self.time),
+ self.format_correspondents())
+ if not self.parts:
+ return fmt + urllib.quote(self.text)
+
+ for i in range(len(self.parts)):
+ fmt += ("<%d>" % (i+1)) + urllib.quote(self.parts[i]) if self.parts[i] else ""
+ return fmt
+
+ def format_correspondents(self):
+ r = ""
+ for i in self.correspondents:
+ if i:
+ r += ',' + urllib.quote(i)
+ if r:
+ return r[1:]
+ else:
+ return '-'
+
+
+class SMSstore:
+ def __init__(self, dir):
+ self.month_re = re.compile("^[0-9]{6}$")
+ self.cached_month = None
+ self.dirname = dir
+ # find message files
+ self.set_files()
+ self.drafts = self.load_list('draft')
+ self.newmesg = self.load_list('newmesg')
+
+ def load_list(self, name, update = None, *args):
+
+ l = []
+ try:
+ f = open(self.dirname + '/' + name, 'r+')
+ except IOError:
+ return l
+
+ if update:
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ for ln in f:
+ l.append(parse_time(ln.strip()))
+ l.sort()
+ l.reverse()
+
+ if update and update(l, *args):
+ f2 = open(self.dirname + '/' + name + '.new', 'w')
+ for t in l:
+ f2.write(format_time(t)+"\n")
+ f2.close()
+ os.rename(self.dirname + '/' + name + '.new',
+ self.dirname + '/' + name)
+ f.close()
+ return l
+
+ def load_month(self, f):
+ # load the messages from f, which is open for read
+ rv = {}
+ for l in f:
+ l.strip()
+ m = SMSmesg(line=l)
+ rv[m.stamp] = m
+ if m.stamp in self.drafts:
+ m.state = 'DRAFT'
+ elif m.stamp in self.newmesg:
+ m.state = 'NEW'
+ return rv
+
+ def store_month(self, l, m):
+ dm = self.dirname + '/' + m
+ f = open(dm+'.new', 'w')
+ for s in l:
+ f.write(l[s].format() + "\n")
+ f.close()
+ os.rename(dm+'.new', dm)
+ if not m in self.files:
+ self.files.append(m)
+ self.files.sort()
+ self.files.reverse()
+ if self.cached_month == m:
+ self.cache = l
+
+ def store(self, sms):
+ orig = None
+ if sms.ref != None:
+ # This is part of a multipart.
+ # If there already exists part of this
+ # merge them together
+ #
+ times = self.load_list('multipart-' + sms.ref)
+ if len(times) == 1:
+ orig = self.load(times[0])
+ if orig and orig.parts:
+ for i in range(len(sms.parts)):
+ if sms.parts[i] == None and i < len(orig.parts):
+ sms.parts[i] = orig.parts[i]
+ else:
+ orig = None
+
+ m = time.strftime("%Y%m", time.gmtime(sms.stamp))
+ try:
+ f = open(self.dirname + '/' + m, "r+")
+ except:
+ f = open(self.dirname + '/' + m, "w+")
+ complete = True
+ if sms.ref != None:
+ for i in sms.parts:
+ if i == None:
+ complete = False
+ if complete:
+ sms.reduce_parts()
+ sms.parts = None
+
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ l = self.load_month(f)
+ while sms.stamp in l:
+ sms.stamp += 1
+ l[sms.stamp] = sms
+ self.store_month(l, m);
+ f.close()
+
+ if orig:
+ self.delete(orig)
+ if sms.ref != None:
+ if complete:
+ try:
+ os.unlink(self.dirname + '/multipart-' + sms.ref)
+ except:
+ pass
+ elif orig:
+ def replacewith(l, tm):
+ while len(l):
+ l.pop()
+ l.append(tm)
+ return True
+ self.load_list('multipart-' + sms.ref, replacewith, sms.stamp)
+ else:
+ f = open(self.dirname +'/multipart-' + sms.ref, 'w')
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ f.write(format_time(sms.stamp) + '\n')
+ f.close()
+
+ if sms.state == 'NEW' or sms.state == 'DRAFT':
+ s = 'newmesg'
+ if sms.state == 'DRAFT':
+ s = 'draft'
+ f = open(self.dirname +'/' + s, 'a')
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ f.write(format_time(sms.stamp) + '\n')
+ f.close()
+ elif sms.state != None:
+ raise ValueError
+
+ def set_files(self):
+ self.files = []
+ for f in os.listdir(self.dirname):
+ if self.month_re.match(f):
+ self.files.append(f)
+ self.files.sort()
+ self.files.reverse()
+
+ def lookup(self, lasttime = None, state = None):
+ if lasttime == None:
+ lasttime = int(time.time())
+ if state == None:
+ return self.getmesgs(lasttime)
+ if state == 'DRAFT':
+ self.drafts = self.load_list('draft')
+ times = self.drafts
+ elif state == 'NEW':
+ self.newmesg = self.load_list('newmesg')
+ times = self.newmesg
+ else:
+ raise ValueError
+
+ self.set_files()
+ self.cached_month = None
+ self.cache = None
+ rv = []
+ for t in times:
+ if t > lasttime:
+ continue
+ s = self.load(t)
+ if s:
+ s.state = state
+ rv.append(s)
+ return(0, rv)
+
+ def getmesgs(self, last):
+ rv = []
+ for m in self.files:
+ t = parse_time(m + '01-000000')
+ if t > last:
+ continue
+ mon = self.load_month(open(self.dirname + '/' + m))
+ for mt in mon:
+ if mt <= last:
+ rv.append(mon[mt])
+ if rv:
+ rv.sort(cmp = lambda x,y:cmp(y.stamp, x.stamp))
+ return (t-1, rv)
+ return (0, [])
+
+ def load(self, t):
+ m = time.strftime("%Y%m", time.gmtime(t))
+ if not m in self.files:
+ return None
+ if self.cached_month != m:
+ self.cached_month = m
+ self.cache = self.load_month(open(self.dirname + '/' + m))
+ if t in self.cache:
+ return self.cache[t]
+ return None
+
+ def delete(self, msg):
+ if isinstance(msg, SMSmesg):
+ tm = msg.stamp
+ else:
+ tm = msg
+ m = time.strftime("%Y%m", time.gmtime(tm))
+ try:
+ f = open(self.dirname + '/' + m, "r+")
+ except:
+ return
+
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ l = self.load_month(f)
+ if tm in l:
+ del l[tm]
+ self.store_month(l, m);
+ f.close()
+
+ def del1(l, tm):
+ if tm in l:
+ l.remove(tm)
+ return True
+ return False
+
+ self.drafts = self.load_list('draft', del1, tm)
+ self.newmesg = self.load_list('newmesg', del1, tm)
+
+ def setstate(self, msg, state):
+ tm = msg.stamp
+
+ def del1(l, tm):
+ if tm in l:
+ l.remove(tm)
+ return True
+ return False
+
+ if tm in self.drafts and state != 'DRAFT':
+ self.drafts = self.load_list('draft', del1, tm)
+ if tm in self.newmesg and state != 'NEW':
+ self.newmesg = self.load_list('newmesg', del1, tm)
+
+ if tm not in self.drafts and state == 'DRAFT':
+ f = open(self.dirname +'/draft', 'a')
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ f.write(format_time(sms.stamp) + '\n')
+ f.close()
+ self.drafts.append(tm)
+ self.drafts.sort()
+ self.drafts.reverse()
+
+ if tm not in self.newmesg and state == 'NEW':
+ f = open(self.dirname +'/newmesg', 'a')
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ f.write(format_time(sms.stamp) + '\n')
+ f.close()
+ self.newmesg.append(tm)
+ self.newmesg.sort()
+ self.newmesg.reverse()
+
+
--- /dev/null
+/*
+ * Copied from the Linux kernel source tree, version 2.6.0-test1.
+ *
+ * Licensed under the GPL v2 as per the whole kernel source tree.
+ *
+ */
+
+#ifndef _LIST_H
+#define _LIST_H
+
+/**
+ * container_of - cast a member of a structure out to the containing structure
+ *
+ * @ptr: the pointer to the member.
+ * @type: the type of the container struct this is embedded in.
+ * @member: the name of the member within the struct.
+ *
+ */
+#define container_of(ptr, type, member) ({ \
+ const typeof( ((type *)0)->member ) *__mptr = (ptr); \
+ (type *)( (char *)__mptr - offsetof(type,member) );})
+
+/*
+ * These are non-NULL pointers that will result in page faults
+ * under normal circumstances, used to verify that nobody uses
+ * non-initialized list entries.
+ */
+#define LIST_POISON1 ((void *) 0x00100100)
+#define LIST_POISON2 ((void *) 0x00200200)
+
+/*
+ * Simple doubly linked list implementation.
+ *
+ * Some of the internal functions ("__xxx") are useful when
+ * manipulating whole lists rather than single entries, as
+ * sometimes we already know the next/prev entries and we can
+ * generate better code by using them directly rather than
+ * using the generic single-entry routines.
+ */
+
+struct list_head {
+ struct list_head *next, *prev;
+};
+
+#define LIST_HEAD_INIT(name) { &(name), &(name) }
+
+#define LIST_HEAD(name) \
+ struct list_head name = LIST_HEAD_INIT(name)
+
+#define INIT_LIST_HEAD(ptr) do { \
+ (ptr)->next = (ptr); (ptr)->prev = (ptr); \
+} while (0)
+
+/*
+ * Insert a new entry between two known consecutive entries.
+ *
+ * This is only for internal list manipulation where we know
+ * the prev/next entries already!
+ */
+static inline void __list_add(struct list_head *new,
+ struct list_head *prev,
+ struct list_head *next)
+{
+ next->prev = new;
+ new->next = next;
+ new->prev = prev;
+ prev->next = new;
+}
+
+/**
+ * list_add - add a new entry
+ * @new: new entry to be added
+ * @head: list head to add it after
+ *
+ * Insert a new entry after the specified head.
+ * This is good for implementing stacks.
+ */
+static inline void list_add(struct list_head *new, struct list_head *head)
+{
+ __list_add(new, head, head->next);
+}
+
+/**
+ * list_add_tail - add a new entry
+ * @new: new entry to be added
+ * @head: list head to add it before
+ *
+ * Insert a new entry before the specified head.
+ * This is useful for implementing queues.
+ */
+static inline void list_add_tail(struct list_head *new, struct list_head *head)
+{
+ __list_add(new, head->prev, head);
+}
+
+/*
+ * Delete a list entry by making the prev/next entries
+ * point to each other.
+ *
+ * This is only for internal list manipulation where we know
+ * the prev/next entries already!
+ */
+static inline void __list_del(struct list_head * prev, struct list_head * next)
+{
+ next->prev = prev;
+ prev->next = next;
+}
+
+/**
+ * list_del - deletes entry from list.
+ * @entry: the element to delete from the list.
+ * Note: list_empty on entry does not return true after this, the entry is
+ * in an undefined state.
+ */
+static inline void list_del(struct list_head *entry)
+{
+ __list_del(entry->prev, entry->next);
+ entry->next = LIST_POISON1;
+ entry->prev = LIST_POISON2;
+}
+
+/**
+ * list_del_init - deletes entry from list and reinitialize it.
+ * @entry: the element to delete from the list.
+ */
+static inline void list_del_init(struct list_head *entry)
+{
+ __list_del(entry->prev, entry->next);
+ INIT_LIST_HEAD(entry);
+}
+
+/**
+ * list_move - delete from one list and add as another's head
+ * @list: the entry to move
+ * @head: the head that will precede our entry
+ */
+static inline void list_move(struct list_head *list, struct list_head *head)
+{
+ __list_del(list->prev, list->next);
+ list_add(list, head);
+}
+
+/**
+ * list_move_tail - delete from one list and add as another's tail
+ * @list: the entry to move
+ * @head: the head that will follow our entry
+ */
+static inline void list_move_tail(struct list_head *list,
+ struct list_head *head)
+{
+ __list_del(list->prev, list->next);
+ list_add_tail(list, head);
+}
+
+/**
+ * list_empty - tests whether a list is empty
+ * @head: the list to test.
+ */
+static inline int list_empty(struct list_head *head)
+{
+ return head->next == head;
+}
+
+static inline void __list_splice(struct list_head *list,
+ struct list_head *head)
+{
+ struct list_head *first = list->next;
+ struct list_head *last = list->prev;
+ struct list_head *at = head->next;
+
+ first->prev = head;
+ head->next = first;
+
+ last->next = at;
+ at->prev = last;
+}
+
+/**
+ * list_splice - join two lists
+ * @list: the new list to add.
+ * @head: the place to add it in the first list.
+ */
+static inline void list_splice(struct list_head *list, struct list_head *head)
+{
+ if (!list_empty(list))
+ __list_splice(list, head);
+}
+
+/**
+ * list_splice_init - join two lists and reinitialise the emptied list.
+ * @list: the new list to add.
+ * @head: the place to add it in the first list.
+ *
+ * The list at @list is reinitialised
+ */
+static inline void list_splice_init(struct list_head *list,
+ struct list_head *head)
+{
+ if (!list_empty(list)) {
+ __list_splice(list, head);
+ INIT_LIST_HEAD(list);
+ }
+}
+
+/**
+ * list_entry - get the struct for this entry
+ * @ptr: the &struct list_head pointer.
+ * @type: the type of the struct this is embedded in.
+ * @member: the name of the list_struct within the struct.
+ */
+#define list_entry(ptr, type, member) \
+ container_of(ptr, type, member)
+
+/**
+ * list_for_each - iterate over a list
+ * @pos: the &struct list_head to use as a loop counter.
+ * @head: the head for your list.
+ */
+#define list_for_each(pos, head) \
+ for (pos = (head)->next; pos != (head); \
+ pos = pos->next)
+
+/**
+ * __list_for_each - iterate over a list
+ * @pos: the &struct list_head to use as a loop counter.
+ * @head: the head for your list.
+ *
+ * This variant differs from list_for_each() in that it's the
+ * simplest possible list iteration code.
+ * Use this for code that knows the list to be very short (empty
+ * or 1 entry) most of the time.
+ */
+#define __list_for_each(pos, head) \
+ for (pos = (head)->next; pos != (head); pos = pos->next)
+
+/**
+ * list_for_each_prev - iterate over a list backwards
+ * @pos: the &struct list_head to use as a loop counter.
+ * @head: the head for your list.
+ */
+#define list_for_each_prev(pos, head) \
+ for (pos = (head)->prev; pos != (head); pos = pos->prev)
+
+/**
+ * list_for_each_safe - iterate over a list safe against removal of list entry
+ * @pos: the &struct list_head to use as a loop counter.
+ * @n: another &struct list_head to use as temporary storage
+ * @head: the head for your list.
+ */
+#define list_for_each_safe(pos, n, head) \
+ for (pos = (head)->next, n = pos->next; pos != (head); \
+ pos = n, n = pos->next)
+
+/**
+ * list_for_each_entry - iterate over list of given type
+ * @pos: the type * to use as a loop counter.
+ * @head: the head for your list.
+ * @member: the name of the list_struct within the struct.
+ */
+#define list_for_each_entry(pos, head, member) \
+ for (pos = list_entry((head)->next, typeof(*pos), member); \
+ &pos->member != (head); \
+ pos = list_entry(pos->member.next, typeof(*pos), member))
+
+/**
+ * list_for_each_entry_reverse - iterate backwards over list of given type.
+ * @pos: the type * to use as a loop counter.
+ * @head: the head for your list.
+ * @member: the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_reverse(pos, head, member) \
+ for (pos = list_entry((head)->prev, typeof(*pos), member); \
+ &pos->member != (head); \
+ pos = list_entry(pos->member.prev, typeof(*pos), member))
+
+/**
+ * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
+ * @pos: the type * to use as a loop counter.
+ * @n: another type * to use as temporary storage
+ * @head: the head for your list.
+ * @member: the name of the list_struct within the struct.
+ */
+#define list_for_each_entry_safe(pos, n, head, member) \
+ for (pos = list_entry((head)->next, typeof(*pos), member), \
+ n = list_entry(pos->member.next, typeof(*pos), member); \
+ &pos->member != (head); \
+ pos = n, n = list_entry(n->member.next, typeof(*n), member))
+
+#endif /* _LIST_H */
--- /dev/null
+
+ snd_config_t *config;
+ file = "/data/senarios/$CHOICE";
+ err = snd_config_top(&config);
+ err = snd_input_stdio_open(&in, file, "r");
+ err = snd_config_load(config, in);
+ snd_input_close(in);
+ cardno = snd_card_get_index(cardname);
+
+ char name[32];
+ snd_ctl_t *handle;
+ snd_ctl_card_info_t *info;
+ snd_ctl_card_info_alloca(&info);
+ sprintf(name, "hw:%d", cardno);
+ err = snd_ctl_open(&handle, name, 0);
+ err = snd_ctl_card_info(handle, info);
+ id = snd_ctl_card_info_get_id(info);
+ err = snd_config_searchv(config, &control, "state", id, "control", 0);
+ snd_config_for_each(i, next, control) {
+ snd_config_t *n = snd_config_iterator_entry(i);
+ snd_ctl_elem_value_alloca(&ctl);
+ snd_ctl_elem_info_alloca(&info);
+ err = snd_config_get_id(control, &id);
+
+
+ err = set_control(handle, n);
+ if (err < 0 && ! force_restore)
+ goto _close;
+ }
+
+
+-------------
+
+Enhancements:
+ want
+ volume control
+ route to bluetooth instead
+ seek, and restart a second or two back
+ route between GSM and main/bluetooth
+ record input - set samples etc?
+ mix samples with routing
+
+separate directory.
+When file appears there, recording is started
+If a symlink appears that names an alsa device, mix the
+recording in to that device. It should also be echo-cancelled
+with the output if enabled for the devices
+(we enable for main and bt)
+
+Cards are: gta04 gta04voice gta04headset (CARD=gta04).
+
+need 'plug:' to ensure sample rate conversion etc.
+need 'dmix:' to allow other sounds to play.
+
+
+We can have multiple devices open at once, which might use
+dmix to output to the one device.
+Each 'record' device can be paired with a 'play' device.
+A record and a play can be marked for echo cancelling.
+This mixes the 'play' stream into the 'record' stream so that
+the echo disappears from it. For this to work, the 'record'
+stream must be nearly synced with the 'play' stream.
+There is no point having 'play' samples which haven't gone out yet
+as they cannot have echo. Rather I want the samples that I just
+played to mix with the samples that I just recored.
+
+However gsm-voice-routing doesn't do that. It mixes the 'just
+recorded' on each path.??
+
+I need to work in whole periods and I would like these to be
+closely synced... Can I do that?
+Not easily. I just need to keep the periods low so the sync difference
+is small.
+
+gsm-voice-routing uses a period_size of 256 of 32msec.
+oslec say 16ms or 32ms tail is fine. 64ms would cope with
+out-of-sync buffers.
+
+So...
+ all processing must be async
+ several sounds with same priority can play at once
+ a sound file which contains 'gsm' triggers copying between
+ gta04voice and current device.
+ We start recording and don't start playing until a sample
+ is available.
+
+So the active things can be:
+ 'sound' which has a plug:dmix: device it is being written to
+ 'mic' which is the souce on the main device
+ 'gsmout' which 'mic' is being copied to
+ 'gsmin' which is being read in
+ 'speaker' which is another plug:dmix:
+
+So we can happily keep the voice routing in a separate process
+and still allow mixing.
+
+
+
+Copying needs to wait for 'voice' to start delivering data.
+So:
+ - configure all stream.
+ -
\ No newline at end of file
--- /dev/null
+/* TOFIX
+ * Record where up to - and total length
+ * Don't reconfigure between identical format sounds.
+ * handle seek for ogg vorbis
+ * ??handle MP3 in WAV files
+ * long gap between sounds.??? maybe all those zeros?
+ */
+
+/*
+ *
+ * This is a daemon that waits for sound files to appear in a particular
+ * directory, and when they do, it plays them.
+ * Files can be WAV or OGG VORBIS
+ * If there are multiple files, the lexically first is played
+ * If a file has a suffix of -NNNNNNN, then play starts that many
+ * milliseconds in to the file.
+ * When a file disappear, play stops.
+ * When the end of the sound is reached the file (typically a link) is removed.
+ * However an empty file is treated as containing infinite silence, so
+ * it is never removed.
+ * When a new file appears which is lexically earlier than the one being
+ * played, the played file is suspended until the earlier files are finished
+ * with.
+ * The current-play position (in milliseconds) is occasionally written to a file
+ * with the same name as the sound file, but with a leading period.
+ * This file starts with 'P' if the sound is playing, and 'S' if it has
+ * stopped. When playing, the actual position can be calculated using
+ * the current time and mtime of the file.
+ *
+ * Expected use is that various alert tones are added to the directory with
+ * early names, and a music file can be added with a later name for general
+ * listening.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <malloc.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <alsa/asoundlib.h>
+#include <sys/signal.h>
+#include <sys/dir.h>
+#include <asm/byteorder.h>
+#include <vorbis/vorbisfile.h>
+#include <event.h>
+
+#include "list.h"
+#include "libsus.h"
+
+#define WAV_RIFF COMPOSE_ID('R','I','F','F')
+#define WAV_WAVE COMPOSE_ID('W','A','V','E')
+#define WAV_FMT COMPOSE_ID('f','m','t',' ')
+#define WAV_DATA COMPOSE_ID('d','a','t','a')
+#define WAV_PCM_CODE 1
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define COMPOSE_ID(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24))
+#define LE_SHORT(v) (v)
+#define LE_INT(v) (v)
+#define BE_SHORT(v) bswap_16(v)
+#define BE_INT(v) bswap_32(v)
+#elif __BYTE_ORDER == __BIG_ENDIAN
+#define COMPOSE_ID(a,b,c,d) ((d) | ((c)<<8) | ((b)<<16) | ((a)<<24))
+#define LE_SHORT(v) bswap_16(v)
+#define LE_INT(v) bswap_32(v)
+#define BE_SHORT(v) (v)
+#define BE_INT(v) (v)
+#else
+#error "Wrong endian"
+#endif
+
+typedef struct {
+ u_int magic; /* 'RIFF' */
+ u_int length; /* filelen */
+ u_int type; /* 'WAVE' */
+} WaveHeader;
+
+typedef struct {
+ u_short format; /* should be 1 for PCM-code */
+ u_short modus; /* 1 Mono, 2 Stereo */
+ u_int sample_fq; /* frequence of sample */
+ u_int byte_p_sec;
+ u_short byte_p_spl; /* samplesize; 1 or 2 bytes */
+ u_short bit_p_spl; /* 8, 12 or 16 bit */
+} WaveFmtBody;
+
+typedef struct {
+ u_int type; /* 'data' or 'fmt ' */
+ u_int length; /* samplecount */
+} WaveChunkHeader;
+
+
+#ifndef LLONG_MAX
+#define LLONG_MAX 9223372036854775807LL
+#endif
+
+#define DEFAULT_FORMAT SND_PCM_FORMAT_U8
+#define DEFAULT_SPEED 8000
+
+#define FORMAT_DEFAULT -1
+#define FORMAT_RAW 0
+#define FORMAT_VOC 1
+#define FORMAT_WAVE 2
+#define FORMAT_AU 3
+#define FORMAT_VORBIS 4
+#define FORMAT_UNKNOWN 9999
+/* global data */
+
+
+struct sound {
+ int fd;
+ int empty;
+ struct list_head list;
+ int seen;
+ char *name;
+ char scenario[20];
+ int ino;
+ long posn;
+ int format; /* FORMAT_WAVE or FORMAT_OGG */
+ char buf[8192];
+ int period_bytes;
+ int bytes, bytes_used, last_read;
+ int eof;
+
+ /* An audio file can contain multiple chunks.
+ * Here we record the remaining bytes expected
+ * before a header
+ */
+ int chunk_bytes;
+
+ OggVorbis_File vf;
+
+ int pcm_format;
+ int sample_bytes;
+ int channels;
+ int rate;
+};
+
+struct dev {
+ snd_pcm_t *handle;
+ char *period_buf;
+ char scenario[20];
+ int buf_size;
+ int period_bytes;
+ int sample_bytes;
+ int present;
+ int pcm_format, channels, rate;
+};
+
+int dir_changed = 1;
+
+void handle_change(int sig)
+{
+ dir_changed = 1;
+}
+
+static void *raw_read(struct sound *s, int bytes)
+{
+ /* Return pointer to next 'bytes' bytes in the buffer.
+ * If there aren't that many, copy down and read some
+ * more. If we hit EOF, return NULL and set ->eof.
+ */
+ while (s->bytes - s->bytes_used < bytes && !s->eof) {
+ if (s->bytes_used &&
+ s->bytes_used < s->bytes)
+ memmove(s->buf, s->buf+s->bytes_used,
+ s->bytes - s->bytes_used);
+ s->bytes -= s->bytes_used;
+ s->bytes_used = 0;
+ while (s->bytes < bytes && !s->eof) {
+ int n = read(s->fd, s->buf+s->bytes, sizeof(s->buf) - s->bytes);
+ if (n <= 0)
+ s->eof = 1;
+ else
+ s->bytes += n;
+ }
+ }
+ if (s->bytes - s->bytes_used < bytes)
+ return NULL;
+ else {
+ void *rv = s->buf + s->bytes_used;
+ s->bytes_used += bytes;
+ s->last_read = bytes;
+ return rv;
+ }
+}
+
+static void raw_unread(struct sound *s)
+{
+ s->bytes_used -= s->last_read;
+ s->last_read = 0;
+}
+
+static void raw_skip(struct sound *s, int bytes)
+{
+ if (s->bytes - s->bytes_used >= bytes) {
+ s->bytes_used += bytes;
+ return;
+ }
+ bytes -= (s->bytes - s->bytes_used);
+ s->bytes = 0;
+ s->bytes_used = 0;
+ lseek(s->fd, bytes, SEEK_CUR);
+}
+
+
+int parse_wave(struct sound *s)
+{
+ WaveHeader *h;
+ WaveChunkHeader *c;
+ WaveFmtBody *f;
+ int n;
+ int len;
+
+ h = raw_read(s, sizeof(*h));
+ if (!h)
+ return 0;
+ if (h->magic != WAV_RIFF || h->type != WAV_WAVE) {
+ raw_unread(s);
+ return 0;
+ }
+ /* Ignore the length - wait for EOF */
+ while (1) {
+ c = raw_read(s, sizeof(*c));
+ if (!c)
+ return 0;
+ len = LE_INT(c->length);
+ len += len % 2;
+ if (c->type == WAV_FMT)
+ break;
+ raw_skip(s, len);
+ }
+ if (len < sizeof(WaveFmtBody))
+ return 0;
+ f = raw_read(s, len);
+ if (!f)
+ return 0;
+
+ if (LE_SHORT(f->format) == 1 &&
+ LE_SHORT(f->bit_p_spl) == 16) {
+ s->pcm_format = SND_PCM_FORMAT_S16_LE;
+ s->sample_bytes = 2;
+ } else if (LE_SHORT(f->format) == 1 &&
+ LE_SHORT(f->bit_p_spl) == 8) {
+ s->pcm_format = SND_PCM_FORMAT_U8;
+ s->sample_bytes = 1;
+ } else
+ return 0;
+ s->channels = LE_SHORT(f->modus);
+ if (s->channels < 1 || s->channels > 2)
+ return 0;
+ s->sample_bytes *= s->channels;
+ s->rate = LE_INT(f->sample_fq);
+ s->eof = 0;
+ s->chunk_bytes = 0;
+ return 1;
+}
+
+static int read_wave(struct sound *s, char *buf, int bytes)
+{
+ WaveChunkHeader *h;
+ int len;
+ int b = 0;
+ char *rbuf;
+ int i;
+
+ while (b < bytes) {
+ while (s->chunk_bytes == 0) {
+ h = raw_read(s, sizeof(*h));
+ if (!h)
+ break;
+ len = LE_INT(h->length);
+ len += len % 2;
+ if (h->type != WAV_DATA) {
+ raw_skip(s, len);
+ continue;
+ }
+ s->chunk_bytes = len;
+ }
+ if (s->chunk_bytes == 0)
+ break;
+ if (s->chunk_bytes >= bytes - b) {
+ rbuf = raw_read(s, bytes - b);
+ if (!rbuf)
+ break;
+ s->chunk_bytes -= bytes - b;
+ memcpy(buf + b, rbuf, bytes - b);
+ return bytes;
+ }
+ /* Remainder of chunk is less than we need */
+ rbuf = raw_read(s, s->chunk_bytes);
+ if (!rbuf)
+ break;
+ memcpy(buf + b, rbuf, s->chunk_bytes);
+ b += s->chunk_bytes;
+ s->chunk_bytes = 0;
+ }
+
+ rbuf = buf;
+ for (i = b; i < bytes && s->chunk_bytes > 1; i++) {
+ char *bf = raw_read(s, 1);
+ if (bf)
+ buf[i] = bf[0];
+ else
+ break;
+ s->chunk_bytes--;
+ }
+ return i;
+}
+
+void seek_wave(struct sound *s, int msec)
+{
+ /* skip over 'msec' milliseconds of the wave */
+ WaveChunkHeader *h;
+ int bytes = ((long long)msec * s->rate / 1000) * s->sample_bytes;
+ int len;
+
+ while (bytes) {
+ while (s->chunk_bytes == 0) {
+ h = raw_read(s, sizeof(*h));
+ if (!h)
+ break;
+ len = LE_INT(h->length);
+ len += len % 2;
+ if (h->type != WAV_DATA) {
+ raw_skip(s, len);
+ continue;
+ }
+ s->chunk_bytes = len;
+ }
+ if (s->chunk_bytes == 0)
+ break;
+ if (s->chunk_bytes >= bytes) {
+ raw_skip(s, bytes);
+ s->chunk_bytes -= bytes;
+ return;
+ }
+ raw_skip(s, s->chunk_bytes);
+ bytes -= s->chunk_bytes;
+ s->chunk_bytes = 0;
+ }
+}
+
+static int parse_vorbis(struct sound *s)
+{
+ FILE *f;
+ vorbis_info *vi;
+ int rv;
+
+ lseek(s->fd, 0, 0);
+ f = fdopen(s->fd, "r");
+ if ((rv = ov_open_callbacks(f, &s->vf, NULL, 0, OV_CALLBACKS_DEFAULT)) < 0) {
+ fclose(f);
+ return 0;
+ }
+
+ vi = ov_info(&s->vf,-1);
+
+ s->pcm_format = SND_PCM_FORMAT_S16_LE;
+ s->channels = vi->channels;
+ s->rate = vi->rate;
+ s->sample_bytes = 2 * s->channels;
+ s->eof = 0;
+ return 1;
+}
+
+static int read_vorbis(struct sound *s, char *buf, int len)
+{
+ int section;
+ int have = 0;
+ if (s->eof)
+ return 0;
+
+ while(!s->eof && have < len) {
+ long ret = ov_read(&s->vf,
+ buf + have,
+ len - have,
+ 0,2,1,§ion);
+ if (ret == 0) {
+ /* EOF */
+ s->eof=1;
+ } else if (ret < 0) {
+ /* error in the stream. Not a problem, just reporting it in
+ case we (the app) cares. In this case, we don't. */
+ s->eof = 1;
+ } else {
+ /* we don't bother dealing with sample rate changes, etc, but
+ you'll have to*/
+ have += ret;
+ }
+ }
+
+ if (s->eof)
+ ov_clear(&s->vf);
+ return have;
+}
+
+static void seek_vorbis(struct sound *s, int msec)
+{
+}
+
+
+snd_pcm_t *open_dev(void)
+{
+ snd_pcm_t *handle;
+ int rc;
+
+ rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0/*SND_PCM_NONBLOCK*/);
+ if (rc < 0)
+ return NULL;
+ else
+ return handle;
+}
+
+void dev_close(struct dev *dev);
+void set_scenario(struct dev *dev, char *scenario)
+{
+ char path[100];
+ if (scenario[0] == 0)
+ return;
+ if (strcmp(dev->scenario, scenario) == 0)
+ return;
+ dev_close(dev);
+ strcpy(dev->scenario, scenario);
+ snprintf(path, 100, "alsactl -f /data/scenarios/%s restore", scenario);
+ system(path);
+}
+
+void set_params(struct dev *dev, struct sound *sound)
+{
+ snd_pcm_hw_params_t *hwp;
+
+ if (sound->format == FORMAT_UNKNOWN)
+ return;
+
+ set_scenario(dev, sound->scenario);
+ if (dev->handle == NULL) {
+ dev->handle = open_dev();
+ dev->pcm_format = 0;
+ }
+
+ if (dev->pcm_format == sound->pcm_format &&
+ dev->channels == sound->channels &&
+ dev->rate == sound->rate)
+ return;
+
+ if (dev->pcm_format)
+ snd_pcm_drop(dev->handle);
+
+ snd_pcm_hw_params_alloca(&hwp);
+ snd_pcm_hw_params_any(dev->handle, hwp);
+ snd_pcm_hw_params_set_access(dev->handle, hwp, SND_PCM_ACCESS_RW_INTERLEAVED);
+ snd_pcm_hw_params_set_format(dev->handle, hwp, sound->pcm_format);
+ snd_pcm_hw_params_set_channels(dev->handle, hwp, sound->channels);
+ snd_pcm_hw_params_set_rate(dev->handle, hwp, sound->rate, 0);
+ snd_pcm_hw_params_set_period_size(dev->handle, hwp,
+ sound->period_bytes/sound->sample_bytes, 0);
+ snd_pcm_hw_params_set_buffer_size(dev->handle, hwp,
+ sound->period_bytes*4/sound->sample_bytes);
+ snd_pcm_hw_params(dev->handle, hwp);
+ dev->pcm_format = sound->pcm_format;
+ dev->channels = sound->channels;
+ dev->rate = sound->rate;
+ dev->sample_bytes = sound->sample_bytes;
+ dev->period_bytes = sound->period_bytes;
+ if (dev->buf_size < dev->period_bytes) {
+ free(dev->period_buf);
+ dev->period_buf = malloc(dev->period_bytes);
+ dev->buf_size = dev->period_bytes;
+ }
+}
+
+void load_some(struct dev *dev, struct sound *sound)
+{
+ int len;
+ if (!dev || !dev->handle || !sound)
+ return;
+
+ switch(sound->format) {
+ case FORMAT_WAVE:
+ len = read_wave(sound,
+ dev->period_buf + dev->present,
+ dev->period_bytes - dev->present);
+ break;
+ case FORMAT_VORBIS:
+ len = read_vorbis(sound,
+ dev->period_buf + dev->present,
+ dev->period_bytes - dev->present);
+ break;
+ default:
+ sound->eof = 1;
+ len = 0;
+ }
+ dev->present += len;
+}
+
+struct sound *open_sound(char *dir, char *name, int ino)
+{
+ char path[200];
+ int fd;
+ struct sound *s;
+ char *eos, *eos1;
+
+ strcpy(path, dir);
+ strcat(path, "/");
+ strcat(path, name);
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return NULL;
+ s = malloc(sizeof(*s));
+ if (!s)
+ return NULL;
+ memset(s, 0, sizeof(*s));
+ s->fd = fd;
+ s->empty = 0;
+ s->eof = 0;
+ s->seen = 0;
+ s->name = strdup(name);
+ s->ino = ino;
+ s->posn = 0;
+ s->bytes = s->bytes_used = 0;
+
+ /* check for millisecond suffix */
+ eos = name + strlen(name);
+ while (eos > name && isdigit(eos[-1]))
+ eos--;
+ if (eos > name && eos[-1] == '-' && eos[0]) {
+ s->posn = atol(eos);
+ eos--;
+ }
+ /* Now pick off scenario name */
+ eos1 = eos;
+ while (eos1 > name && isalpha(eos1[-1]))
+ eos1--;
+ if (eos1 > name && eos1 < eos && eos - eos1 < 20) {
+ strncpy(s->scenario, eos1, eos-eos1);
+ s->scenario[eos-eos1] = 0;
+ }
+
+ if (lseek(fd, 0L, 2) == 0) {
+ close(fd);
+ s->fd = -1;
+ s->empty = 1;
+ s->format = FORMAT_UNKNOWN;
+ return s;
+ }
+ lseek(fd, 0L, 0);
+ /* Read header and set parameters */
+
+ if (parse_wave(s))
+ s->format = FORMAT_WAVE;
+ else if (parse_vorbis(s))
+ s->format = FORMAT_VORBIS;
+ else
+ s->format = FORMAT_UNKNOWN;
+
+ if (s->rate <= 8000) {
+ /* 100 ms == 800 samples, 1600 bytes */
+ s->period_bytes = s->rate / 100 * s->sample_bytes;
+ } else {
+ /* 44100, 2 seconds, 4 bytes would be 160K !!
+ * Doesn't work yet.
+ */
+ s->period_bytes = 8192;
+ }
+
+ if (s->posn)
+ switch(s->format) {
+ case FORMAT_WAVE:
+ seek_wave(s, s->posn);
+ break;
+ case FORMAT_VORBIS:
+ seek_vorbis(s, s->posn);
+ break;
+ }
+
+ return s;
+
+ fail:
+ close(s->fd);
+ free(s->name);
+ free(s);
+ return NULL;
+}
+
+void close_sound(struct sound *sound)
+{
+ close(sound->fd);
+ free(sound->name);
+ free(sound);
+}
+
+
+struct sound *find_match(struct list_head *list,
+ char *name, int ino,
+ int *matched)
+{
+ /* If name/ino is found in list, return it and set
+ * matched.
+ * else return previous entry (or NULL) and clear matched.
+ */
+ struct sound *rv = NULL;
+ struct sound *s;
+
+ *matched = 0;
+ list_for_each_entry(s, list, list) {
+ int c = strcmp(s->name, name);
+ if (c > 0)
+ /* we have gone beyond */
+ break;
+ rv = s;
+ if (c == 0) {
+ if (s->ino == ino)
+ *matched = 1;
+ break;
+ }
+ }
+ return rv;
+}
+
+void scan_dir(char *path, struct list_head *soundqueue)
+{
+ DIR *dir = opendir(path);
+ struct dirent *de;
+ struct sound *match;
+ struct sound *pos;
+
+ list_for_each_entry(match, soundqueue, list)
+ match->seen = 0;
+
+ while ((de = readdir(dir)) != NULL) {
+ struct sound *new;
+ int matched = 0;
+ if (de->d_ino == 0 ||
+ de->d_name[0] == '.')
+ continue;
+
+ match = find_match(soundqueue, de->d_name, de->d_ino, &matched);
+ if (matched) {
+ match->seen = 1;
+ continue;
+ }
+ new = open_sound(path, de->d_name, de->d_ino);
+ if (! new)
+ continue;
+ new->seen = 1;
+ if (match)
+ list_add(&new->list, &match->list);
+ else
+ list_add(&new->list, soundqueue);
+ }
+ closedir(dir);
+
+ list_for_each_entry_safe(match, pos, soundqueue, list)
+ if (!match->seen) {
+ list_del(&match->list);
+ close_sound(match);
+ }
+}
+
+void play_buf(struct dev *dev)
+{
+ if (dev->present == dev->period_bytes) {
+ alarm(30);
+ snd_pcm_writei(dev->handle,
+ dev->period_buf,
+ dev->period_bytes / dev->sample_bytes);
+ alarm(0);
+ dev->present = 0;
+ }
+}
+
+void dev_close(struct dev *dev)
+{
+ if (!dev->handle)
+ return;
+ if (dev->present) {
+ memset(dev->period_buf + dev->present, 0,
+ dev->period_bytes - dev->present);
+ snd_pcm_writei(dev->handle,
+ dev->period_buf,
+ dev->period_bytes / dev->sample_bytes);
+ dev->present = 0;
+ }
+ snd_pcm_drain(dev->handle);
+ snd_pcm_close(dev->handle);
+ dev->handle = NULL;
+}
+
+char *dir = "/var/run/sound";
+int dfd;
+struct dev dev;
+int suspend_handle;
+struct sound *last = NULL;
+struct event work_ev;
+
+static void do_scan(int fd, short ev, void *vp)
+{
+ struct list_head *soundqueue = vp;
+ struct sound *next;
+ struct timeval tv = {0, 0};
+
+ fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME);
+ scan_dir(dir, soundqueue);
+
+ if (list_empty(soundqueue)) {
+ dev_close(&dev);
+ set_scenario(&dev, "off");
+ suspend_allow(suspend_handle);
+ last = NULL;
+ return;
+ }
+ next = list_entry(soundqueue->next,
+ struct sound, list);
+ if (next->empty) {
+ dev_close(&dev);
+ set_scenario(&dev, next->scenario);
+ suspend_allow(suspend_handle);
+ last = next;
+ return;
+ }
+ suspend_block(suspend_handle);
+ event_add(&work_ev, &tv);
+}
+
+static void do_work(int fd, short ev, void *vp)
+{
+ struct list_head *soundqueue = vp;
+ struct sound *next;
+
+ if (list_empty(soundqueue)) {
+ set_scenario(&dev, "off");
+ return;
+ }
+ next = list_entry(soundqueue->next,
+ struct sound, list);
+ if (next->empty) {
+ set_scenario(&dev, next->scenario);
+ return;
+ }
+
+ if (next != last) {
+ set_params(&dev, next);
+ last = next;
+ }
+ load_some(&dev, next);
+ play_buf(&dev);
+ if (next->eof) {
+ char buf[1000];
+ sprintf(buf, "%s/%s", dir, next->name);
+ unlink(buf);
+ list_del(&next->list);
+ close_sound(next);
+ do_scan(fd, ev, vp);
+ } else {
+ struct timeval tv = {0, 0};
+ event_add(&work_ev, &tv);
+ }
+}
+
+static int suspend(void *vp)
+{
+ /* Don't need to do anything here, just as long as
+ * we cause suspend to block until check_alarms
+ * had a chance to run.
+ */
+ do_scan(-1, 0, vp);
+ return 1;
+}
+
+
+int main(int argc, char *argv[])
+{
+ struct list_head soundqueue;
+ struct event ev;
+
+ INIT_LIST_HEAD(&soundqueue);
+
+ suspend_handle = suspend_block(-1);
+ mkdir(dir, 0755);
+ dfd = open(dir, O_RDONLY|O_DIRECTORY);
+ if (dfd < 0) {
+ fprintf(stderr, "sound: Cannot open %s\n", dir);
+ exit(1);
+ }
+
+ event_init();
+
+ suspend_watch(suspend, NULL, &soundqueue);
+ signal_set(&ev, SIGIO, do_scan, &soundqueue);
+ signal_add(&ev, NULL);
+ fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME);
+
+ event_set(&work_ev, -1, 0, do_work, &soundqueue);
+
+ memset(&dev, 0, sizeof(dev));
+
+ suspend_allow(suspend_handle);
+ event_loop(0);
+ exit(0);
+}
--- /dev/null
+#!/usr/bin/env python
+
+# TODO
+# - Show time
+# - Make actual call
+# - integrate with launcher
+
+# Dialer
+#
+# Listen for the 'voice-dial' selection. When it is active we steal
+# it and raise the dialer.
+# Display is:
+# Number, or DTMF send
+# Name/number (time-of-call)
+# Keypad:
+#
+# 1 2 3 BS
+# 4 5 6 BS
+# 7 8 9 Ca
+# * 0 # ll
+#
+# Interaction with Modem:
+# - request call-out
+# write number to /var/run/gsm-state/call
+# - answer incoming
+# write 'answer' to /var/run/gsm-state/call
+# - hang-up
+# write empty string to /var/run/gsm-state/call
+# - determine status: on-call, incoming, idle
+# examine /var/run/gsm-state/status
+# - send request
+# - recv reply
+
+import gtk, pygtk, gobject
+import pango
+import os, sys, time
+import dnotify
+from subprocess import Popen
+from contactdb import contacts
+
+def record(key, value):
+ f = open('/var/run/gsm-state/.new.' + key, 'w')
+ f.write(value)
+ f.close()
+ os.rename('/var/run/gsm-state/.new.' + key,
+ '/var/run/gsm-state/' + key)
+
+def recall(key):
+ try:
+ fd = open("/var/run/gsm-state/" + key)
+ l = fd.read(1000)
+ fd.close()
+ except IOError:
+ l = ""
+ return l.strip()
+
+
+class Dialer(gtk.Window):
+ def __init__(self):
+ gtk.Window.__init__(self)
+ self.set_default_size(480,640)
+ self.set_title("Dialer")
+ self.connect('destroy', self.close_win)
+
+ self.number = ""
+ self.dtmf = False
+
+ self.oncall = False
+
+ self.book = contacts()
+ self.create_ui()
+ self.watch_clip('voice-dial')
+
+ d = dnotify.dir('/var/run/gsm-state')
+ self.status_watcher = d.watch('status', self.check_status)
+ self.incoming_watcher = d.watch('incoming', self.check_incoming)
+ self.show()
+
+ def close_win(self, *a):
+ gtk.main_quit()
+
+ def create_ui(self):
+
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(70 * pango.SCALE)
+ self.bfont = fd
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(80 * pango.SCALE)
+ self.nfont = fd
+ self.nfont_size = 80 * pango.SCALE
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(80 * pango.SCALE)
+ self.cfont = fd
+ self.cfont_size = 80 * pango.SCALE
+ v = gtk.VBox(); v.show(); self.add(v)
+
+ # number or DTMF
+ n = gtk.Entry(); n.show()
+ n.modify_font(self.nfont)
+ n.set_alignment(0.5)
+ n.set_size_request(-1, 90)
+ v.pack_start(n, expand=False)
+ self.num = n
+
+ # name (or number) of other end.
+ n = gtk.Label(); n.show()
+ n.modify_font(self.nfont)
+ n.set_size_request(-1, 90)
+ v.pack_start(n, expand=False)
+ self.callee = n
+ self.check_callee_font()
+
+ k = self.create_keypad()
+ v.pack_start(k, expand=True)
+
+ def create_keypad(self):
+ h = gtk.HBox(); h.show()
+
+ h.pack_start(self.create_col('1','4','7','*'))
+ h.pack_start(self.create_col('2','5','8','0'))
+ h.pack_start(self.create_col('3','6','9','#'))
+ cl = self.create_col('BS','CA\nLL')
+ h.pack_start(cl)
+ ch = cl.get_children()
+ self.BS = ch[0]
+ self.CALL = ch[1]
+ h.set_homogeneous(True)
+ return h
+
+ def create_col(self, *r):
+ v = gtk.VBox(); v.show(); v.set_homogeneous(True)
+ for b in r:
+ bt = gtk.Button(b);
+ bt.child.modify_font(self.bfont)
+ bt.connect('button_press_event', self.press, b)
+ bt.set_property('can-focus', False)
+ bt.show()
+ v.pack_start(bt)
+ return v
+
+ def press(self, b, ev, key):
+ if len(key) == 1:
+ if self.oncall == 2:
+ # Incoming call needs to be answered
+ return
+ if self.oncall and not self.dtmf:
+ self.num.set_text("")
+ self.num.set_position(-1)
+ self.dtmf = True
+ self.num.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("blue"))
+ if not self.oncall and key == '#' and self.num.get_text() == "":
+ key = '+'
+ self.num.insert_text(key, self.num.get_position())
+ self.num.set_position(self.num.get_position()+1)
+ if self.oncall:
+ self.do_dtmf(key)
+ self.check_num_font()
+ n = self.num.get_text()
+ if len(n) <= 1:
+ self.book.load()
+ e = self.book.find_num(n)
+ if e:
+ self.callee.set_text('To:' + e.name)
+ self.check_callee_font()
+ else:
+ self.callee.set_text('')
+ elif key == 'BS':
+ if self.oncall:
+ self.endcall()
+ else:
+ p = self.num.get_position()
+ if p > 0:
+ self.num.delete_text(p-1, p)
+ self.num.set_position(p-1)
+ self.check_num_font()
+ n = self.num.get_text()
+ if len(n) <= 1:
+ self.book.load()
+ e = self.book.find_num(n)
+ if e:
+ self.callee.set_text('To:' + e.name)
+ self.check_callee_font()
+ else:
+ self.callee.set_text('')
+ else:
+ if self.oncall == 2:
+ self.takecall()
+ elif not self.oncall:
+ self.makecall()
+
+ def watch_clip(self, board):
+ self.cb = gtk.Clipboard(selection=board)
+ self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ]
+
+ self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+
+ def got_clip(self, clipb, data):
+ a = clipb.wait_for_text()
+ if not self.oncall:
+ if a[:9] == "Incoming:":
+ self.incoming(a[9:])
+ else:
+ self.num.set_text(a)
+ self.num.set_position(-1)
+ self.check_num_font()
+ self.makecall()
+ self.cb.set_with_data(self.targets, self.get, self.got_clip, None)
+ self.present()
+
+ def incoming(self, num):
+
+ self.BS.child.set_text("En")
+ self.CALL.child.set_text("OK")
+ self.oncall = 2
+ self.dtmf = False
+ self.book.load()
+ self.set_incoming(num)
+
+ def set_incoming(self, num):
+ if num == '' or num == '-':
+ num = "Private Number"
+ self.num.set_text(num)
+ self.num.set_position(-1)
+ self.check_num_font()
+ self.number = num
+ e = self.book.find_num(num)
+ if e:
+ num = e.name
+ self.callee.set_text('From:' + num)
+ self.check_callee_font()
+
+ def check_incoming(self, f):
+ if self.oncall != 2:
+ return
+ n = recall('incoming')
+ self.set_incoming(n)
+
+ def get(self, sel, info, data):
+ sel.set_text("Number Please")
+
+ def check_status(self, f):
+ l = recall('status')
+ if l == 'INCOMING':
+ l = recall('incoming')
+ self.incoming(l)
+ self.present()
+ elif l == 'BUSY':
+ self.endcall()
+ self.callee.set_text('BUSY')
+ self.check_callee_font()
+ self.present()
+ elif l == 'on-call':
+ pass
+ elif l == '':
+ self.endcall()
+
+ def check_num_font(self):
+ n = self.num.get_text()
+ l = len(n)
+ if l <= 9:
+ s = 80 * pango.SCALE
+ else:
+ if l > 16:
+ l = 16
+ s = 80 * pango.SCALE * 9 / l
+ if self.nfont_size != s:
+ self.nfont.set_absolute_size(s)
+ self.nfont_size = s
+ self.num.modify_font(self.nfont)
+
+
+ def check_callee_font(self):
+ n = self.callee.get_text()
+ l = len(n)
+ if l <= 9:
+ s = 80 * pango.SCALE
+ else:
+ if l > 16:
+ l = 16
+ s = 80 * pango.SCALE * 9 / l
+ if self.cfont_size != s:
+ self.cfont.set_absolute_size(s)
+ self.cfont_size = s
+ self.callee.modify_font(self.cfont)
+
+
+ def makecall(self):
+ n = self.num.get_text()
+ self.num.select_region(0,-1)
+ if not n:
+ return
+ self.BS.child.set_text("En")
+ self.oncall = True
+ #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/gsmhandset.state',
+ # 'restore' ], shell=False, close_fds = True)
+ self.number = n
+ self.dtmf = False
+ self.book.load()
+ e = self.book.find_num(n)
+ if e:
+ self.callee.set_text('To:' + e.name)
+ else:
+ self.callee.set_text('To:' + n)
+ self.check_callee_font()
+ record('call',n)
+
+ def takecall(self):
+ self.oncall = True
+ #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/gsmhandset.state',
+ # 'restore' ], shell=False, close_fds = True)
+ self.num.select_region(0,-1)
+ record('call','answer')
+
+ def endcall(self):
+ if self.oncall == False:
+ return
+ record('call','')
+ self.oncall = False
+ #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/stereoout.state',
+ # 'restore' ], shell=False, close_fds = True)
+ self.BS.child.set_text("BS")
+ self.CALL.child.set_text("CA\nLL")
+ self.num.set_text("")
+ self.num.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))
+ self.num.set_position(-1)
+ self.callee.set_text("")
+ self.check_callee_font()
+
+ def do_dtmf(self, ch):
+ record('dtmf',ch)
+
+
+o = Dialer()
+gtk.main()
--- /dev/null
+#!/usr/bin/env python
+
+# Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# Connect 'tapboard' with 'fakeinput' to make a working on-screen
+# keyboard.
+# This script uses a long press on the 'power' button to raise
+# the keyboard, and also a double-tap on the 'z' axis of the
+# accelerometer. However that requires a kernel driver that I
+# never published, so it won't work at the moment.
+
+import time, gtk
+from fakeinput import fakeinput
+from tapboard_dextr import TapBoard
+from evdev import EvDev
+import gobject
+
+class KeyboardIcon(gtk.StatusIcon):
+ def __init__(self, activate = None):
+ gtk.StatusIcon.__init__(self)
+ self.set_from_file('/usr/local/pixmaps/tapinput-dextr.png')
+ self.set_activate(activate)
+
+ def set_activate(self, activate):
+ if activate:
+ self.connect('activate', activate)
+
+power_timer = None
+def power_pressed(cnt, type, code, value, win):
+ if type != 1:
+ # not a key press
+ return
+ if code != 116:
+ # not the power key
+ return
+ global power_timer
+ if value != 1:
+ # not a down press
+ if power_timer != None:
+ gobject.source_remove(power_timer)
+ power_timer = None
+ return
+ power_timer = gobject.timeout_add(300, power_held, win)
+
+def power_held(win):
+ global power_timer
+ power_timer = None
+ if win.visible:
+ win.hide()
+ win.visible = False
+ else:
+ win.hide()
+ win.show()
+ win.activate()
+ win.visible = True
+ return False
+
+last_tap = 0
+def tap_pressed(cnt, type, code, value, win):
+ global last_tap
+ if type != 1:
+ # not a key press
+ return
+ if code != 309:
+ # not BtnZ
+ return
+ if value != 1:
+ # only want dow, not up
+ return
+ now = time.time()
+ print now, last_tap
+ if now - last_tap < 0.2:
+ # two taps
+ if win.visible:
+ win.hide()
+ win.visible = False
+ else:
+ win.hide()
+ win.show()
+ win.activate()
+ win.visible = True
+ last_tap = 0
+ else:
+ last_tap = now
+
+ico = KeyboardIcon()
+w = gtk.Window(type=gtk.WINDOW_POPUP)
+w.visible = True
+w.connect("destroy", lambda w: gtk.main_quit())
+ti = TapBoard()
+w.add(ti)
+w.set_default_size(ti.width, ti.height)
+root = gtk.gdk.get_default_root_window()
+(x,y,width,height,depth) = root.get_geometry()
+x = int((width - ti.width)/2)
+y = int((height - ti.height)/2)
+w.move(x,y)
+def activate(ignore):
+ w.hide()
+ w.show()
+ w.visible = True
+ w.activate()
+ w.move(x,y)
+ico.set_activate(activate)
+fi = fakeinput()
+def dokey(ti, str):
+ fi.send_char(str)
+ti.connect('key', dokey)
+def domove(ti, x,y):
+ global startx, starty
+ if x == 0 and y == 0:
+ (startx, starty) = w.get_position()
+ x = 0
+ w.move(startx+x, starty+y)
+
+ti.connect('move', domove)
+def hideme(ti):
+ w.hide()
+ w.visible = False
+ti.connect('hideme', hideme)
+try:
+ pbtn = EvDev("/dev/input/power", power_pressed, w)
+ #tbtn = EvDev("/dev/input/accel", tap_pressed, w)
+except:
+ pass
+ti.show()
+w.show()
+fi.new_window()
+hideme(ti)
+gtk.main()
+
ti.connect('hideme', hideme)
try:
pbtn = EvDev("/dev/input/power", power_pressed, w)
- tbtn = EvDev("/dev/input/accel", tap_pressed, w)
+ #tbtn = EvDev("/dev/input/accel", tap_pressed, w)
except:
pass
ti.show()
w.show()
fi.new_window()
+hideme(ti)
gtk.main()