#!/usr/pkg/bin/python2.7
#
# Browser bookmarks menu as a GNOME panel applet.
#
# (C) 2004-2005 Nigel Tao.
# Licensed under the GNU GPL.

import fileinput
import gconf
import gnome
import gnomeapplet
import gnome.ui
import gtk
import gobject
import HTMLParser
import os.path
import pango
import re
import sys
import xml.sax

#from gettext import gettext as _



APP_NAME = "browser-bookmarks-menu"
VERSION = "0.6"

gnome_datadir = gnome.program_init(APP_NAME, VERSION).get_property(gnome.PARAM_GNOME_DATADIR)


# We do not support "javascript:" URL bookmarks.
global_unsupported_url_pattern = re.compile("^javascript:", re.IGNORECASE)

# We mark with an icon those URL bookmarks that contain a "%s"
global_query_url_pattern = re.compile("%s")




# Encapsulated code to read the Mozilla/Firefox bookmarks file format.
# This class is based on GPL code taken from bookmarks-applet.py
# as found in gnome-python version 1.4.4 (author unknown).
class MozillaFormatBookmarksParser(HTMLParser.HTMLParser):
	def __init__(self, bookmarks_file_name):
		HTMLParser.HTMLParser.__init__(self)
		self.chars = ""
		self.root_menu = []
		self.tree_stack = []
		self.item_title = None
		self.item_href = None
		
		if bookmarks_file_name and os.path.exists(bookmarks_file_name):
			self.feed(file(bookmarks_file_name, 'r').read())

	def handle_starttag(self, tag, attrs):
		tag = tag.lower()
		if tag == "a":
			self.chars = ""
			for tag, value in attrs:
				if tag.lower() == 'href':
					self.item_href = value
		elif tag == "dl":
			new_menu = []
			self.tree_stack.append(new_menu)
			if len(self.tree_stack) > 1:
				self.tree_stack[-2].append((self.item_title, new_menu))
			else:
				self.root_menu = new_menu
		elif tag == "h1" or tag == "h3":
			self.chars = ""
		elif tag == "hr":
			self.tree_stack[-1].append((None, None))

	def handle_endtag(self, tag):
		tag = tag.lower()
		if tag == "a":
			self.tree_stack[-1].append((self.chars, self.item_href))
		elif tag == "dl":
			del self.tree_stack[-1]
		elif tag == "h1" or tag == "h3":
			self.item_title = self.chars

	def handle_data(self, chars):
		self.chars = self.chars + chars
# end class MozillaFormatBookmarksParser



# Encapsulated code to read the Epiphany bookmarks file format.
class EpiphanyFormatBookmarksParser(xml.sax.ContentHandler):
	def __init__(self, bookmarks_file_name):
		xml.sax.ContentHandler.__init__(self)
		self.subjects = {}
		self.bookmarks_with_no_subjects = []
		self.chars = ""
		self.title = None
		self.href = None
		self.root_menu = []
		self.root_menu_map = {}
		
		parser = xml.sax.make_parser()
		parser.setContentHandler(self)
		if bookmarks_file_name and os.path.exists(bookmarks_file_name):
			parser.parse(bookmarks_file_name)

	def startElement(self, name, attrs):
		self.chars = ""
		if name == "item":
			self.title = None
			self.href = None
			self.no_subject = True

	def endElement(self, name):
		if name == "title":
			self.title = self.chars
		elif name == "link":
			if self.href == None:
				self.href = self.chars
		elif name == "ephy:smartlink":
			self.href = self.chars
		elif name == "dc:subject":
			self.no_subject = False
			s = self.chars
			if not self.subjects.has_key(s):
				self.subjects[s] = []
			self.subjects[s].append((self.title, self.href))
		elif name == "item":
			if self.no_subject:
				self.bookmarks_with_no_subjects.append((self.title, self.href))

	def characters(self, chars):
		self.chars = self.chars + chars
	
	def endDocument(self):
		self.root_menu = []
		createEpiphanyMenuSystem(self.root_menu, True, self.subjects.items(), self.bookmarks_with_no_subjects)

