#!/usr/bin/python

# Copyright (C) 2010 Matthew Turnbull. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright notice, this 
#     list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright notice, 
#     this list of conditions and the following disclaimer in the documentation 
#     and/or other materials provided with the distribution. 
#  3. Neither the name of Caligon Studios nor the names of its contributors may 
#     be used to endorse or promote products derived from this software without 
#     specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import traceback, gconf, xml.parsers.expat, textwrap
from optparse import OptionParser, IndentedHelpFormatter
from os import path, stat, getuid
from random import randint
from time import sleep
from pwd import getpwuid
from commands import getstatusoutput

version = """\
%prog 2010.11.05.18.00
Copyright (C) 2010 Matthew Turnbull. All rights reserved.
Use of this source code is governed by the 3-clause BSD License
that can be found in the %prog source.
"""

usage="Usage: %prog [options] [background.xml file]"

description = """\
%prog is a utility to automatically change the GNOME desktop backgrounds.

The default delay is 5 minutes. If both MINUTES and SECONDS are specified, \
they are added together.

%prog uses ~/.gnome2/backgrounds.xml as the default background.xml file. \
This is the file used by the GNOME Appearance Properties, so you can continue \
to use that to configure your backgrounds. %prog will detect when the file \
changed and will re-parse it.\
"""

epilog = """ """

# Constants
pgrep_cmd = "pgrep -fx -U %s gnome-appearance-properties"

# GRRR... Keep newlines in the description text.
class IndentedHelpFormatterNL(IndentedHelpFormatter):
	def format_description(self, description):
		if not description:
			return description
		desc_width = self.width - self.current_indent
		indent = " "*self.current_indent
		bits = description.split("\n")
		formatted_bits = [
			textwrap.fill(bit,
				desc_width,
				initial_indent=indent,
				subsequent_indent=indent)
	                 for bit in bits]
		return "\n".join(formatted_bits) + "\n"
	format_epilog = format_description

# Background handler and parser
class BGHandler:
	def __init__(self, xml_file_path):
		assert(path.isfile(xml_file_path))
		self.path = xml_file_path
		self.clear()

	def clear(self):
		self.list = []
		self.next = 0
		self.data = None
		self.data_name = None

	def HasBackgrounds(self):
		return len(self.list) > 0

	def NextBackground(self, random = False):
		llen = len(self.list)
		if llen == 0:
			return None

		if random:
			self.next = randint(1, llen) - 1
		self.data = self.list[self.next]

		self.next = self.next + 1
		if self.next == llen:
			self.next = 0

		return self.data

	def Parse(self):
		self.clear()

		parser = xml.parsers.expat.ParserCreate()
		parser.returns_unicode = False
		parser.StartElementHandler = self.StartElementHandler
		parser.EndElementHandler = self.EndElementHandler
		parser.CharacterDataHandler = self.CharacterDataHandler

		try:
			with open(self.path, "r") as f:
				parser.ParseFile(f)
			return True
		except:
			print "Error: Parsing failed"
			traceback.print_exc()
			self.clear()
			return False

	def StartElementHandler(self, name, attrs):
		name = name.lower()
		if name == 'wallpaper':
			if not 'deleted' in attrs \
			or attrs['deleted'] == 'false':
				self.data = {}
		elif self.data != None:
			self.data_name = name

	def EndElementHandler(self, name):
		if name == 'wallpaper':
			if 'shade_type' in self.data and self.data['shade_type'] and \
			   'filename' in self.data and self.data['filename'] and \
			   'options' in self.data and self.data['options'] and \
			   'pcolor' in self.data and self.data['pcolor'] and \
			   'scolor' in self.data and self.data['scolor'] and \
			   'name' in self.data and self.data['name'] and \
			   path.isfile(self.data['filename']):
				self.list.append(self.data)
			self.data = None

	def CharacterDataHandler(self, data):
		if self.data_name:
			self.data[self.data_name] = data.strip()
			self.data_name = None

def mainLoop():
	# Handle command line
	parser = OptionParser(version=version, description=description, usage=usage,
		formatter=IndentedHelpFormatterNL())
	parser.epilog=parser.expand_prog_name(epilog)
	parser.add_option("-m", "--minutes", type="int", default=0,
		help="Minutes between background change")
	parser.add_option("-s", "--seconds", type="int", default=0,
		help="Seconds between background change")
	parser.add_option("-r", "--random", action="store_true", default=False,
		help="Change backgrounds randomly")
	parser.add_option("-v", "--verbose", action="store_true", default=False,
		help="Print extra information")
	(opts, args) = parser.parse_args()

	random = opts.random
	verbose = opts.verbose

	seconds = opts.seconds + (opts.minutes * 60)
	if not seconds:
		seconds = 300

	fpath = "~/.gnome2/backgrounds.xml"
	if args:
		fpath = args[0]
	fpath = path.expanduser(fpath)
	fpath = path.expandvars(fpath)

	if verbose:
		print "Options:"
		print "  Verbose: %s" % verbose
		print "  Random: %s" % random
		print "  Change time (seconds): %d" % seconds
		print "  Config file: %s" % fpath
		print ""

	bg = BGHandler(fpath)
	client = gconf.client_get_default()

	mt = 0
	last_mt = 0

	# DO THE LOOP
	while True:
		# Make sure that gnome-appearance-properties isn't running
		(pgrep_status, pgrep_output) = getstatusoutput(pgrep_cmd % getpwuid(getuid()).pw_name)
		if pgrep_status == 0:
			if verbose:
				print "Skipping wallpaper change: gnome-appearance-properties is running."
			sleep(seconds)
			continue

		# Check the backgrounds.xml file and parse if necessary
		mt = stat(fpath).st_mtime
		if mt > last_mt:
			last_mt = mt

			if verbose:
				print "Parsing background config file."

			bg.Parse()

			if verbose:
				for data in bg.list:
					print "Found background %s: " % data['name']
					for (key, val) in data.items():
						if key != 'name':
							print "  %s: %s" % (key, val)
				print ""

		# If we have available backgrounds, change it
		data = bg.NextBackground(opts.random)
		if data:
			if verbose:
				print "Changing background to %s" % data['name']

			# Change background image
			client.set_string("/desktop/gnome/background/picture_filename", data['filename']);
			client.set_string("/desktop/gnome/background/picture_options", data['options']);

			# Change background color
			client.set_string("/desktop/gnome/background/primary_color", data['pcolor']);
			client.set_string("/desktop/gnome/background/secondary_color", data['scolor']);
			client.set_string("/desktop/gnome/background/color_shading_type", data['shade_type']);

		sleep(seconds)

if __name__ == "__main__":
	mainLoop()

