Commits

Iñigo Serna committed b3b781d

pdfviewer: initial check in

  • Participants
  • Parent commits da1a01f

Comments (0)

Files changed (2)

File pdfviewer/Makefile

+TARGET = pdfviewer
+SRCS = pdfviewer.vala ../irex/irex.vala ../irex/irexplus.vala
+VALACPKGS = --pkg=gtk+-2.0 --pkg=gdk-x11-2.0 --pkg=poppler-glib --pkg=liberipc --pkg=liberutils --pkg=posix --pkg=sqlite3 --pkg=MyUtils
+VALACC = arm-poky-linux-gnueabi-gcc
+VALACOPTS = --vapidir=../irex $(VALACPKGS) --cc=$(VALACC) $(patsubst %,-X %,$(CFLAGS))  --enable-experimental #-g --save-temps --disable-warnings --deps
+STRIP = arm-poky-linux-gnueabi-strip -s
+
+
+all: $(TARGET)
+
+$(TARGET_DR): $(SRCS)
+	valac $(VALACOPTS) $(SRCS) -o $(TARGET)
+	$(STRIP) $(TARGET)
+
+clean:
+	rm $(TARGET) *.o *.c

File pdfviewer/pdfviewer.vala

+////////////////////////////////////////////////////////////////////////
+// pdfviewer: experimental PDF viewer for IREX DR ereaders
+//
+// Copyright (C) 2011  Iñigo Serna <inigoserna@gmail.com>
+// Released under GPL v3+
+//
+// Time-stamp: <2011-11-01 23:30:10 inigo>
+//////////////////////////////////////////////////////////////////////
+
+
+using Gtk;
+using Poppler;
+
+using irex;
+using irexplus;
+using liberutils;
+using MyUtils;
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Constants
+const string[] PROGRAM_AUTHORS = { "Iñigo Serna <inigoserna@gmail.com>", null };
+const string   PROGRAM_NAME    = "pdfviewer";
+const string   PROGRAM_VERSION = "1.0";
+const string   PROGRAM_WEBSITE = "http://inigo.katxi.org/devel/xxx";
+
+enum ViewType { LOADING, SUMMARY, TOC, BOOK, THUMBS, BOOKMARKS }
+
+const string SDCARD = "/media/mmcblk0p1/";
+const int LONGPRESS_STEP = 5;
+
+const int KEY_UP       = 65362;
+const int KEY_PAGEUP   = 65365;
+const int KEY_DOWN     = 65364;
+const int KEY_PAGEDOWN = 65366;
+const int KEY_ENTER    = 65293;
+const int KEY_LSHIFT   = 65505;
+const string DEFAULT_STYLE = """
+style 'basic_style' {
+  GtkScrollbar::slider_width = 12
+  GtkScrollbar::stepper_size = 12
+  GtkTreeView::horizontal-separator = 3
+  GtkTreeView::vertical-separator = 0
+  font_name = 'sans 8'
+}
+
+class 'GtkWidget' style 'basic_style'
+""";
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Global variables
+App app;
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Errors
+public errordomain PdfDocError {
+    NO_TOC,
+    FAILED
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Table of Contents
+public class ToC {
+    public List<ToCNode>? children;
+
+    public ToC(Poppler.Document doc) throws PdfDocError.NO_TOC {
+        this.children = new List<ToCNode>();
+        var it = new Poppler.IndexIter(doc);
+        if (it == null)
+            throw new PdfDocError.NO_TOC("No ToC in the document");
+        var i = 0;
+        while (it != null) {
+            unowned Poppler.Action act = it.get_action();
+            if (act.any.type == Poppler.ActionType.GOTO_DEST) {
+                this.children.append(new ToCNode(doc, it, i));
+                i++;
+            }
+            if (!it.next())
+                break;
+        }
+        //   if toc children.length==1 -> move children one level up
+		if (this.children.length() == 1) {
+			var node = this.children.last().data;
+			this.children = new List<ToCNode>();
+			foreach(var child in node.children) {
+                this.children.append(child);
+			}
+		}
+    }
+
+    public int prev_chapter(int pagenum) {
+        var prev = 0;
+        foreach (var node in this.children) {
+            if (node.pagenum < pagenum)
+                prev = node.pagenum;
+            else
+                break;
+        }
+        return prev;
+    }
+
+    public int next_chapter(int pagenum) {
+        foreach (var node in this.children) {
+            if (node.pagenum > pagenum)
+                return node.pagenum;
+        }
+        return -1;
+    }
+
+    public string to_string() {
+        var buf = "";
+        foreach (var node in this.children) {
+            buf += node.to_string();
+        }
+        return buf;
+    }
+}
+
+
+public class ToCNode : GLib.Object {
+    public string title;
+    public int    pagenum;
+    public int[]  path;     // [2,1,3]
+    public string path_str; // 3.2.4.
+    public List<ToCNode>? children;
+
+    public ToCNode(Poppler.Document doc, Poppler.IndexIter it,
+                   int idx, int[]? parent_path=null) {
+        unowned Poppler.Action act = it.get_action();
+        if (act.any.type == Poppler.ActionType.GOTO_DEST) {
+            this.title = act.goto_dest.title;
+            this.pagenum = act.goto_dest.dest.page_num;
+            if (act.goto_dest.dest.named_dest!=null && act.goto_dest.dest.named_dest!="") {
+                unowned Poppler.Dest dest = doc.find_dest(act.goto_dest.dest.named_dest);
+                this.pagenum = dest.page_num;
+            }
+            if (parent_path == null) {
+                this.path = { idx };
+            } else {
+                this.path = parent_path;
+                this.path.resize(parent_path.length+1);
+                this.path[parent_path.length] = idx;
+            }
+            this.path_str = this._path2str();
+            this.children = new List<ToCNode>();
+            unowned Poppler.IndexIter it2 = it.get_child();
+            var i = 0;
+            while (it2 != null) {
+                this.children.append(new ToCNode(doc, it2, i, this.path));
+                i++;
+                if (!it2.next())
+                    break;
+            }
+        }
+    }
+
+    private string _path2str() {
+        var buf = "";
+        foreach (var i in this.path) {
+            buf += @"$(i+1).";
+        }
+        return buf;
+    }
+
+    public string to_string() {
+        var buf = @"$path_str $title ($pagenum)\n";
+        foreach (var node in this.children) {
+            buf += node.to_string();
+        }
+        return buf;
+    }
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// PDFDocument
+public class PdfDocument : GLib.Object {
+    public string filename;
+    public int    num_pages;
+    public ToC    toc;
+
+    private Poppler.Document doc;
+
+    public PdfDocument (string filename) throws PdfDocError.FAILED {
+        try {
+            this.doc = new Poppler.Document.from_file(Filename.to_uri(filename), "");
+        } catch (GLib.Error e) {
+            throw new PdfDocError.FAILED(e.message);
+        }
+        // attributes
+        this.num_pages = this.doc.get_n_pages();
+        this.filename = filename;
+        // ToC
+        try {
+            this.toc = new ToC(this.doc);
+        } catch (PdfDocError.NO_TOC e) {
+            this.toc = null;
+        }
+	}
+
+	public string get_attr(string attr) {
+		if (attr=="title")
+			return this.doc.title;
+		else if (attr=="author")
+			return this.doc.author;
+		else if (attr=="subject")
+			return this.doc.subject;
+		else if (attr=="keywords")
+			return this.doc.keywords;
+		else if (attr=="numpages")
+			return this.num_pages.to_string();
+		else if (attr=="filename")
+			return GLib.Path.get_basename(this.filename);
+		else if (attr=="path")
+			return GLib.Path.get_dirname(this.filename);
+		else if (attr=="size") {
+			int64 size;
+			File file = File.new_for_path(this.filename);
+			try {
+				size = file.query_info("*", FileQueryInfoFlags.NONE).get_size();
+			} catch (GLib.Error e) {
+				size = 0;
+			}
+			return MyUtils.format_size(size);
+		} else if (attr=="format")
+			return this.doc.format;
+		else if (attr=="producer")
+			return this.doc.producer;
+		else if (attr=="creator")
+			return this.doc.creator;
+		else if (attr=="creation_date")
+			return MyUtils.secs2strftime((long) this.doc.creation_date, "%A, %d %B %Y  %X");
+		else if (attr=="modification_date")
+			return MyUtils.secs2strftime((long) this.doc.mod_date, "%A, %d %B %Y  %X");
+		else
+			return "";
+	}
+
+	public Gdk.Pixbuf get_page_pixbuf(int pagenum, int ww, int wh) {
+		double pw, ph;
+		var page = this.doc.get_page(pagenum);
+		page.get_size(out pw, out ph);
+        var pb = new Gdk.Pixbuf(Gdk.Colorspace.RGB, false, 8, (int) pw, (int) ph);
+		page.render_to_pixbuf(0, 0, (int) pw, (int) ph, 1, 0, pb);
+		if (ww/wh > pw/ph)
+			return pb.scale_simple(ww, (int) (ww*ph/pw), Gdk.InterpType.BILINEAR);
+		else
+			return pb.scale_simple((int) (wh*pw/ph), wh, Gdk.InterpType.BILINEAR);
+	}
+
+	public void render_page(int pagenum, Cairo.Context ctx, int ww, int wh) {
+		double pw, ph;
+		var page = this.doc.get_page(pagenum);
+		page.get_size(out pw, out ph);
+		ctx.scale(ww/pw, wh/ph);
+		page.render(ctx);
+	}
+
+	public Cairo.ImageSurface get_page_imgsurface(int pagenum, int ww, int wh) {
+		double pw, ph;
+		var page = this.doc.get_page(pagenum);
+		page.get_size(out pw, out ph);
+		var imgsurface = new Cairo.ImageSurface(Cairo.Format.RGB24, (int) ww, (int) wh);
+		var ctx = new Cairo.Context(imgsurface);
+        ctx.set_source_rgb(1.0, 1.0, 1.0);
+		ctx.paint();
+		ctx.scale(ww/pw, wh/ph);
+		page.render(ctx);
+		return imgsurface;
+	}
+
+	public int[] get_chapters_marks() {
+		int[] marks = {};
+		if (this.toc==null || this.toc.children.length()==0)
+			return marks;
+		foreach(var node in this.toc.children) {
+			marks += (int) (100*node.pagenum / this.num_pages);
+		}
+		return marks;
+	}
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Common widgets
+public class MyLabel: Gtk.Label {
+	public MyLabel(string text="", string fmt="%s") {
+		this.set_alignment(0, (float) 0.5);
+		this.set_markup(fmt.printf(text));
+	}
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Loading Page
+public class LoadingPage : Gtk.VBox {
+	public LoadingPage() {
+		var lbl = new Gtk.Label("");
+		lbl.set_markup("<b><span size='xx-large'>Please wait\nwhile loding</span></b>");
+		this.pack_start(lbl, true, true);
+		this.show_all();
+	}
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Summary Page
+public class SummaryPage : Gtk.VBox {
+	private App app;
+	private Gtk.Image img_thumb;
+	private Gtk.Table tbl1;
+	private Gtk.Table tbl2;
+	private Gtk.Table tbl3;
+	private bool is_built;
+
+	public SummaryPage(App app) {
+		this.app = app;
+		this.homogeneous = false;
+		this.spacing = 10;
+		var hbox = new Gtk.HBox(false, 10);
+		var hdr = new Gtk.Label("");
+		hdr.set_markup("<b><span size='x-large'>Book Summary</span></b>");
+		hbox.pack_start(hdr, true, true, 10);
+		var btn = new Gtk.Button();
+		btn.relief = ReliefStyle.NONE;
+        btn.set_image(new Gtk.Image.from_file("/usr/share/ctb/icon-shortcut-button.png"));
+		btn.clicked.connect(() => { this.app.change_notebook_page(ViewType.BOOK); });
+		hbox.pack_end(btn, false, false, 0);
+		this.pack_start(hbox, false, false, 0);
+		var hbox2 = new Gtk.HBox(false, 20);
+		this.img_thumb = new Gtk.Image();
+		hbox2.pack_start(this.img_thumb, false, false, 0);
+		this.tbl1 = new Gtk.Table(7, 2, false);
+		this.tbl1.row_spacing = 10;
+		this.tbl1.column_spacing = 15;
+		this.tbl1.attach_defaults(new Gtk.Label(" "), 0, 2, 0, 1);
+		this.tbl1.attach(new MyLabel("Title:", "<b>%s</b>"), 0, 1, 1, 2, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl1.attach(new MyLabel("Author:", "<b>%s</b>"), 0, 1, 2, 3, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl1.attach(new MyLabel("Subject:", "<b>%s</b>"), 0, 1, 3, 4, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl1.attach(new MyLabel("Keywords:", "<b>%s</b>"), 0, 1, 4, 5, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl1.attach(new MyLabel("Pages:", "<b>%s</b>"), 0, 1, 5, 6, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl1.attach_defaults(new Gtk.Label(" "), 0, 2, 6, 7);
+		hbox2.pack_start(this.tbl1, true, true, 0);
+		this.pack_start(hbox2, false, false, 15);
+
+		this.tbl2 = new Gtk.Table(3, 2, false);
+		this.tbl2.row_spacing = 10;
+		this.tbl2.column_spacing = 15;
+		this.tbl2.attach(new MyLabel("Filename:", "<b>%s</b>"), 0, 1, 0, 1, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl2.attach(new MyLabel("Path:", "<b>%s</b>"), 0, 1, 1, 2, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl2.attach(new MyLabel("Size:", "<b>%s</b>"), 0, 1, 2, 3, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.pack_start(this.tbl2, false, false, 15);
+
+		this.tbl3 = new Gtk.Table(5, 2, false);
+		this.tbl3.row_spacing = 10;
+		this.tbl3.column_spacing = 15;
+		this.tbl3.attach(new MyLabel("Format:", "<b>%s</b>"), 0, 1, 0, 1, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl3.attach(new MyLabel("Producer:", "<b>%s</b>"), 0, 1, 1, 2, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl3.attach(new MyLabel("Creator:", "<b>%s</b>"), 0, 1, 2, 3, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl3.attach(new MyLabel("Creation date:", "<b>%s</b>"), 0, 1, 3, 4, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.tbl3.attach(new MyLabel("Modification date:", "<b>%s</b>"), 0, 1, 4, 5, AttachOptions.FILL, AttachOptions.SHRINK, 0, 0);
+		this.pack_start(this.tbl3, false, false, 15);
+		this.is_built = false;
+	}
+
+	public void update() {
+		if (this.is_built)
+			return;
+		var pb = this.app.pdfdoc.get_page_pixbuf(0, 200, 250);
+		this.img_thumb.set_from_pixbuf(pb);
+		this.tbl1.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("title")), 1, 2, 1, 2);
+		this.tbl1.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("author")), 1, 2, 2, 3);
+		this.tbl1.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("subject")), 1, 2, 3, 4);
+		this.tbl1.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("keywords")), 1, 2, 4, 5);
+		this.tbl1.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("numpages")), 1, 2, 5, 6);
+		this.tbl2.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("filename")), 1, 2, 0, 1);
+		this.tbl2.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("path")), 1, 2, 1, 2);
+		this.tbl2.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("size")), 1, 2, 2, 3);
+		this.tbl3.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("format")), 1, 2, 0, 1);
+		this.tbl3.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("producer")), 1, 2, 1, 2);
+		this.tbl3.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("creator")), 1, 2, 2, 3);
+		this.tbl3.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("creation_date")), 1, 2, 3, 4);
+		this.tbl3.attach_defaults(new MyLabel(this.app.pdfdoc.get_attr("modification_date")), 1, 2, 4, 5);
+		this.show_all();
+		this.is_built = true;
+	}
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// ToC Page
+public class TocPage : Gtk.VBox {
+	private App app;
+	private Gtk.TreeView tvToc;
+
+	public TocPage(App app) {
+		this.app = app;
+		this.homogeneous = false;
+		this.spacing = 10;
+		var hbox = new Gtk.HBox(false, 10);
+		var hdr = new Gtk.Label("");
+		hdr.set_markup("<b><span size='x-large'>Table of Contents</span></b>");
+		hbox.pack_start(hdr, true, true, 10);
+		var btn = new Gtk.Button();
+		btn.relief = ReliefStyle.NONE;
+        btn.set_image(new Gtk.Image.from_file("/usr/share/ctb/icon-shortcut-button.png"));
+		btn.clicked.connect(() => { this.app.change_notebook_page(ViewType.BOOK); });
+		hbox.pack_end(btn, false, false, 0);
+		this.pack_start(hbox, false, false, 0);
+		var toc_model = new Gtk.TreeStore(3, typeof (string), typeof (string), typeof (string));
+		this.tvToc = new Gtk.TreeView();
+		// common text cell renderers
+		var cr_left = new Gtk.CellRendererText();
+        cr_left.set("scale-set", true, "scale", 1.0, "xpad", 5, "xalign", 0.0,
+                    "ellipsize-set", true, "ellipsize", Pango.EllipsizeMode.END);
+        var cr_right = new Gtk.CellRendererText();
+		cr_right.set("scale-set", true, "scale", 1.0, "xpad", 5, "xalign", 1.0);
+		// columns
+		var col1 = new Gtk.TreeViewColumn.with_attributes("Section", cr_left, "text", 0);
+		col1.set("max-width", 100);
+		col1.set_data("colnum", 0);
+		this.tvToc.append_column(col1);
+		var col2 = new Gtk.TreeViewColumn.with_attributes("Title", cr_left, "text", 1);
+		col2.set("expand", true, "sizing", TreeViewColumnSizing.AUTOSIZE);
+		col2.set_data("colnum", 1);
+		this.tvToc.append_column(col2);
+		var col3 = new Gtk.TreeViewColumn.with_attributes("Page", cr_right, "text", 2);
+		col3.set_data("colnum", 2);
+		this.tvToc.append_column(col3);
+		this.tvToc.set_model(toc_model);
+		this.tvToc.button_press_event.connect(this.on_toc_clicked);
+		var swToc = new Gtk.ScrolledWindow(null, null);
+		swToc.set_policy(PolicyType.NEVER, PolicyType.AUTOMATIC);
+        swToc.add(tvToc);
+		this.pack_start(swToc, true, true, 0);
+	}
+
+	private bool on_toc_clicked(Gdk.EventButton ev) {
+		if (this.app.pdfdoc.toc== null)
+			return false;
+		Gtk.TreeIter iter;
+        Gtk.TreePath tpath;
+        Gtk.TreeViewColumn col;
+        if (!this.tvToc.get_path_at_pos((int) ev.x, (int) ev.y, out tpath, out col, null, null))
+            return false;
+		var model = (Gtk.TreeStore) this.tvToc.get_model();
+        if (!model.get_iter(out iter, tpath))
+            return false;
+		int col_idx = col.get_data("colnum");
+		if (col_idx == 0) {
+			if (!model.iter_has_child(iter))
+				this._toc_goto_page(model, iter);
+			if (this.tvToc.is_row_expanded(tpath))
+				this.tvToc.collapse_row(tpath);
+			else
+				this.tvToc.expand_row(tpath, false);
+		} else {
+			this._toc_goto_page(model, iter);
+		}
+		return true;
+	}
+
+	private void _toc_goto_page(Gtk.TreeStore model, Gtk.TreeIter iter) {
+		GLib.Value val;
+		model.get_value(iter, 2, out val);
+		this.app.update_page(int.parse((string) val)-1);
+		this.app.change_notebook_page(ViewType.BOOK);
+	}
+
+	private void _toc_append_sub(Gtk.TreeStore model, Gtk.TreeIter it0, ToCNode parent) {
+        Gtk.TreeIter it1;
+		foreach(var node in parent.children) {
+			model.append(out it1, it0);
+			model.set(it1, 0, (node.path[node.path.length-1]+1).to_string(), -1);
+			model.set(it1, 1, node.title, -1);
+            model.set(it1, 2, node.pagenum.to_string(), -1);
+			if (node.children != null)
+				this._toc_append_sub(model, it1, node);
+		}
+	}
+
+	public void populate() {
+		var model = (Gtk.TreeStore) this.tvToc.get_model();
+		model.clear();
+        Gtk.TreeIter it0;
+		foreach(var node in this.app.pdfdoc.toc.children) {
+			model.append(out it0, null);
+			model.set(it0, 0, (node.path[node.path.length-1]+1).to_string(), -1);
+			model.set(it0, 1, node.title, -1);
+            model.set(it0, 2, node.pagenum.to_string(), -1);
+			if (node.children != null)
+				this._toc_append_sub(model, it0, node);
+		}
+	}
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// BookPage
+public class BookPage : Gtk.VBox {
+	private App app;
+	private DaPage        da;
+	// private Gtk.Image     img;
+    public  PageBar       pagebar;
+    private Gtk.Label     lblPages;
+
+	public BookPage(App app) {
+		this.app = app;
+		this.homogeneous = false;
+		this.spacing = 0;
+        this.da = new DaPage(this.app);
+		this.pack_start(this.da, true, true, 0);
+        // this.img = new Gtk.Image();
+		// this.pack_start(this.img, true, true, 0);
+        var hbox = new Gtk.HBox(false, 0);
+        this.pack_start(hbox, false, false, 0);
+        this.pagebar = new PageBar();
+		hbox.pack_start(this.pagebar, true, true, 0);
+		this.lblPages = new Gtk.Label("Pages");
+		hbox.pack_end(this.lblPages, false, false, 0);
+	}
+
+	public void update() {
+		// var pb = this.app.pdfdoc.get_page_pixbuf(this.app.npage, this.img.allocation.width, this.img.allocation.height);
+		// this.img.set_from_pixbuf(pb);
+debug("Page start");
+		this.da.update();
+		this.pagebar.update((int) (100*(this.app.npage+1)/this.app.pdfdoc.num_pages));
+		this.lblPages.set_markup("<tt><span size='small' background='#FFFFFF'> %d/%d </span></tt>".printf(this.app.npage+1, this.app.pdfdoc.num_pages));
+debug("Page end");
+	}
+}
+
+
+public class DaPage : Gtk.DrawingArea {
+	private App app;
+	private int width;
+	private int height;
+	private Cairo.ImageSurface? imgsurf;
+
+	public DaPage(App app) {
+		this.double_buffered = false;
+		this.app_paintable = true;
+        this.configure_event.connect(this.on_configure);
+        this.expose_event.connect(this.on_draw);
+		// this.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
+		// this.button_press_event.connect(this.on_button_press);
+		this.app = app;
+		this.imgsurf = null;
+	}
+
+	private bool on_configure(Gdk.EventConfigure ev) {
+debug("Configure");
+		this.width = ev.width;
+		this.height = ev.height;
+		this.imgsurf = null;
+		return true;
+	}
+
+	private bool on_draw(Gtk.Widget w, Gdk.EventExpose? ev) {
+debug("Draw");
+        var ctx = Gdk.cairo_create(w.window);
+		// if (this.app.pdfdoc != null)
+		// 	this.app.pdfdoc.render_page(this.app.npage, ctx, this.width, this.height);
+		if (this.app.pdfdoc == null) {
+			ctx.set_source_rgb(1.0, 1.0, 1.0);
+			ctx.set_operator(Cairo.Operator.SOURCE);
+		} else {
+			if (this.imgsurf == null)
+				this.imgsurf = this.app.pdfdoc.get_page_imgsurface(this.app.npage, this.width, this.height);
+			ctx.set_source_surface(this.imgsurf, 0, 0);
+		}
+		ctx.paint();
+		return true;
+	}
+
+	public void update() {
+debug("Update");
+		this.imgsurf = null;
+		if (!this.is_realized())
+			return;
+		this.on_draw(this, null);
+debug("Page liberutils0");
+        liberutils.display_gain_control();
+		liberutils.display_update_return_control(1); // DM_HINT_FULL
+debug("Page liberutils1");
+	}
+}
+
+
+public class PageBar : Gtk.DrawingArea {
+	private const int XPAD = 5;
+	private const int YPAD = 2;
+	private int   width;
+	private int   height;
+	private int   fill_pct;
+	private int[] marks_pct;
+
+	public PageBar() {
+		this.app_paintable = true;
+        this.configure_event.connect(this.on_configure);
+        this.expose_event.connect(this.on_draw);
+		this.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
+		this.button_press_event.connect(this.on_button_press);
+		this.fill_pct = 0;
+		this.marks_pct = {};
+	}
+
+	private bool on_configure(Gdk.EventConfigure ev) {
+		this.width = ev.width;
+		this.height = ev.height;
+		this.queue_draw();
+		return true;
+	}
+
+	private bool on_draw(Gdk.EventExpose? ev) {
+		var ctx = Gdk.cairo_create(this.window);
+		ctx.set_source_rgb(1.0, 1.0, 1.0);
+        // ctx.set_operator(Cairo.Operator.SOURCE);
+		ctx.paint();
+		var w = this.width - 2*XPAD;
+		var h = this.height - 2*YPAD;
+		// fill
+        ctx.set_source_rgb(0.5, 0.5, 0.5);
+        ctx.rectangle(XPAD, YPAD, (int) (w*this.fill_pct/100), h);
+        ctx.fill();
+		// rectangle
+        ctx.set_line_width(2.0);
+        ctx.set_source_rgb(0, 0, 0);
+        ctx.set_line_join(Cairo.LineJoin.ROUND);
+        ctx.rectangle(XPAD, YPAD, w, h);
+        ctx.stroke();
+		// chapters marks
+        ctx.set_line_width(1.0);
+		foreach (var x in this.marks_pct) {
+			ctx.move_to(5+x*w/100, YPAD);
+			ctx.line_to(5+x*w/100, YPAD+h);
+		}
+		ctx.stroke();
+        return true;
+    }
+
+    private bool on_button_press(Gdk.EventButton ev) {
+		if (app.pdfdoc == null)
+			return true;
+		var pos = (ev.x-XPAD) / (this.width-2*XPAD);
+		app.update_page_with_pos(pos);
+        return true;
+    }
+
+	public void update_marks(int[] marks_pct) {
+		this.marks_pct = marks_pct;
+	}
+
+	public void update(int percent) {
+		this.fill_pct = percent;
+        if (!this.is_realized())
+            return;
+        this.on_draw(null);
+        liberutils.display_gain_control();
+        liberutils.display_update_return_control(4); // DM_HINT_PARTIAL
+    }
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Thumbnails Page
+public class ThumbsPage : Gtk.VBox {
+    private const int NUM_THUMBS = 9;
+    private App app;
+    private Gtk.Table table;
+    private Gtk.Button btn_left;
+    private Gtk.Button btn_right;
+    private int startpage;
+    private int i;
+
+    public ThumbsPage(App app) {
+        this.app = app;
+        this.homogeneous = false;
+        this.spacing = 20;
+        var hbox = new Gtk.HBox(false, 10);
+        this.pack_start(hbox, false, false, 0);
+        var hdr = new Gtk.Label("");
+        hdr.set_markup("<b><span size='x-large'>Thumbnails</span></b>");
+        hbox.pack_start(hdr, true, true, 10);
+        var btn = new Gtk.Button();
+        btn.relief = ReliefStyle.NONE;
+        btn.set_image(new Gtk.Image.from_file("/usr/share/ctb/icon-shortcut-button.png"));
+		btn.clicked.connect(() => { this.app.change_notebook_page(ViewType.BOOK); });
+        hbox.pack_end(btn, false, false, 0);
+
+        this.table = new Gtk.Table(3, 3, true);
+        this.table.row_spacing = 20;
+        this.table.column_spacing = 20;
+        this.pack_start(this.table, true, true, 0);
+
+        var hbox2 = new Gtk.HBox(false, 10);
+        this.pack_start(hbox2, false, false, 0);
+        this.btn_left = new Gtk.Button();
+        this.btn_left.relief = ReliefStyle.NONE;
+        this.btn_left.set_image(new Gtk.Image.from_stock(Stock.GO_BACK, IconSize.LARGE_TOOLBAR));
+        this.btn_left.clicked.connect(() => {
+                if (this.startpage >= NUM_THUMBS)
+                    this.update(startpage-NUM_THUMBS);
+            });
+        hbox2.pack_start(this.btn_left, false, false, 0);
+        this.btn_right = new Gtk.Button();
+        this.btn_right.relief = ReliefStyle.NONE;
+        this.btn_right.set_image(new Gtk.Image.from_stock(Stock.GO_FORWARD, IconSize.LARGE_TOOLBAR));
+        this.btn_right.clicked.connect(() => {
+                if (this.startpage+NUM_THUMBS < this.app.pdfdoc.num_pages-1)
+                    this.update(startpage+NUM_THUMBS);
+            });
+        hbox2.pack_end(this.btn_right, false, false, 0);
+        this.startpage = -2;
+    }
+
+    public void update(owned int startpage=-1) {
+        if (startpage == -1)
+            startpage = this.app.npage;
+        startpage = startpage/NUM_THUMBS * NUM_THUMBS;
+        if (startpage == this.startpage)
+            return;
+        this.startpage = startpage;
+        this.table.foreach( (w) => { this.table.remove(w); });
+        this.btn_left.set_sensitive((this.startpage>=NUM_THUMBS));
+        this.btn_right.set_sensitive((this.startpage+NUM_THUMBS < this.app.pdfdoc.num_pages-1));
+        this.i = 0;
+        GLib.Idle.add(this._update_idle);
+        this.table.show_all();
+    }
+
+    public bool _update_idle() {
+        if (i == NUM_THUMBS)
+            return false;
+        var n = this.startpage + i;
+        if (n >= this.app.pdfdoc.num_pages-1)
+            return false;
+        var r = i / 3;
+        var c = i % 3;
+        var evbox = new Gtk.EventBox();
+        evbox.button_press_event.connect(() => {
+                this.app.update_page(n);
+                this.app.change_notebook_page(ViewType.BOOK);
+                return true;
+            });
+        var vbox = new Gtk.VBox(false, 6);
+        var img = new Gtk.Image.from_pixbuf(this.app.pdfdoc.get_page_pixbuf(n, 200, 240));
+        vbox.pack_start(img, false, false, 0);
+        var lbl = new Gtk.Label("");
+        if (n == this.app.npage)
+            lbl.set_markup(@"<span size='small' foreground='#333333' background='#CCCCCC' >Page $(n+1)</span>");
+        else
+            lbl.set_markup(@"<span size='small'>Page $(n+1)</span>");
+        vbox.pack_start(lbl, false, false, 0);
+        evbox.add(vbox);
+        evbox.show_all();
+        this.table.attach_defaults(evbox, c, c+1, r, r+1);
+        this.i++;
+        liberutils.display_gain_control();
+        liberutils.display_update_return_control(4); // DM_HINT_PARTIAL
+        return true;
+    }
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Bookmarks Page
+public class BookmarksPage : Gtk.VBox {
+    private App app;
+
+    public BookmarksPage(App app) {
+        this.app = app;
+        this.homogeneous = false;
+        this.spacing = 10;
+        var hbox = new Gtk.HBox(false, 10);
+        var hdr = new Gtk.Label("");
+        hdr.set_markup("<b><span size='x-large'>Bookmarks &amp; Annotations</span></b>");
+        hbox.pack_start(hdr, true, true, 10);
+        var btn = new Gtk.Button();
+        btn.relief = ReliefStyle.NONE;
+        btn.set_image(new Gtk.Image.from_file("/usr/share/ctb/icon-shortcut-button.png"));
+        btn.clicked.connect(() => { this.app.change_notebook_page(ViewType.BOOK); });
+        hbox.pack_end(btn, false, false, 0);
+        this.pack_start(hbox, false, false, 0);
+
+        var btn_quit = new Gtk.Button.from_stock("gtk-quit");
+        btn_quit.clicked.connect(() => { Gtk.main_quit(); });
+        this.pack_start(btn_quit, false, false, 0);
+    }
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// App
+public class App : Gtk.Window, irex.Application {
+    public PdfDocument pdfdoc;
+    public int npage;
+    private bool is_fullscreen;
+
+    private Gtk.Notebook  notebook;
+    private LoadingPage   loading_page;
+    private SummaryPage   summary_page;
+    private TocPage       toc_page;
+    private BookPage      book_page;
+    private ThumbsPage    thumbs_page;
+    private BookmarksPage bookmarks_page;
+
+    private irex.IPC         ipc;
+    private irex.MenuManager menu_manager;
+    private irex.Menu        menu_main;
+
+    // public App(string filename) {
+    public App() {
+        this.pdfdoc = null;
+        this.is_fullscreen = false;
+        this.build_ui();
+        this.build_dr();
+    }
+
+    private void build_ui() {
+        this.title = PROGRAM_NAME;
+        // this.set_size_request(768, 1024); // TODO: delete
+        this.maximize();
+        this.position = WindowPosition.CENTER;
+        this.delete_event.connect(this.on_quit);
+        this.destroy.connect(Gtk.main_quit);
+        this.key_press_event.connect(this.on_key_pressed);
+        this.key_release_event.connect(this.on_key_released);
+        this.notebook = new Gtk.Notebook();
+        this.notebook.show_tabs = false;
+        this.add(this.notebook);
+        // loading view
+        var align0 = new Gtk.Alignment((float) 0.5, (float) 0.5, 1, 1);
+        align0.set_padding(50, 50, 50, 50);
+        this.loading_page = new LoadingPage();
+        align0.add(this.loading_page);
+        notebook.append_page(align0, null);
+        // summary view
+        var align1 = new Gtk.Alignment((float) 0.5, 0, 1, 1);
+        align1.set_padding(10, 10, 20, 20);
+        this.summary_page = new SummaryPage(this);
+        align1.add(this.summary_page);
+        notebook.append_page(align1, null);
+        // toc view
+        var align2 = new Gtk.Alignment((float) 0.5, 0, 1, 1);
+        align2.set_padding(10, 10, 10, 10);
+        this.toc_page = new TocPage(this);
+        align2.add(this.toc_page);
+        notebook.append_page(align2, null);
+        // book view
+        var align3 = new Gtk.Alignment((float) 0.5, 0, 1, 1);
+        align3.set_padding(0, 2, 5, 0);
+        this.book_page = new BookPage(this);
+        align3.add(this.book_page);
+        notebook.append_page(align3, null);
+        // thumbnails view
+        var align4 = new Gtk.Alignment((float) 0.5, 0, 1, 1);
+        align4.set_padding(10, 10, 10, 10);
+        this.thumbs_page = new ThumbsPage(this);
+        align4.add(this.thumbs_page);
+        notebook.append_page(align4, null);
+        // bookmarks & annotations view
+        var align5 = new Gtk.Alignment((float) 0.5, 0, 1, 1);
+        align5.set_padding(10, 10, 10, 10);
+        this.bookmarks_page = new BookmarksPage(this);
+        align5.add(this.bookmarks_page);
+        notebook.append_page(align5, null);
+        // end
+        this.notebook.switch_page.connect((a, b, nb_pagenum) => {this.on_notebook_switch_page(nb_pagenum);});
+        this.show_all();
+    }
+
+    private void build_dr() {
+        ipc = new irex.IPC("Pdfviewer", "1.0", this);
+        menu_manager = new irex.MenuManager(ipc);
+        // menu
+        menu_main = new irex.Menu(menu_manager, "menumain", "Pdfviewer");
+        var group_main = menu_main.addGroup("groupmain", "Main Buttons");
+
+        var subgroup_goto0 = group_main.addSubGroup("groupgoto0", "Go to...");
+        var subgroup_goto = subgroup_goto0.addSubGroup("groupgoto", "Go to...");
+        subgroup_goto.addItem("gopage",          "Goto Page",            "goto_page");
+        subgroup_goto.addItem("goprevchapter",   "Beginning of Chapter", "history_back");
+        subgroup_goto.addItem("gonextchapter",   "Next Chapter",	     "history_forward");
+        subgroup_goto.addItem("goprevbookmark",  "Previous Bookmark",	 "history_back");
+        subgroup_goto.addItem("gonextbookmark",  "Next Bookmark",        "history_forward");
+
+		var subgroup_views0 = group_main.addSubGroup("groupviews0", "Views");
+        var subgroup_views = subgroup_views0.addSubGroup("groupviews", "Views");
+        subgroup_views.addItem("viewsummary",   "Book summary",            "view_bookinfo");
+        subgroup_views.addItem("viewtoc",       "Table of Contents",       "view_toc");
+        subgroup_views.addItem("viewbook",      "Book",                    "view_reading");
+        subgroup_views.addItem("viewthumbs",    "Thumbnails",              "view_thumbnail");
+        subgroup_views.addItem("viewbookmarks", "Bookmarks & Annotations", "view_annotation");
+
+        group_main.addItem("fullscreen",  "Fullscreen",   "mode_full_screen");
+        group_main.addItem("close",       "Close",        "close");
+		menu_main.realise();
+        menu_main.show();
+
+        // toolbar
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "system_top",           "desktop");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupviews", "viewsummary");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupviews", "viewtoc");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupviews", "viewbook");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupviews", "viewthumbs");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupviews", "viewbookmarks");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupmain",  "fullscreen");
+        menu_manager.add_toolbar_item("Pdfviewer_menumain", "Pdfviewer_groupmain",  "close");
+    }
+
+    public Gtk.Window getMainWindow() {
+        return this;
+    }
+
+	public void onMenuClick(string item, string group, string menu, string state) {
+print("onMenuClick: %s %s %s %s\n", item, group, menu, state);
+        if (group=="Pdfviewer_groupmain") {
+            if (item=="close") {
+                this.on_quit();
+            } else if (item=="fullscreen") {
+                this.is_fullscreen = !this.is_fullscreen;
+                if (this.is_fullscreen) {
+                    menu_manager.set_item_state("fullscreen", group, "selected");
+                    this.fullscreen();
+                } else {
+                    menu_manager.set_item_state("fullscreen", group, "normal");
+                    this.unfullscreen();
+                }
+			}
+        } else if (group=="Pdfviewer_groupviews") {
+            if (item=="viewsummary")
+				this.change_notebook_page(ViewType.SUMMARY);
+            else if (item=="viewtoc")
+				this.change_notebook_page(ViewType.TOC);
+            else if (item=="viewbook")
+				this.change_notebook_page(ViewType.BOOK);
+            else if (item=="viewthumbs")
+				this.change_notebook_page(ViewType.THUMBS);
+            // else if (item=="viewbookmarks")
+			// 	this.change_notebook_page(ViewType.BOOKMARKS);
+        } else if (group=="Pdfviewer_groupgoto") {
+            if (item=="goprevchapter")
+				this.update_page(this.pdfdoc.toc.prev_chapter(this.npage+1)-1);
+            else if (item=="gonextchapter")
+				this.update_page(this.pdfdoc.toc.next_chapter(this.npage+1)-1);
+		}
+   }
+
+    public void onWindowChange(int xid, bool activated) {
+print("onWindowChange: %d, %d\n", xid, (int) activated);
+        if (activated)
+            menu_main.show();
+    }
+
+    public bool onFileOpen(string filename, out int xid, out string error) { return true; }
+    public bool onFileClose(string filename) { return true; }
+    public void onPrepareUnmount(string device) {}
+    public void onUnmounted(string device) {}
+    public void onMounted(string device) {}
+    public void onPrepareHibernate() {}
+    public void onChangedLocale(string locale) {}
+    public void onChangedOrientation(string orientation) {}
+
+
+    //////////////////////////////////////////////////
+	private bool on_quit() {
+		// TODO: write config, book state, etc
+        Gtk.main_quit();
+        return true;
+    }
+
+	private bool on_key_pressed (Widget source, Gdk.EventKey key) {
+		if (this.pdfdoc == null)
+			return false;
+		int page = -1;
+        if (key.str == "q") {
+            Gtk.main_quit ();
+        }
+		if (this.notebook.page != ViewType.BOOK)
+			return false;
+		if (key.str == "-") {
+			if (this.npage == 0)
+				return false;
+			page = this.pdfdoc.toc.prev_chapter(this.npage+1)-1;
+        } else if (key.str == "+") {
+			if (this.npage == this.pdfdoc.num_pages-1)
+				return false;
+			var next = this.pdfdoc.toc.next_chapter(this.npage+1);
+			page = (next==-1) ? this.pdfdoc.num_pages-1 : next-1;
+		}
+		if (page != -1)
+			this.update_page(page);
+        return false;
+    }
+
+	private bool on_key_released (Widget source, Gdk.EventKey ev) {
+		if (this.pdfdoc == null)
+			return false;
+		if (this.notebook.page != ViewType.BOOK)
+			return false;
+		int page = -1;
+        if (ev.keyval == KEY_UP) {
+			if (this.npage == 0)
+				return false;
+			page = this.npage - 1;
+        } else if (ev.keyval == KEY_PAGEUP) {
+			if (this.npage == 0)
+				return false;
+			page = this.npage - LONGPRESS_STEP;
+        } else if (ev.keyval == KEY_DOWN) {
+			if (this.npage == this.pdfdoc.num_pages-1)
+				return false;
+			page = this.npage + 1;
+        } else if (ev.keyval == KEY_PAGEDOWN) {
+			if (this.npage == this.pdfdoc.num_pages-1)
+				return false;
+			page = this.npage + LONGPRESS_STEP;
+        } else if (ev.keyval == KEY_LSHIFT) {
+            return false;
+        } else {
+            return false;
+        }
+		if (page != -1) {
+			this.update_page(page);
+		}
+		return true;
+	}
+
+	private void on_notebook_switch_page(uint nb_pagenum) {
+		if (this.pdfdoc == null)
+			return;
+debug(@"NB PAGE: $(nb_pagenum)");
+        if (nb_pagenum == ViewType.SUMMARY)
+			this.summary_page.update();
+		else if (nb_pagenum == ViewType.BOOK)
+			this.book_page.update();
+		else if (nb_pagenum == ViewType.THUMBS)
+			this.thumbs_page.update(this.npage);
+	}
+
+	//////////////////////////////////////////////////
+	public void change_notebook_page(int page) {
+		this.notebook.page = page;
+	}
+
+	public void update_page(int page=0) {
+		this.npage = page;
+		if (this.npage < 0)
+			this.npage = 0;
+		else if (this.npage > this.pdfdoc.num_pages-1)
+			this.npage = this.pdfdoc.num_pages-1;
+debug(@"Page: $(this.npage+1)");
+		this.book_page.update();
+	}
+
+	public void update_page_with_pos(double pos) {
+		var page = (int) (pos * this.pdfdoc.num_pages);
+		this.update_page(page);
+	}
+
+	//////////////////////////////////////////////////
+	private string dialog_load_file() {
+		string filename =  "";
+		var dlg = new Gtk.FileChooserDialog ("Open PDF File", this,
+											 Gtk.FileChooserAction.OPEN,
+											 Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL,
+											 Gtk.Stock.OPEN, Gtk.ResponseType.ACCEPT);
+		dlg.set_current_folder(SDCARD);
+		var filter = new Gtk.FileFilter();
+		filter.set_name("PDF files");
+		filter.add_pattern("*.pdf");
+		dlg.add_filter(filter);
+        if (dlg.run() == Gtk.ResponseType.ACCEPT)
+			filename = dlg.get_filename();
+		dlg.hide_all();
+		dlg.destroy();
+		return filename;
+	}
+
+	public bool load_file(string filename) {
+		try {
+            this.pdfdoc = new PdfDocument(filename);
+        } catch (PdfDocError.FAILED e) {
+            error(e.message);
+        }
+        this.book_page.pagebar.update_marks(this.pdfdoc.get_chapters_marks());
+		if (this.pdfdoc.toc == null) {
+			menu_manager.set_item_state("goprevchapter", "Pdfviewer_groupgoto", "disabled");
+			menu_manager.set_item_state("gonextchapter", "Pdfviewer_groupgoto", "disabled");
+		} else {
+			this.toc_page.populate();
+			menu_manager.set_item_state("goprevchapter", "Pdfviewer_groupgoto", "normal");
+			menu_manager.set_item_state("gonextchapter", "Pdfviewer_groupgoto", "normal");
+		}
+		this.update_page(0);
+		this.change_notebook_page(ViewType.BOOK);
+		return true;
+	}
+
+	public void run(owned string filename) {
+		ipc.send_startup_complete();
+		while (filename == "") {
+			filename = this.dialog_load_file();
+		}
+		this.load_file(filename);
+        Gtk.main();
+	}
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+///// Main entry point
+public static int main(string[] args) {
+	// arguments
+	string filename;
+	if (args.length == 1)
+		filename = "";
+	else if (args.length == 2)
+		filename = args[1];
+	else
+		error("Usage: %s /full/path/to/some.pdf", PROGRAM_NAME);
+	// initialize application and run
+	Gtk.init(ref args);
+	Gtk.rc_parse_string(DEFAULT_STYLE);
+	app = new App();
+	app.run(filename);
+	return 0;
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+// TODO:
+// . remember book state (page number, zoom, fullscreen, bookmarks, annotations)
+// . sys_set_bg_busy
+// . libpoppler patch
+// . new file selection dialog
+// . zoom: implement a rectangular area zoom (i.e. crop borders), menu
+// . goto page
+// . app icon
+// . bookmarks: view, mark in page, marks in pagebar, menu
+// . thumbnails view: cursor and keyboard interaction
+// . stylus-based annotation: view, mark in page, marks in pagebar, menu
+// . cache images
+//   . if size change -> invalidate images cache
+
+// NEVER:
+// . other zooms
+// . continuous and landscape modes
+// . multidoc
+// . stylus based annotations
+// . change font size
+// . links in .pdf
+// . find text
+
+// BUGS:
+// . ghostling