def createEpiphanyMenuSystem(menu, toplevel, topics, uncategorized):
	# Apply Epiphany hierarchical bookmark system: see
	# http://home.exetel.com.au/harvey/epiphany/
	# We must use the exact same algorithm to get the same output.
	# This produces the same output as Epiphany 1.9.1.
	# Types:
	# 	* a Bookmark is a (title, href) pair, rendered as a 
	#	  separator if title is None.
	#	* a BookmarkList is a list of Bookmarks and 
	#	  (title, BookmarkList) pairs
	#	* titles, hrefs and topics are strings
	# input: 
	#	* topics is a list of (topic, list of Bookmarks) pairs
	#	* uncategorized is a list of Bookmarks, maybe empty
	#	* toplevel is True if this is the top level menu, where
	#	  we do not allow subdividions
	# output:
	#	* menu is a BookmarkList
	remaining = reduce(lambda x, (topic, bookmarks): x.union(bookmarks), 
			topics, set(uncategorized))
	unused = [(len(bookmarks), topic, bookmarks) 
			for topic, bookmarks in topics]
	menu_divisions = [[]]
	menu_divisions_named = []
	while True:
		unused = filter(lambda (l, t, b): l != 0, unused)
		unused.sort()
		if len(unused) == 0:
			break
		l, topic, bookmarks = unused.pop()
		if toplevel or (len(bookmarks) > 6 and len(remaining) > 20):
			# create a submenu
			submenu = []
			menu_divisions[0].append((topic, submenu))
			subtopics = []
			subuncat = set(bookmarks)
			for topic2, bookmarks2 in topics:
				if topic2 is topic:
					continue
				bookmarks_int = set(bookmarks).intersection(bookmarks2)
				if not bookmarks_int:
					continue
				subuncat -= bookmarks_int
				subtopics.append((topic2, list(bookmarks_int)))
			createEpiphanyMenuSystem(submenu, False, subtopics,
					list(subuncat))
		else:
			menu_divisions_named.append((topic, bookmarks))
		remaining -= set(bookmarks)
		unused = [(len(remaining.intersection(bookmarks)), topic, 
				bookmarks) for l, topic, bookmarks in unused]
	if menu_divisions_named:
		menu_divisions_named.sort(key = lambda x: x[0].lower())
		menu_divisions.extend(zip(*menu_divisions_named)[1])
	menu_divisions.append(uncategorized)
	for division in menu_divisions:
		if division:
			if menu and menu[-1][0] and not toplevel:
				# append a separator
				menu.append((None, None))
			division.sort(key = lambda x: x[0].lower())
			menu.extend(division)
# end class EpiphanyFormatBookmarksParser



# Encapsulated code to read favicons from the Epiphany internal bookmarks file 
# format.
# This has to use internal xml plist files, so could break without warning.
class EpiphanyFormatFaviconsParser(xml.sax.ContentHandler):
	class EpiphanyFaviconCacheFileParser(xml.sax.ContentHandler):
		def __init__(self, favicon_cache_file_name, 
				favicon_cache_path):
			xml.sax.ContentHandler.__init__(self)
			self.favicon_uri_path_map = {}
			parser = xml.sax.make_parser()
			parser.setContentHandler(self)
			self.path = favicon_cache_path
			if favicon_cache_file_name and os.path.exists(
					favicon_cache_file_name):
				parser.parse(favicon_cache_file_name)

		def startElement(self, name, attrs):
			self.chars = ""
			if name == "node":
				self.uri = None
				self.hash = None
			elif name == "property":
				self.propertyId = attrs["id"]

		def endElement(self, name):
			if name == "node":
				self.favicon_uri_path_map[self.uri] = os.path.join(self.path, self.hash)
			elif name == "property":
				if self.propertyId == "2":
					self.uri = self.chars
				elif self.propertyId == "3":
					self.hash = self.chars
		
		def characters(self, chars):
			self.chars += chars
	
	def __init__(self, internal_bookmarks_file_name, 
			favicon_cache_file_name, favicon_cache_path, 
			pixbuf_cache = [{}]):
		xml.sax.ContentHandler.__init__(self)
		self.path_favicons_cached = pixbuf_cache[0]
		self.path_favicons = {}
		self.favicons = {}
		self.favicon_cache_path = favicon_cache_path

		self.favicon_uri_path_map = self.EpiphanyFaviconCacheFileParser(favicon_cache_file_name, favicon_cache_path).favicon_uri_path_map
		parser = xml.sax.make_parser()
		parser.setContentHandler(self)
		if internal_bookmarks_file_name and os.path.exists(
				internal_bookmarks_file_name):
			parser.parse(internal_bookmarks_file_name)
		pixbuf_cache[0] = self.path_favicons
	
	def pixbufFromUri(self, uri):
		for icon_path in [os.path.join(self.favicon_cache_path, uri.replace("/", "_")), self.favicon_uri_path_map.get(self.favicon)]:
			pixbuf = None
			if self.path_favicons_cached.has_key(icon_path):
				pixbuf = self.path_favicons_cached[icon_path]
			elif icon_path and os.path.exists(icon_path):
				try:
					pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(icon_path, *gtk.icon_size_lookup(gtk.ICON_SIZE_MENU))
				except gobject.GError:
					pass
			self.path_favicons[icon_path] = pixbuf
			if pixbuf:
				return pixbuf
	
	def startElement(self, name, attrs):
		self.chars = ""
		if name == "node":
			self.href = None
			self.favicon = None
		elif name == "property":
			self.propertyId = attrs["id"]
	
	def endElement(self, name):
		if name == "node":
			if not self.favicon:
				return
			pixbuf = self.pixbufFromUri(self.favicon)
			if pixbuf:
				self.favicons[self.href] = pixbuf
		elif name == "property":
			if self.propertyId == "3":
				self.href = self.chars
			elif self.propertyId == "7":
				self.favicon = self.chars
	
	def characters(self, chars):
		self.chars += chars
# end class EpiphanyFormatFaviconsParser



# Encapsulated code to read the XBEL bookmarks file format.
class XbelFormatBookmarksParser(xml.sax.ContentHandler):
	def __init__(self, bookmarks_file_name):
		xml.sax.ContentHandler.__init__(self)
		self.root_menu = []
		
		self.currently_in_bookmark = False
		self.currently_in_folder = False
		self.currently_in_smarturl = False
		self.currently_in_title = False
		
		self.title = ""
		self.current_bookmark_name = None
		self.current_bookmark_href = None
		self.current_folder = self.root_menu
		self.folder_stack = [self.root_menu]
		
		parser = xml.sax.make_parser()
		parser.setContentHandler(self)
		if bookmarks_file_name and os.path.exists(bookmarks_file_name):
			parser.parse(bookmarks_file_name)

	def startElement(self, name, attrs):
		if name == "folder":
			self.currently_in_folder = True

		elif name == "bookmark":
			self.currently_in_bookmark = True
			if attrs.has_key("href"):
				self.current_bookmark_href = attrs["href"]
			else:
				self.current_bookmark_href = ""

		elif name == "smarturl":
			self.currently_in_smarturl = True
			self.current_bookmark_href = ""

		elif name == "title":
			self.currently_in_title = True
			self.title = ""

	def endElement(self, name):
		if name == "folder":
			self.folder_stack.pop()
			self.currently_in_folder = False

		if name == "separator":
			self.folder_stack[-1].append((None, None))

		elif name == "bookmark":
			self.folder_stack[-1].append((self.current_bookmark_name, self.current_bookmark_href))
			self.currently_in_bookmark = False
			self.current_bookmark_name = None
			self.current_bookmark_href = None

		elif name == "smarturl":
			self.currently_in_smarturl = False

		elif name == "title":
			if self.currently_in_bookmark:
				self.current_bookmark_name = self.title
			elif self.currently_in_folder:
				f = []
				self.folder_stack[-1].append((self.title, f))
				self.folder_stack.append(f)
			self.currently_in_title = False

	def characters(self, chars):
		if self.currently_in_title:
			self.title += chars
		elif self.currently_in_smarturl:
			self.current_bookmark_href += chars
# end class XbelFormatBookmarksParser



# begin browser-specific file location methods
def get_firefox_bookmarks_file_name():
	try:
		firefox_dir = os.path.expanduser("~/.mozilla/firefox/")
		path_pattern = re.compile("^Path=(.*)")
		for line in fileinput.input(firefox_dir + "profiles.ini"):
			if line == "":
				break
			match_obj = path_pattern.search(line)
			if match_obj:
				if match_obj.group(1).startswith("/"):
					return match_obj.group(1) + "/bookmarks.html"
				else:
					return firefox_dir + match_obj.group(1) + "/bookmarks.html"
	finally:
		fileinput.close()
	return None


def get_mozilla_bookmarks_file_name():
	default_profile_dir = os.path.expanduser("~/.mozilla/default")
	if os.path.exists(default_profile_dir):
		for d in os.listdir(default_profile_dir):
			fn = os.path.join(default_profile_dir, d, "bookmarks.html")
			if os.path.exists(fn):
				return fn
	return None


def get_epiphany_bookmarks_file_name():
	return os.path.expanduser("~/.gnome2/epiphany/bookmarks.rdf")


def get_epiphany_favicon_paths():
	return [os.path.expanduser(x) for x in (
			"~/.gnome2/epiphany/ephy-bookmarks.xml",
			"~/.gnome2/epiphany/ephy-favicon-cache.xml",
			"~/.gnome2/epiphany/favicon_cache/")]


def get_galeon_bookmarks_file_name():
	return os.path.expanduser("~/.galeon/bookmarks.xbel")


def get_konqueror_bookmarks_file_name():
	return os.path.expanduser("~/.kde/share/apps/konqueror/bookmarks.xml")
# end browser-specific file location methods



def get_http_handler():
	return gconf.client_get_default().get_string("/desktop/gnome/url-handlers/http/command")


def get_bookmarks():
	hh = get_http_handler()
	if hh.find("firefox") != -1:
		return (MozillaFormatBookmarksParser(get_firefox_bookmarks_file_name()).root_menu,\
		"Manage Bookmarks...", None, None)
		# I would love to let the above line end with:
		# "firefox -chrome chrome://browser/content/bookmarks/bookmarksManager.xul")
		# but see mozilla bug 276422
		# https://bugzilla.mozilla.org/show_bug.cgi?id=276422
	elif hh.startswith("mozilla"):
		return (MozillaFormatBookmarksParser(get_mozilla_bookmarks_file_name()).root_menu,\
		None, None, None)
	elif hh.startswith("epiphany"):
		return (EpiphanyFormatBookmarksParser(get_epiphany_bookmarks_file_name()).root_menu,\
		"Edit Bookmarks", "epiphany --bookmarks-editor",
		EpiphanyFormatFaviconsParser(*get_epiphany_favicon_paths()).favicons)
	elif hh.startswith("galeon"):
		return (XbelFormatBookmarksParser(get_galeon_bookmarks_file_name()).root_menu,\
		None, None, None)
	elif hh.startswith("konqueror"):
		return (XbelFormatBookmarksParser(get_konqueror_bookmarks_file_name()).root_menu,\
		None, None, None)
	else:
		return ([], None, None, None)


def applet_factory(applet, iid):
	gtk.rc_parse_string('''
		style "browser-bookmarks-menubar-style"
		{
			GtkMenuBar::shadow-type = none
			GtkMenuBar::internal-padding = 0
			GtkMenuItem::horizontal-padding = 3
		}
		widget "*.browser-bookmarks-menu" style "browser-bookmarks-menubar-style"''')
	
	root_menu = gtk.Menu()
	root_menu.connect("hide", on_menu_hidden)
	root_menu.connect("show", on_menu_shown)

	root_menu_item = gtk.MenuItem("Bookmarks")
	root_menu_item.set_name("browser-bookmarks-menu")
	root_menu_item.set_submenu(root_menu)

	menubar = gtk.MenuBar()
	menubar.set_name("browser-bookmarks-menu")
	menubar.connect("button-press-event", on_menubar_click)
	menubar.connect("size-allocate", on_menubar_size_allocate)
	menubar.append(root_menu_item)

	applet.add(menubar)
	applet.connect("destroy", on_applet_destroy, None)
	applet.set_applet_flags(gnomeapplet.EXPAND_MINOR)
	applet.show_all()
	
	# funky right-click menu
	menuXml = """
	<popup name="button3">
	        <menuitem
	        	name="Browser Bookmarks Menu About Item"
	        	verb="BBM About"
	        	_label="_About"
	        	pixtype="stock"
	        	pixname="gnome-stock-about"/>
	</popup>
	"""
	applet.setup_menu(menuXml, [("BBM About", on_about)], applet)
	
	return True


def fill_root_menu(menu):
	tooltips = gtk.Tooltips()
	tooltips.enable()
	(bookmarks_tree, bookmarks_editor_text, bookmarks_editor_cmd, favicons) = get_bookmarks()
	if bookmarks_editor_cmd is not None:
		menu_item = gtk.ImageMenuItem(bookmarks_editor_text)
		# little hack to scale the PNG file to the menu item height - there
		# is probably an easier (and more efficient) way to do this.
		height = menu.render_icon(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU).get_height()
		image = gtk.Image()
		image.set_from_file(gnome_datadir + "/pixmaps/epiphany-bookmarks.png")
		pixbuf = image.get_pixbuf().scale_simple(height, height, gtk.gdk.INTERP_BILINEAR)
		image.set_from_pixbuf(pixbuf)
		# end of hack
		menu_item.set_image(image)
		menu_item.connect("activate", run_command, bookmarks_editor_cmd)
		menu.append(menu_item)
		menu.append(gtk.SeparatorMenuItem())
	fill_menu(menu, tooltips, bookmarks_tree, favicons)


def fill_menu(menu, tooltips, contents, favicons):
	if len(contents) == 0:
		menu_item = gtk.MenuItem("No Bookmarks")
		menu_item.set_sensitive(False)
		menu.append(menu_item)
	else:
		for (name, href_or_folder) in contents:
			if name == None:
				menu_item = gtk.SeparatorMenuItem()
			else:
				menu_item = gtk.ImageMenuItem(name)
				menu_item.get_child().set_ellipsize(pango.ELLIPSIZE_END)
				menu_item.get_child().set_max_width_chars(32)
				if href_or_folder.__class__ == [].__class__:
					folder = href_or_folder
					sub_menu = gtk.Menu()
					menu_item.set_submenu(sub_menu)
					fill_menu(sub_menu, tooltips, folder, favicons)
				else:
					href = href_or_folder
					tooltips.set_tip(menu_item, href)
					if global_unsupported_url_pattern.search(href):
						menu_item.set_sensitive(False)
					else:
						menu_item.connect("activate", url_show, (name, href))
					if favicons and favicons.has_key(href):
						image = gtk.Image()
						image.set_from_pixbuf(favicons[href])
						menu_item.set_image(image)
					elif global_query_url_pattern.search(href):
						image = gtk.image_new_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
						menu_item.set_image(image)
			menu.append(menu_item)


def url_show(menu_item, (name,href)):
	if global_query_url_pattern.search(href):
		href = query_dialog(name, href)
	
	if href <> None:
		os.system(get_http_handler().replace("%s", "\"" + href + "\"") + " &")


def run_command(menu_item, command):
	os.system(command + " &")


def query_dialog(name, href):
	dialog = gtk.Dialog(title=name, buttons=
		(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
		gtk.STOCK_OPEN, gtk.RESPONSE_OK))
	
	hbox = gtk.HBox(False, 8)
	hbox.set_border_width(8)
	
	dialog.vbox.pack_start(hbox, False, False, 0)
	
	stock = gtk.image_new_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_DIALOG)
	hbox.pack_start(stock, False, False, 0)
	
	entry = gtk.Entry()
	entry.set_activates_default(True)
	hbox.pack_start(entry, False, False, 0)

	pixbuf = dialog.render_icon(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
	dialog.set_icon(pixbuf)
    	dialog.set_default_response(gtk.RESPONSE_OK)
	dialog.show_all()

	if dialog.run() == gtk.RESPONSE_OK:
		href = href.replace("%s", entry.get_text())
	else:
		href = None

	dialog.destroy()
	return href


def on_applet_destroy(widget, arg):
	del widget


def on_about(component, verb, applet):
	icon = gtk.Image()
	icon.set_from_file(gnome_datadir + "/pixmaps/epiphany-bookmarks.png")
	
	fullname = "Browser Bookmarks Menu"
	copyright = "Copyright (C) 2004-2005 Nigel Tao"
	description = "A menu for your web bookmarks."
	authors = ["Nigel Tao <nigel.tao@myrealbox.com>","","with thanks to", \
		"Gustavo J. A. M. Carneiro",  "Ed Catmur", "Johan Dahlin", \
		"David Fritzsche", "Toshio Kuratomi", "Reinout van Schouwen", \
		"Ricardo Veguilla", "Brett Viren",]

	about = gnome.ui.About(fullname, VERSION, copyright, description, authors, None, None, icon.get_pixbuf())
	about.set_icon(icon.get_pixbuf())
	about.show()


def on_menubar_click(widget, event):
	# allow Middle- and Right-Mouse-Button to go through to the applet window
	if event.button != 1:
		widget.emit_stop_by_name("button-press-event")

	return False


def on_menu_hidden(menu):
	menu.foreach(lambda x: menu.remove(x))


def on_menu_shown(menu):
	fill_root_menu(menu)
	menu.show_all()


# This little hack is required to get Fitt's Law compliance -
# i.e. to get the Menubar to be the full height of the panel.
def on_menubar_size_allocate(menubar, rect):
	if (rect.x <= 0) or (rect.y <= 0):
		return False
	rect.x -= 1
	rect.y -= 1
	rect.width  += 2
	rect.height += 2
	gtk.Widget.size_allocate(menubar, rect)
	return False


def printBookmarkList(bookmarks, level):
	s = ".   " * level
	for (name, href_or_folder) in bookmarks:
		if name == None:
			print s + "-----"
		else:
			if href_or_folder.__class__ == [].__class__:
				folder = href_or_folder
				print s + "[" + name + "]"
				printBookmarkList(folder, level + 1)
			else:
				href = href_or_folder
				print s + name + "  ->  " + href


if __name__ == "__main__":
	if len(sys.argv) == 2:
		if sys.argv[1] == "-print":
			printBookmarkList(get_bookmarks()[0], 0)

		elif sys.argv[1] == "-window":
			main_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
			main_window.set_title("Browser Bookmarks Menu")
			main_window.connect("destroy", gtk.main_quit)
			
			applet = gnomeapplet.Applet()
			applet_factory(applet, None)
			applet.reparent(main_window)
			
			main_window.show_all()
			gtk.main()

	else:
		gnomeapplet.bonobo_factory( \
			"OAFIID:Browser_Bookmarks_Menu_Factory", \
			gnomeapplet.Applet.__gtype__, \
			"browser_bookmarks_menu", \
			"0", \
			applet_factory)
