[Thunar-workers] CVS: design/ui/rox .cvsignore, NONE, 1.1 __init__.py, NONE, 1.1 applet.py, NONE, 1.1 basedir.py, NONE, 1.1 choices.py, NONE, 1.1 filer.py, NONE, 1.1 i18n.py, NONE, 1.1 icon_theme.py, NONE, 1.1 mime.py, NONE, 1.1 saving.py, NONE, 1.1

Benedikt Meurer benny at xfce.org
Sun Feb 27 19:13:52 CET 2005


Update of /var/cvs/thunar/design/ui/rox
In directory espresso.foo-projects.org:/tmp/cvs-serv27506/rox

Added Files:
	.cvsignore __init__.py applet.py basedir.py choices.py 
	filer.py i18n.py icon_theme.py mime.py saving.py 
Log Message:
2005-02-27	Benedikt Meurer <benny at xfce.org>

	* rox/: Use parts of the ROX Python libraries for the browser-like
	  prototype.
	* Main.py, ThunarFileInfo.py, ThunarListView.py, ThunarMimeDatabase.py,
	  ThunarModel.py, ThunarPropertiesDialog.py, ThunarTreePane.py,
	  ThunarView.py, ThunarWindow.py, thunar.ui: More work on the
	  browser-like prototype. It is now possible to browse your file system,
	  similar to the spatial prototype.




--- NEW FILE: .cvsignore ---
*.pyc
.*.swp

--- NEW FILE: __init__.py ---
"""To use ROX-Lib2 you need to copy the findrox.py script into your application
directory and import that before anything else. This module will locate
ROX-Lib2 and add ROX-Lib2/python to sys.path. If ROX-Lib2 is not found, it
will display a suitable error and quit.

Since the name of the gtk2 module can vary, it is best to import it from rox,
where it is named 'g'.

The AppRun script of a simple application might look like this:

	#!/usr/bin/env python
	import findrox; findrox.version(1, 9, 12)
	import rox

	window = rox.Window()
	window.set_title('My window')
	window.show()

	rox.mainloop()

This program creates and displays a window. The rox.Window widget keeps
track of how many toplevel windows are open. rox.mainloop() will return
when the last one is closed.

'rox.app_dir' is set to the absolute pathname of your application (extracted
from sys.argv).

The builtin names True and False are defined to 1 and 0, if your version of
python is old enough not to include them already.
"""

import sys, os, codecs

_to_utf8 = codecs.getencoder('utf-8')

roxlib_version = (1, 9, 17)

_path = os.path.realpath(sys.argv[0])
app_dir = os.path.dirname(_path)
if _path.endswith('/AppRun') or _path.endswith('/AppletRun'):
	sys.argv[0] = os.path.dirname(_path)

# In python2.3 there is a bool type. Later versions of 2.2 use ints, but
# early versions don't support them at all, so create them here.
try:
	True
except:
	import __builtin__
	__builtin__.False = 0
	__builtin__.True = 1

try:
	iter
except:
	sys.stderr.write('Sorry, you need to have python 2.2, and it \n'
			 'must be the default version. You may be able to \n'
			 'change the first line of your program\'s AppRun \n'
			 'file to end \'python2.2\' as a workaround.\n')
	raise SystemExit(1)

import i18n

_roxlib_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
_ = i18n.translation(os.path.join(_roxlib_dir, 'Messages'))

_old_sys_path = sys.path[:]

try:
	zhost = 'zero-install.sourceforge.net'
	zpath = '/uri/0install/' + zhost
	if not os.getenv('ROXLIB_DISABLE_ZEROINSTALL') and os.path.exists(zpath):
		zpath = os.path.join(zpath, 'libs/pygtk-2/platform/latest')
		if not os.path.exists(zpath):
			os.system('0refresh ' + zhost)
		if os.path.exists(zpath):
			sys.path.insert(0, zpath +
				'/lib/python%d.%d/site-packages' % sys.version_info[:2])
	import pygtk; pygtk.require('2.0')
	import gtk._gtk		# If this fails, don't leave a half-inited gtk
	import gtk; g = gtk	# Don't syntax error for python1.5
	assert g.Window		# Ensure not 1.2 bindings
except:
	try:
		# Try again without Zero Install
		sys.path = _old_sys_path
		if 'gtk' in sys.modules:
			del sys.modules['gtk']
		try:
			# Try to support 1.99.12, at least to show an error
			import pygtk; pygtk.require('2.0')
		except:
			print "Warning: very old pygtk!"
		import gtk; g = gtk	# Don't syntax error for python1.5
		assert g.Window		# Ensure not 1.2 bindings
	except:
		sys.stderr.write(_('The pygtk2 package (1.99.13 or later) must be '
			   'installed to use this program:\n'
			   'http://rox.sourceforge.net/rox_lib.php3\n'))
		raise

# Put argv back the way it was, now that Gtk has initialised
sys.argv[0] = _path

def _warn_old_findrox():
	try:
		import findrox
	except:
		return	# Don't worry too much if it's missing
	if not hasattr(findrox, 'version'):
		print >>sys.stderr, "WARNING from ROX-Lib: the version of " \
			"findrox.py used by this application (%s) is very " \
			"old and may cause problems." % app_dir
_warn_old_findrox()

# For backwards compatibility. Use True and False in new code.
TRUE = True
FALSE = False

class UserAbort(Exception):
	"""Raised when the user aborts an operation, eg by clicking on Cancel
	or pressing Escape."""
	def __init__(self, message = None):
		Exception.__init__(self,
			message or _("Operation aborted at user's request"))

def alert(message):
	"Display message in an error box. Return when the user closes the box."
	toplevel_ref()
	box = g.MessageDialog(None, 0, g.MESSAGE_ERROR, g.BUTTONS_OK, message)
	box.set_position(g.WIN_POS_CENTER)
	box.set_title(_('Error'))
	box.run()
	box.destroy()
	toplevel_unref()

def bug(message = "A bug has been detected in this program. Please report "
		  "the problem to the authors."):
	"Display an error message and offer a debugging prompt."
	try:
		raise Exception(message)
	except:
		type, value, tb = sys.exc_info()
		import debug
		debug.show_exception(type, value, tb, auto_details = True)

def croak(message):
	"""Display message in an error box, then quit the program, returning
	with a non-zero exit status."""
	alert(message)
	sys.exit(1)

def info(message):
	"Display informational message. Returns when the user closes the box."
	toplevel_ref()
	box = g.MessageDialog(None, 0, g.MESSAGE_INFO, g.BUTTONS_OK, message)
	box.set_position(g.WIN_POS_CENTER)
	box.set_title(_('Information'))
	box.run()
	box.destroy()
	toplevel_unref()

def confirm(message, stock_icon, action = None):
	"""Display a <Cancel>/<Action> dialog. Result is true if the user
	chooses the action, false otherwise. If action is given then that
	is used as the text instead of the default for the stock item. Eg:
	if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
	"""
	toplevel_ref()
	box = g.MessageDialog(None, 0, g.MESSAGE_QUESTION,
				g.BUTTONS_CANCEL, message)
	if action:
		button = ButtonMixed(stock_icon, action)
	else:
		button = g.Button(stock = stock_icon)
	button.set_flags(g.CAN_DEFAULT)
	button.show()
	box.add_action_widget(button, g.RESPONSE_OK)
	box.set_position(g.WIN_POS_CENTER)
	box.set_title(_('Confirm:'))
	box.set_default_response(g.RESPONSE_OK)
	resp = box.run()
	box.destroy()
	toplevel_unref()
	return resp == g.RESPONSE_OK

def report_exception():
	"""Display the current python exception in an error box, returning
	when the user closes the box. This is useful in the 'except' clause
	of a 'try' block. Uses rox.debug.show_exception()."""
	type, value, tb = sys.exc_info()
	import debug
	debug.show_exception(type, value, tb)

_icon_path = os.path.join(app_dir, '.DirIcon')
_window_icon = None
if os.path.exists(_icon_path):
	try:
		g.window_set_default_icon_list(g.gdk.pixbuf_new_from_file(_icon_path))
	except:
		# Older pygtk
		_window_icon = g.gdk.pixbuf_new_from_file(_icon_path)
del _icon_path

class Window(g.Window):
	"""This works in exactly the same way as a GtkWindow, except that
	it calls the toplevel_(un)ref functions for you automatically,
	and sets the window icon to <app_dir>/.DirIcon if it exists."""
	def __init__(*args, **kwargs):
		apply(g.Window.__init__, args, kwargs)
		toplevel_ref()
		args[0].connect('destroy', toplevel_unref)

		if _window_icon:
			args[0].set_icon(_window_icon)

class Dialog(g.Dialog):
	"""This works in exactly the same way as a GtkDialog, except that
	it calls the toplevel_(un)ref functions for you automatically."""
	def __init__(*args, **kwargs):
		apply(g.Dialog.__init__, args, kwargs)
		toplevel_ref()
		args[0].connect('destroy', toplevel_unref)

class ButtonMixed(g.Button):
	"""A button with a standard stock icon, but any label. This is useful
	when you want to express a concept similar to one of the stock ones."""
	def __init__(self, stock, message):
		"""Specify the icon and text for the new button. The text
		may specify the mnemonic for the widget by putting a _ before
		the letter, eg:
		button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
		g.Button.__init__(self)
	
		label = g.Label('')
		label.set_text_with_mnemonic(message)
		label.set_mnemonic_widget(self)

		image = g.image_new_from_stock(stock, g.ICON_SIZE_BUTTON)
		box = g.HBox(FALSE, 2)
		align = g.Alignment(0.5, 0.5, 0.0, 0.0)

		box.pack_start(image, FALSE, FALSE, 0)
		box.pack_end(label, FALSE, FALSE, 0)

		self.add(align)
		align.add(box)
		align.show_all()

_toplevel_windows = 0
_in_mainloops = 0
def mainloop():
	"""This is a wrapper around the gtk2.mainloop function. It only runs
	the loop if there are top level references, and exits when
	rox.toplevel_unref() reduces the count to zero."""
	global _toplevel_windows, _in_mainloops

	_in_mainloops = _in_mainloops + 1	# Python1.5 syntax
	try:
		while _toplevel_windows:
			g.main()
	finally:
		_in_mainloops = _in_mainloops - 1

def toplevel_ref():
	"""Increment the toplevel ref count. rox.mainloop() won't exit until
	toplevel_unref() is called the same number of times."""
	global _toplevel_windows
	_toplevel_windows = _toplevel_windows + 1

def toplevel_unref(*unused):
	"""Decrement the toplevel ref count. If this is called while in
	rox.mainloop() and the count has reached zero, then rox.mainloop()
	will exit. Ignores any arguments passed in, so you can use it
	easily as a callback function."""
	global _toplevel_windows
	assert _toplevel_windows > 0
	_toplevel_windows = _toplevel_windows - 1
	if _toplevel_windows == 0 and _in_mainloops:
		g.main_quit()

_host_name = None
def our_host_name():
	"""Try to return the canonical name for this computer. This is used
	in the drag-and-drop protocol to work out whether a drop is coming from
	a remote machine (and therefore has to be fetched differently)."""
	from socket import getfqdn
	global _host_name
	if _host_name:
		return _host_name
	try:
		_host_name = getfqdn()
	except:
		_host_name = 'localhost'
		alert("ROX-Lib socket.getfqdn() failed!")
	return _host_name

def escape(uri):
	"Convert each space to %20, etc"
	import re
	return re.sub('[^:-_./a-zA-Z0-9]',
		lambda match: '%%%02x' % ord(match.group(0)),
		_to_utf8(uri)[0])

def unescape(uri):
	"Convert each %20 to a space, etc"
	if '%' not in uri: return uri
	import re
	return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
		lambda match: chr(int(match.group(0)[1:], 16)),
		uri)

def get_local_path(uri):
	"""Convert 'uri' to a local path and return, if possible. If 'uri'
	is a resource on a remote machine, return None. URI is in the escaped form
	(%20 for space)."""
	if not uri:
		return None

	if uri[0] == '/':
		if uri[1:2] != '/':
			return unescape(uri)	# A normal Unix pathname
		i = uri.find('/', 2)
		if i == -1:
			return None	# //something
		if i == 2:
			return unescape(uri[2:])	# ///path
		remote_host = uri[2:i]
		if remote_host == our_host_name():
			return unescape(uri[i:])	# //localhost/path
		# //otherhost/path
	elif uri[:5].lower() == 'file:':
		if uri[5:6] == '/':
			return get_local_path(uri[5:])
	elif uri[:2] == './' or uri[:3] == '../':
		return unescape(uri)
	return None

app_options = None
def setup_app_options(program, leaf = 'Options.xml', site = None):
	"""Most applications only have one set of options. This function can be
	used to set up the default group. 'program' is the name of the
	directory to use and 'leaf' is the name of the file used to store the
	group. You can refer to the group using rox.app_options.

	If site is given, the basedir module is used for saving options (the
	new system). Otherwise, the deprecated choices module is used.

	See rox.options.OptionGroup."""
	global app_options
	assert not app_options
	from options import OptionGroup
	app_options = OptionGroup(program, leaf, site)

_options_box = None
def edit_options(options_file = None):
	"""Edit the app_options (set using setup_app_options()) using the GUI
	specified in 'options_file' (default <app_dir>/Options.xml).
	If this function is called again while the box is still open, the
	old box will be redisplayed to the user."""
	assert app_options

	global _options_box
	if _options_box:
		_options_box.present()
		return
	
	if not options_file:
		options_file = os.path.join(app_dir, 'Options.xml')
	
	import OptionsBox
	_options_box = OptionsBox.OptionsBox(app_options, options_file)

	def closed(widget):
		global _options_box
		assert _options_box == widget
		_options_box = None
	_options_box.connect('destroy', closed)
	_options_box.open()

try:
	import xml
except:
	alert(_("You do not have the Python 'xml' module installed, which "
	        "ROX-Lib2 requires. You need to install python-xmlbase "
	        "(this is a small package; the full PyXML package is not "
	        "required)."))

if g.pygtk_version[:2] == (1, 99) and g.pygtk_version[2] < 12:
	# 1.99.12 is really too old too, but RH8.0 uses it so we'll have
	# to work around any problems...
	sys.stderr.write('Your version of pygtk (%d.%d.%d) is too old. '
	      'Things might not work correctly.' % g.pygtk_version)

--- NEW FILE: applet.py ---
"""To create a panel applet for ROX-Filer, you should add a file called
AppletRun to your application. This is run when your applet it dragged onto
a panel. It works like the usual AppRun, except that it is passed the XID of
the GtkSocket widget that ROX-Filer creates for you on the panel.

A sample AppletRun might look like this:

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 12)
import rox
import sys
from rox import applet, g

plug = applet.Applet(sys.argv[1])

label = g.Label('Hello\\nWorld!')
plug.add(label)
plug.show_all()

rox.mainloop()
"""

import rox
from rox import g

_root_window = g.gdk.get_default_root_window()

class Applet(g.Plug):
	"""When your AppletRun file is used, create an Applet widget with
	the argument passed to AppletRun. Show the widget to make it appear
	in the panel. toplevel_* functions are called automatically."""
	def __init__(self, xid):
		"""xid is the sys.argv[1] passed to AppletRun."""
		xid = long(xid)
		g.Plug.__init__(self, xid)
		self.socket = g.gdk.window_foreign_new(xid)
		rox.toplevel_ref()
		self.connect('destroy', rox.toplevel_unref)
	
	def position_menu(self, menu):
		"""Use this as the third argument to Menu.popup()."""
		x, y, mods = _root_window.get_pointer()
		pos = self.socket.property_get('_ROX_PANEL_MENU_POS',
						'STRING', g.FALSE)
		if pos: pos = pos[2]
		if pos:
			side, margin = pos.split(',')
			margin = int(margin)
		else:
			side, margin = None, 2

		width, height = g.gdk.screen_width(), g.gdk.screen_height()
		
		req = menu.size_request()

		if side == 'Top':
			y = margin
			x -= 8 + req[0] / 4
		elif side == 'Bottom':
			y = height - margin - req[1]
			x -= 8 + req[0] / 4
		elif side == 'Left':
			x = margin
			y -= 16
		elif side == 'Right':
			x = width - margin - req[0]
			y -= 16
		else:
			x -= req[0] / 2
			y -= 32

		def limit(v, min, max):
			if v < min: return min
			if v > max: return max
			return v

		x = limit(x, 4, width - 4 - req[0])
		y = limit(y, 4, height - 4 - req[1])
		
		return (x, y, True)

--- NEW FILE: basedir.py ---
"""The freedesktop.org Base Directory specification provides a way for
applications to locate shared data and configuration:

	http://www.freedesktop.org/standards/

This module can be used to load and save from and to these directories.

Typical usage:

	from rox import basedir
	
	for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'):
		print "Load settings from", dir

	dir = basedir.save_config_path('mydomain.org', 'MyProg')
	print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2"

Note: see the rox.Options module for a higher-level API for managing options.
"""

from __future__ import generators
import os

_home = os.environ.get('HOME', '/')
xdg_data_home = os.environ.get('XDG_DATA_HOME',
			os.path.join(_home, '.local', 'share'))

xdg_data_dirs = [xdg_data_home] + \
	os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')

xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
			os.path.join(_home, '.config'))

xdg_config_dirs = [xdg_config_home] + \
	os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':')

xdg_data_dirs = filter(lambda x: x, xdg_data_dirs)
xdg_config_dirs = filter(lambda x: x, xdg_config_dirs)

def save_config_path(*resource):
	"""Ensure $XDG_CONFIG_HOME/<resource>/ exists, and return its path.
	'resource' should normally be the name of your application. Use this
	when SAVING configuration settings. Use the xdg_config_dirs variable
	for loading."""
	resource = os.path.join(*resource)
	assert not resource.startswith('/')
	path = os.path.join(xdg_config_home, resource)
	if not os.path.isdir(path):
		os.makedirs(path, 0700)
	return path

def save_data_path(*resource):
	"""Ensure $XDG_DATA_HOME/<resource>/ exists, and return its path.
	'resource' is the name of some shared resource. Use this when updating
	a shared (between programs) database. Use the xdg_data_dirs variable
	for loading."""
	resource = os.path.join(*resource)
	assert not resource.startswith('/')
	path = os.path.join(xdg_data_home, resource)
	if not os.path.isdir(path):
		os.makedirs(path)
	return path

def load_config_paths(*resource):
	"""Returns an iterator which gives each directory named 'resource' in the
	configuration search path. Information provided by earlier directories should
	take precedence over later ones (ie, the user's config dir comes first)."""
	resource = os.path.join(*resource)
	for config_dir in xdg_config_dirs:
		path = os.path.join(config_dir, resource)
		if os.path.exists(path): yield path

def load_first_config(*resource):
	"""Returns the first result from load_config_paths, or None if there is nothing
	to load."""
	for x in load_config_paths(*resource):
		return x
	return None

def load_data_paths(*resource):
	"""Returns an iterator which gives each directory named 'resource' in the
	shared data search path. Information provided by earlier directories should
	take precedence over later ones."""
	resource = os.path.join(*resource)
	for data_dir in xdg_data_dirs:
		path = os.path.join(data_dir, resource)
		if os.path.exists(path): yield path

--- NEW FILE: choices.py ---
"""This module implements the Choices system for user preferences.
The environment variable CHOICESPATH gives a list of directories to search
for choices. Changed choices are saved back to the first directory in the
list.

The choices system is DEPRECATED. See choices.migrate().
"""

import os, sys
from os.path import exists

_migrated = {}

try:
	path = os.environ['CHOICESPATH']
	paths = path.split(':')
except KeyError:
	paths = [ os.environ['HOME'] + '/Choices',
		  '/usr/local/share/Choices',
		  '/usr/share/Choices' ]
	
def load(dir, leaf):
	"""When you want to load user choices, use this function. 'dir' is
	the subdirectory within Choices where the choices are saved (usually
	this will be the name of your program). 'leaf' is the file within it.
	If serveral files are present, the most important one is returned. If
	no files are there, returns None.
	Eg ('Edit', 'Options') - > '/usr/local/share/Choices/Edit/Options'"""

	assert dir not in _migrated

	for path in paths:
		if path:
			full = path + '/' + dir + '/' + leaf
			if exists(full):
				return full

	return None

def save(dir, leaf, create = 1):
	"""Returns a path to save to, or None if saving is disabled.
	If 'create' is FALSE then no directories are created. 'dir' and
	'leaf' are as for load()."""

	assert dir not in _migrated

	p = paths[0]
	if not p:
		return None

	if create and not os.path.exists(p):
		os.mkdir(p, 0x1ff)
	p = p + '/' + dir
	if create and not os.path.exists(p):
		os.mkdir(p, 0x1ff)
		
	return p + '/' + leaf

def migrate(dir, site):
	"""Move <Choices>/dir (if it exists) to $XDG_CONFIG_HOME/site/dir, and
	put a symlink in its place. The choices load and save functions cannot
	be used for 'dir' after this; use the basedir module instead. 'site'
	should be a domain name owned or managed by you. Eg:
	choices.migrate('Edit', 'rox.sourceforge.net')"""
	assert dir not in _migrated

	_migrated[dir] = True

	home_choices = paths[0]
	if not home_choices:
		return		# Saving disabled
	
	full = home_choices + '/' + dir
	if os.path.islink(full) or not os.path.exists(full):
		return

	import basedir
	dest = os.path.join(basedir.xdg_config_home, site, dir)
	if os.path.exists(dest):
		print >>sys.stderr, \
			"Old config directory '%s' and new config " \
			"directory '%s' both exist. Not migrating settings!" % \
			(full, dest)
		return
	
	site_dir = os.path.join(basedir.xdg_config_home, site)
	if not os.path.isdir(site_dir):
		os.makedirs(site_dir)
	os.rename(full, dest)
	os.symlink(dest, full)

--- NEW FILE: filer.py ---
"""An easy way to get ROX-Filer to do things."""

# Note: do a double-fork in case it's an old version of the filer
# and doesn't automatically background itself.
def _spawn(argv):
	from os import fork, _exit, execvp, waitpid
	child = fork()
	if child == 0:
		# We are the child
		child = fork()
		if child == 0:
			# Grandchild
			try:
				execvp(argv[0], argv)
			except:
				pass
			print "Warning: exec('%s') failed!" % argv[0]
			_exit(1)
		elif child == -1:
			print "Error: fork() failed!"
		_exit(1)
	elif child == -1:
		print "Error: fork() failed!"
	waitpid(child, 0)

def spawn_rox(args):
	"""Run rox (either from PATH or through Zero Install) with the
	given arguments."""
	import os.path
	for bindir in os.environ.get('PATH', '').split(':'):
		path = os.path.join(bindir, 'rox')
		if os.path.isfile(path):
			_spawn(('rox',) + args)
			return
	if os.path.exists('/uri/0install/rox.sourceforge.net'):
		_spawn(('/bin/0run', 'rox.sourceforge.net/rox 2002-01-01') + args)
	else:
		print "Didn't find rox in PATH, and Zero Install not present. Trying 'rox' anyway..."
		_spawn(('rox',) + args)

def open_dir(dir):
	"Open 'dir' in a new filer window."
	spawn_rox(('-d', dir))

def examine(file):
	"""'file' may have changed (maybe you just created it, for example). Update
	any filer views of it."""
	spawn_rox(('-x', file))

def show_file(file):
	"""Open a directory and draw the user's attention to this file. Useful for
	'Up' toolbar buttons that show where a file is saved."""
	spawn_rox(('-s', file))

--- NEW FILE: i18n.py ---
"""If you want your program to be translated into multiple languages you need
to do the following:

- Pass all strings that should be translated through the '_' function, eg:
	print _('Hello World!')

- Create a Messages subdirectory in your application.

- Run 'pygettext *.py' to extract all the marked strings.

- Copy messages.pot as Messages/<lang>.po and edit (see ROX-Lib2's README).

- Use msgfmt to convert the .po files to .gmo files.

- In your application, use the rox.i18n.translation() function to set the _ function:
	__builtins__._ = rox.i18n.translation(os.path.join(rox.app_dir, 'Messages'))
  (for libraries, just do '_ ='; don't mess up the builtins)

Note that the marked strings must be fixed. If you're using formats, mark up the
format, eg:

	print _('You have %d lives remaining') % lives

You might like to look at the scripts in ROX-Lib2's Messages directory for
more help.
"""

import os

def _expand_lang(locale):
	from locale import normalize
	locale = normalize(locale)
	COMPONENT_CODESET   = 1 << 0
	COMPONENT_TERRITORY = 1 << 1
	COMPONENT_MODIFIER  = 1 << 2
	# split up the locale into its base components
	mask = 0
	pos = locale.find('@')
	if pos >= 0:
		modifier = locale[pos:]
		locale = locale[:pos]
		mask |= COMPONENT_MODIFIER
	else:
		modifier = ''
	pos = locale.find('.')
	if pos >= 0:
		codeset = locale[pos:]
		locale = locale[:pos]
		mask |= COMPONENT_CODESET
	else:
		codeset = ''
	pos = locale.find('_')
	if pos >= 0:
		territory = locale[pos:]
		locale = locale[:pos]
		mask |= COMPONENT_TERRITORY
	else:
		territory = ''
	language = locale
	ret = []
	for i in range(mask+1):
		if not (i & ~mask):  # if all components for this combo exist ...
			val = language
			if i & COMPONENT_TERRITORY: val += territory
			if i & COMPONENT_CODESET:   val += codeset
			if i & COMPONENT_MODIFIER:  val += modifier
			ret.append(val)
	ret.reverse()
	return ret

def expand_languages(languages = None):
	# Get some reasonable defaults for arguments that were not supplied
	if languages is None:
		languages = []
		for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
			val = os.environ.get(envar)
			if val:
				languages = val.split(':')
				break
        if 'C' not in languages:
		languages.append('C')

	# now normalize and expand the languages
	nelangs = []
	for lang in languages:
		for nelang in _expand_lang(lang):
			if nelang not in nelangs:
				nelangs.append(nelang)
	return nelangs

# Locate a .mo file using the ROX strategy
def find(messages_dir, languages = None):
	"""Look in messages_dir for a .gmo file for the user's preferred language
	(or override this with the 'languages' argument). Returns the filename, or
	None if there was no translation."""
	# select a language
	for lang in expand_languages(languages):
		if lang == 'C':
			break
		mofile = os.path.join(messages_dir, '%s.gmo' % lang)
		if os.path.exists(mofile):
			return mofile
	return None

def translation(messages_dir, languages = None):
	"""Load the translation for the user's language and return a function
	which translates a string into its unicode equivalent."""
	mofile = find(messages_dir, languages)
	if not mofile:
		return lambda x: x
	import gettext
	return gettext.GNUTranslations(file(mofile)).ugettext

langs = expand_languages()

--- NEW FILE: icon_theme.py ---
"""This is an internal module. Do not use it. GTK 2.4 will contain functions
that replace those defined here."""

import os
import basedir
import rox

theme_dirs = [os.path.join(os.environ.get('HOME', '/'), '.icons')] + \
		list(basedir.load_data_paths('icons'))

class Index:
	"""A theme's index.theme file."""
	def __init__(self, dir):
		self.dir = dir
		sections = file(os.path.join(dir, "index.theme")).read().split('\n[')
		self.sections = {}
		for s in sections:
			lines = s.split('\n')
			sname = lines[0].strip()
			
			# Python < 2.2.2 doesn't support an argument to strip...
			assert sname[-1] == ']'
			if sname.startswith('['):
				sname = sname[1:-1]
			else:
				sname = sname[:-1]
			
			section = self.sections[sname] = {}
			for line in lines[1:]:
				if not line.strip(): continue
				if line.startswith('#'): continue
				key, value = map(str.strip, line.split('=', 1))
				section[key] = value

		subdirs = self.get('Icon Theme', 'Directories')
		
		subdirs = subdirs.replace(';', ',')	# Just in case...
		
		self.subdirs = [SubDir(self, d) for d in subdirs.split(',')]

	def get(self, section, key):
		"None if not found"
		return self.sections.get(section, {}).get(key, None)

class SubDir:
	"""A subdirectory within a theme."""
	def __init__(self, index, subdir):
		icontype = index.get(subdir, 'Type')
		self.name = subdir
		self.size = int(index.get(subdir, 'Size'))
		if icontype == "Fixed":
			self.min_size = self.max_size = self.size
		elif icontype == "Threshold":
			threshold = int(index.get(subdir, 'Threshold'))
			self.min_size = self.size - threshold
			self.max_size = self.size + threshold
		elif icontype == "Scaled":
			self.min_size = int(index.get(subdir, 'MinSize'))
			self.max_size = int(index.get(subdir, 'MaxSize'))
		else:
			self.min_size = self.max_size = 100000

class IconTheme:
	"""Icon themes are located by searching through various directories. You can use an IconTheme
	to convert an icon name into a suitable image."""
	
	def __init__(self, name):
		self.name = name

		self.indexes = []
		for leaf in theme_dirs:
			theme_dir = os.path.join(leaf, name)
			index_file = os.path.join(theme_dir, 'index.theme')
			if os.path.exists(os.path.join(index_file)):
				try:
					self.indexes.append(Index(theme_dir))
				except:
					rox.report_exception()
	
	def lookup_icon(self, iconname, size):
		icon = self._lookup_this_theme(iconname, size)
		if icon: return icon
		# XXX: inherits
	
	def _lookup_this_theme(self, iconname, size):
		dirs = []
		for i in self.indexes:
			for d in i.subdirs:
				if size < d.min_size:
					diff = d.min_size - size
				elif size > d.max_size:
					diff = size - d.max_size
				else:
					diff = 0
				dirs.append((diff, os.path.join(i.dir, d.name)))

		# Sort by closeness of size
		dirs.sort()

		for _, subdir in dirs:
			for extension in ("png", "svg"):
				filename = os.path.join(subdir,
					iconname + '.' + extension)
				if os.path.exists(filename):
					return filename
		return None

rox_theme = IconTheme('ROX')

--- NEW FILE: mime.py ---
"""This module provides access to the shared MIME database.

types is a dictionary of all known MIME types, indexed by the type name, e.g.
types['application/x-python']

Applications can install information about MIME types by storing an
XML file as <MIME>/packages/<application>.xml and running the
update-mime-database command, which is provided by the freedesktop.org
shared mime database package.

See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
information about the format of these files."""

import os
import stat
import fnmatch

import rox
import rox.choices
from rox import i18n, _, basedir

from xml.dom import Node, minidom, XML_NAMESPACE

FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'

types = {}		# Maps MIME names to type objects

# Icon sizes when requesting MIME type icon
ICON_SIZE_HUGE=96
ICON_SIZE_LARGE=52
ICON_SIZE_SMALL=18
ICON_SIZE_UNSCALED=None

exts = None		# Maps extensions to types
globs = None		# List of (glob, type) pairs
literals = None		# Maps liternal names to types
magic = None

def _get_node_data(node):
	"""Get text of XML node"""
	return ''.join([n.nodeValue for n in node.childNodes]).strip()

def lookup(media, subtype = None):
	"Get the MIMEtype object for this type, creating a new one if needed."
	if subtype is None and '/' in media:
		media, subtype = media.split('/', 1)
	if (media, subtype) not in types:
		types[(media, subtype)] = MIMEtype(media, subtype)
	return types[(media, subtype)]

class MIMEtype:
	"""Type holding data about a MIME type"""
	def __init__(self, media, subtype):
		"Don't use this constructor directly; use mime.lookup() instead."
		assert media and '/' not in media
		assert subtype and '/' not in subtype
		assert (media, subtype) not in types

		self.media = media
		self.subtype = subtype
		self._comment = None
	
	def _load(self):
		"Loads comment for current language. Use get_comment() instead."
		resource = os.path.join('mime', self.media, self.subtype + '.xml')
		for path in basedir.load_data_paths(resource):
			doc = minidom.parse(path)
			if doc is None:
				continue
			for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
				lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
				goodness = 1 + (lang in i18n.langs)
				if goodness > self._comment[0]:
					self._comment = (goodness, _get_node_data(comment))
				if goodness == 2: return

	def get_comment(self):
		"""Returns comment for current language, loading it if needed."""
		# Should we ever reload?
		if self._comment is None:
			self._comment = (0, str(self))
			self._load()
		return self._comment[1]

	def __str__(self):
		return self.media + '/' + self.subtype

	def __repr__(self):
		return '[%s: %s]' % (self, self._comment or '(comment not loaded)')

	def get_icon(self, size=None):
		"""Return a GdkPixbuf with the icon for this type.  If size
		is None then the image is returned at its natural size,
		otherwise the image is scaled to that width with the height
		at the correct aspect ratio.  The constants
		ICON_SIZE_{HUGE,LARGE,SMALL} match the sizes used by the
		filer."""
		# I suppose it would make more sense to move the code
		# from saving to here...
		import saving
		base=saving.image_for_type(self.media + '/' + self.subtype)
		if not base or not size:
			return base

		h=int(base.get_width()*float(size)/base.get_height())
		return base.scale_simple(size, h, rox.g.gdk.INTERP_BILINEAR)

class MagicRule:
	def __init__(self, f):
		self.next=None
		self.prev=None

		#print line
		ind=''
		while True:
			c=f.read(1)
			if c=='>':
				break
			ind+=c
		if not ind:
			self.nest=0
		else:
			self.nest=int(ind)

		start=''
		while True:
			c=f.read(1)
			if c=='=':
				break
			start+=c
		self.start=int(start)
		
		hb=f.read(1)
		lb=f.read(1)
		self.lenvalue=ord(lb)+(ord(hb)<<8)

		self.value=f.read(self.lenvalue)

		c=f.read(1)
		if c=='&':
			self.mask=f.read(self.lenvalue)
			c=f.read(1)
		else:
			self.mask=None

		if c=='~':
			w=''
			while c!='+' and c!='\n':
				c=f.read(1)
				if c=='+' or c=='\n':
					break
				w+=c
			
			self.word=int(w)
		else:
			self.word=1

		if c=='+':
			r=''
			while c!='\n':
				c=f.read(1)
				if c=='\n':
					break
				r+=c
			#print r
			self.range=int(r)
		else:
			self.range=1

		if c!='\n':
			raise 'Malformed MIME magic line'

	def getLength(self):
		return self.start+self.lenvalue+self.range

	def appendRule(self, rule):
		if self.nest<rule.nest:
			self.next=rule
			rule.prev=self

		elif self.prev:
			self.prev.appendRule(rule)
		
	def match(self, buffer):
		if self.match0(buffer):
			if self.next:
				return self.next.match(buffer)
			return True

	def match0(self, buffer):
		l=len(buffer)
		for o in range(self.range):
			s=self.start+o
			e=s+self.lenvalue
			if l<e:
				return False
			if self.mask:
				test=''
				for i in range(self.lenvalue):
					c=ord(buffer[s+i]) & ord(self.mask[i])
					test+=chr(c)
			else:
				test=buffer[s:e]

			if test==self.value:
				return True

	def __repr__(self):
		return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
							      self.start,
							      self.lenvalue,
							      `self.value`,
							      `self.mask`,
							      self.word,
							      self.range)

class MagicType:
	def __init__(self, mtype):
		self.mtype=mtype
		self.top_rules=[]
		self.last_rule=None

	def getLine(self, f):
		nrule=MagicRule(f)

		if nrule.nest and self.last_rule:
			self.last_rule.appendRule(nrule)
		else:
			self.top_rules.append(nrule)

		self.last_rule=nrule

		return nrule

	def match(self, buffer):
		for rule in self.top_rules:
			if rule.match(buffer):
				return self.mtype

	def __repr__(self):
		return '<MagicType %s>' % self.mtype
	
class MagicDB:
	def __init__(self):
		self.types={}   # Indexed by priority, each entry is a list of type rules
		self.maxlen=0

	def mergeFile(self, fname):
		f=file(fname, 'r')
		line=f.readline()
		if line!='MIME-Magic\0\n':
			raise 'Not a MIME magic file'

		while True:
			shead=f.readline()
			#print shead
			if not shead:
				break
			if shead[0]!='[' or shead[-2:]!=']\n':
				raise 'Malformed section heading'
			pri, tname=shead[1:-2].split(':')
			#print shead[1:-2]
			pri=int(pri)
			mtype=lookup(tname)

			try:
				ents=self.types[pri]
			except:
				ents=[]
				self.types[pri]=ents

			magictype=MagicType(mtype)
			#print tname

			#rline=f.readline()
			c=f.read(1)
			f.seek(-1, 1)
			while c and c!='[':
				rule=magictype.getLine(f)
				#print rule
				if rule and rule.getLength()>self.maxlen:
					self.maxlen=rule.getLength()

				c=f.read(1)
				f.seek(-1, 1)

			ents.append(magictype)
			#self.types[pri]=ents
			if not c:
				break

	def match(self, path, max_pri=100, min_pri=0):
		try:
			buf=file(path, 'r').read(self.maxlen)
			pris=self.types.keys()
			pris.sort(lambda a, b: -cmp(a, b))
			for pri in pris:
				#print pri, max_pri, min_pri
				if pri>max_pri:
					continue
				if pri<min_pri:
					break
				for type in self.types[pri]:
					m=type.match(buf)
					if m:
						return m
		except:
			pass
		
		return None
	
	def __repr__(self):
		return '<MagicDB %s>' % self.types
			

# Some well-known types
text = lookup('text', 'plain')
inode_block = lookup('inode', 'blockdevice')
inode_char = lookup('inode', 'chardevice')
inode_dir = lookup('inode', 'directory')
inode_fifo = lookup('inode', 'fifo')
inode_socket = lookup('inode', 'socket')
inode_symlink = lookup('inode', 'symlink')
inode_door = lookup('inode', 'door')
app_exe = lookup('application', 'executable')

_cache_uptodate = False

def _cache_database():
	global exts, globs, literals, magic, _cache_uptodate

	_cache_uptodate = True

	exts = {}		# Maps extensions to types
	globs = []		# List of (glob, type) pairs
	literals = {}		# Maps liternal names to types
	magic = MagicDB()

	def _import_glob_file(path):
		"""Loads name matching information from a MIME directory."""
		for line in file(path):
			if line.startswith('#'): continue
			line = line[:-1]

			type_name, pattern = line.split(':', 1)
			mtype = lookup(type_name)

			if pattern.startswith('*.'):
				rest = pattern[2:]
				if not ('*' in rest or '[' in rest or '?' in rest):
					exts[rest] = mtype
					continue
			if '*' in pattern or '[' in pattern or '?' in pattern:
				globs.append((pattern, mtype))
			else:
				literals[pattern] = mtype

	for path in basedir.load_data_paths(os.path.join('mime', 'globs')):
		_import_glob_file(path)
	for path in basedir.load_data_paths(os.path.join('mime', 'magic')):
		magic.mergeFile(path)

	# Sort globs by length
	globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))

def get_type_by_name(path):
	"""Returns type of file by its name, or None if not known"""
	if not _cache_uptodate:
		_cache_database()

	leaf = os.path.basename(path)
	if leaf in literals:
		return literals[leaf]

	lleaf = leaf.lower()
	if lleaf in literals:
		return literals[lleaf]

	ext = leaf
	while 1:
		p = ext.find('.')
		if p < 0: break
		ext = ext[p + 1:]
		if ext in exts:
			return exts[ext]
	ext = lleaf
	while 1:
		p = ext.find('.')
		if p < 0: break
		ext = ext[p+1:]
		if ext in exts:
			return exts[ext]
	for (glob, mime_type) in globs:
		if fnmatch.fnmatch(leaf, glob):
			return mime_type
		if fnmatch.fnmatch(lleaf, glob):
			return mime_type
	return None

def get_type_by_contents(path, max_pri=100, min_pri=0):
	"""Returns type of file by its contents, or None if not known"""
	if not _cache_uptodate:
		_cache_database()

	return magic.match(path, max_pri, min_pri)

def get_type(path, follow=1, name_pri=100):
	"""Returns type of file indicated by path.
	path	 - pathname to check (need not exist)
	follow   - when reading file, follow symbolic links
	name_pri - Priority to do name matches.  100=override magic"""
	if not _cache_uptodate:
		_cache_database()
	
	try:
		if follow:
			st = os.stat(path)
		else:
			st = os.lstat(path)
	except:
		t = get_type_by_name(path)
		return t or text

	if stat.S_ISREG(st.st_mode):
		t = get_type_by_contents(path, min_pri=name_pri)
		if not t: t = get_type_by_name(path)
		if not t: t = get_type_by_contents(path, max_pri=name_pri)
		if t is None:
			if stat.S_IMODE(st.st_mode) & 0111:
				return app_exe
			else:
				return text
		return t
	elif stat.S_ISDIR(st.st_mode): return inode_dir
	elif stat.S_ISCHR(st.st_mode): return inode_char
	elif stat.S_ISBLK(st.st_mode): return inode_block
	elif stat.S_ISFIFO(st.st_mode): return inode_fifo
	elif stat.S_ISLNK(st.st_mode): return inode_symlink
	elif stat.S_ISSOCK(st.st_mode): return inode_socket
	return inode_door

def install_mime_info(application, package_file = None):
	"""Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
	If package_file is None, install <app_dir>/<application>.xml.
	If already installed, does nothing. May overwrite an existing
	file with the same name (if the contents are different)"""
	application += '.xml'
	if not package_file:
		package_file = os.path.join(rox.app_dir, application)
	
	new_data = file(package_file).read()

	# See if the file is already installed
		
	package_dir = os.path.join('mime', 'packages')
	resource = os.path.join(package_dir, application)
	for x in basedir.load_data_paths(resource):
		try:
			old_data = file(x).read()
		except:
			continue
		if old_data == new_data:
			return	# Already installed

	global _cache_uptodate
	_cache_uptodate = False
	
	# Not already installed; add a new copy
	try:
		# Create the directory structure...
		new_file = os.path.join(basedir.save_data_path(package_dir), application)

		# Write the file...
		file(new_file, 'w').write(new_data)

		# Update the database...
		if os.path.isdir('/uri/0install/zero-install.sourceforge.net'):
			command = '/uri/0install/zero-install.sourceforge.net/bin/update-mime-database'
		else:
			command = 'update-mime-database'
		if os.spawnlp(os.P_WAIT, command, command, basedir.save_data_path('mime')):
			os.unlink(new_file)
			raise Exception(_("The '%s' command returned an error code!\n" \
					  "Make sure you have the freedesktop.org shared MIME package:\n" \
					  "http://www.freedesktop.org/standards/shared-mime-info.html") % command)
	except:
		rox.report_exception()

def _test(name):
	"""Print results for name.  Test routine"""
	t=get_type(name, name_pri=80)
	print name, t, t.get_comment()

if __name__=='__main__':
	import sys
	if len(sys.argv)<2:
		_test('file.txt')
	else:
		for f in sys.argv[1:]:
			_test(f)
	#print globs

--- NEW FILE: saving.py ---
"""All ROX applications that can save documents should use drag-and-drop saving.
The document itself should use the Saveable mix-in class and override some of the
methods to actually do the save.

If you want to save a selection then you can create a new object specially for
the purpose and pass that to the SaveBox."""

import os, sys
import rox
from rox import alert, info, g, _, filer, escape
from rox import choices, get_local_path

gdk = g.gdk

TARGET_XDS = 0
TARGET_RAW = 1

def _write_xds_property(context, value):
	win = context.source_window
	if value:
		win.property_change('XdndDirectSave0', 'text/plain', 8,
					gdk.PROP_MODE_REPLACE,
					value)
	else:
		win.property_delete('XdndDirectSave0')

def _read_xds_property(context, delete):
	"""Returns a UTF-8 encoded, non-escaped, URI."""
	win = context.source_window
	retval = win.property_get('XdndDirectSave0', 'text/plain', delete)
	if retval:
		return retval[2]
	return None
	
def image_for_type(type):
	'Search <Choices> for a suitable icon. Returns a pixbuf, or None.'
	from icon_theme import rox_theme
	media, subtype = type.split('/', 1)
	path = choices.load('MIME-icons', media + '_' + subtype + '.png')
	if not path:
		icon = 'mime-%s:%s' % (media, subtype)
		try:
			path = rox_theme.lookup_icon(icon, 48)
			if not path:
				icon = 'mime-%s' % media
				path = rox_theme.lookup_icon(icon, 48)
		except:
			print "Error loading MIME icon"
	if not path:
		path = choices.load('MIME-icons', media + '.png')
	if path:
		return gdk.pixbuf_new_from_file(path)
	else:
		return None

def _report_save_error():
	"Report a AbortSave nicely, otherwise use report_exception()"
	value = sys.exc_info()[1]
	if isinstance(value, AbortSave):
		value.show()
	else:
		rox.report_exception()

class AbortSave(rox.UserAbort):
	"""Raise this to cancel a save. If a message is given, it is displayed
	in a normal alert box (not in the report_exception style). If the
	message is None, no message is shown (you should have already shown
	it!)"""
	def __init__(self, message):
		self.message = message
		rox.UserAbort.__init__(self, message)
	
	def show(self):
		if self.message:
			rox.alert(self.message)

class Saveable:
	"""This class describes the interface that an object must provide
	to work with the SaveBox/SaveArea widgets. Inherit from it if you
	want to save. All methods can be overridden, but normally only
	save_to_stream() needs to be. You can also set save_last_stat to
	the result of os.stat(filename) when loading a file to make ROX-Lib
	restore permissions and warn about other programs editing the file."""

	save_last_stat = None

	def set_uri(self, uri):
		"""When the data is safely saved somewhere this is called
		with its new name. Mark your data as unmodified and update
		the filename for next time. Saving to another application
		won't call this method. Default method does nothing.
		The URI may be in the form of a URI or a local path.
		It is UTF-8, not escaped (% really means %)."""
		pass

	def save_to_stream(self, stream):
		"""Write the data to save to the stream. When saving to a
		local file, stream will be the actual file, otherwise it is a
		cStringIO object."""
		raise Exception('You forgot to write the save_to_stream() method...'
				'silly programmer!')

	def save_to_file(self, path):
		"""Write data to file. Raise an exception on error.
		The default creates a temporary file, uses save_to_stream() to
		write to it, then renames it over the original. If the temporary file
		can't be created, it writes directly over the original."""

		# Ensure the directory exists...
		parent_dir = os.path.dirname(path)
		if not os.path.isdir(parent_dir):
			from rox import fileutils
			try:
				fileutils.makedirs(parent_dir)
			except OSError:
				raise AbortSave(None)	# (message already shown)
		
		import random
		tmp = 'tmp-' + `random.randrange(1000000)`
		tmp = os.path.join(parent_dir, tmp)

		def open_private(path):
			return os.fdopen(os.open(path, os.O_CREAT | os.O_WRONLY, 0600), 'wb')
		
		try:
			stream = open_private(tmp)
		except:
			# Can't create backup... try a direct write
			tmp = None
			stream = open_private(path)
		try:
			try:
				self.save_to_stream(stream)
			finally:
				stream.close()
			if tmp:
				os.rename(tmp, path)
		except:
			_report_save_error()
			if tmp and os.path.exists(tmp):
				if os.path.getsize(tmp) == 0 or \
				   rox.confirm(_("Delete temporary file '%s'?") % tmp,
				   		g.STOCK_DELETE):
					os.unlink(tmp)
			raise AbortSave(None)
		self.save_set_permissions(path)
		filer.examine(path)

	def save_to_selection(self, selection_data):
		"""Write data to the selection. The default method uses save_to_stream()."""
		from cStringIO import StringIO
		stream = StringIO()
		self.save_to_stream(stream)
		selection_data.set(selection_data.target, 8, stream.getvalue())

	save_mode = None	# For backwards compat
	def save_set_permissions(self, path):
		"""The default save_to_file() creates files with the mode 0600
		(user read/write only). After saving has finished, it calls this
		method to set the final permissions. The save_set_permissions():
		- sets it to 0666 masked with the umask (if save_mode is None), or
		- sets it to save_last_stat.st_mode (not masked) otherwise."""
		if self.save_last_stat is not None:
			save_mode = self.save_last_stat.st_mode
		else:
			save_mode = self.save_mode
		
		if save_mode is not None:
			os.chmod(path, save_mode)
		else:
			mask = os.umask(0077)	# Get the current umask
			os.umask(mask)		# Set it back how it was
			os.chmod(path, 0666 & ~mask)
	
	def save_done(self):
		"""Time to close the savebox. Default method does nothing."""
		pass

	def discard(self):
		"""Discard button clicked, or document safely saved. Only called if a SaveBox 
		was created with discard=1.
		The user doesn't want the document any more, even if it's modified and unsaved.
		Delete it."""
		raise Exception("Sorry... my programmer forgot to tell me how to handle Discard!")
	
	save_to_stream._rox_default = 1
	save_to_file._rox_default = 1
	save_to_selection._rox_default = 1
	def can_save_to_file(self):
		"""Indicates whether we have a working save_to_stream or save_to_file
		method (ie, whether we can save to files). Default method checks that
		one of these two methods has been overridden."""
		if not hasattr(self.save_to_stream, '_rox_default'):
			return 1	# Have user-provided save_to_stream
		if not hasattr(self.save_to_file, '_rox_default'):
			return 1	# Have user-provided save_to_file
		return 0
	def can_save_to_selection(self):
		"""Indicates whether we have a working save_to_stream or save_to_selection
		method (ie, whether we can save to selections). Default methods checks that
		one of these two methods has been overridden."""
		if not hasattr(self.save_to_stream, '_rox_default'):
			return 1	# Have user-provided save_to_stream
		if not hasattr(self.save_to_selection, '_rox_default'):
			return 1	# Have user-provided save_to_file
		return 0
	
	def save_cancelled(self):
		"""If you multitask during a save (using a recursive mainloop) then the
		user may click on the Cancel button. This function gets called if so, and
		should cause the recursive mainloop to return."""
		raise Exception("Lazy programmer error: can't abort save!")

class SaveArea(g.VBox):
	"""A SaveArea contains the widgets used in a save box. You can use
	this to put a savebox area in a larger window."""
	
	document = None		# The Saveable with the data
	entry = None
	initial_uri = None	# The pathname supplied to the constructor
	
	def __init__(self, document, uri, type):
		"""'document' must be a subclass of Saveable.
		'uri' is the file's current location, or a simple name (eg 'TextFile')
		if it has never been saved.
		'type' is the MIME-type to use (eg 'text/plain').
		"""
		g.VBox.__init__(self, False, 0)

		self.document = document
		self.initial_uri = uri

		drag_area = self._create_drag_area(type)
		self.pack_start(drag_area, True, True, 0)
		drag_area.show_all()

		entry = g.Entry()
		entry.connect('activate', lambda w: self.save_to_file_in_entry())
		self.entry = entry
		self.pack_start(entry, False, True, 4)
		entry.show()

		entry.set_text(uri)
	
	def _set_icon(self, type):
		pixbuf = image_for_type(type)
		if pixbuf:
			self.icon.set_from_pixbuf(pixbuf)
		else:
			self.icon.set_from_stock(g.STOCK_MISSING_IMAGE, g.ICON_SIZE_DND)

	def _create_drag_area(self, type):
		align = g.Alignment()
		align.set(.5, .5, 0, 0)

		self.drag_box = g.EventBox()
		self.drag_box.set_border_width(4)
		self.drag_box.add_events(gdk.BUTTON_PRESS_MASK)
		align.add(self.drag_box)

		self.icon = g.Image()
		self._set_icon(type)

		self._set_drag_source(type)
		self.drag_box.connect('drag_begin', self.drag_begin)
		self.drag_box.connect('drag_end', self.drag_end)
		self.drag_box.connect('drag_data_get', self.drag_data_get)
		self.drag_in_progress = 0

		self.drag_box.add(self.icon)

		return align

	def set_type(self, type, icon = None):
		"""Change the icon and drag target to 'type'.
		If 'icon' is given (as a GtkImage) then that icon is used,
		otherwise an appropriate icon for the type is used."""
		if icon:
			self.icon.set_from_pixbuf(icon.get_pixbuf())
		else:
			self._set_icon(type)
		self._set_drag_source(type)
	
	def _set_drag_source(self, type):
		if self.document.can_save_to_file():
			targets = [('XdndDirectSave0', 0, TARGET_XDS)]
		else:
			targets = []
		if self.document.can_save_to_selection():
			targets = targets + [(type, 0, TARGET_RAW),
				  ('application/octet-stream', 0, TARGET_RAW)]

		if not targets:
			raise Exception("Document %s can't save!" % self.document)
		self.drag_box.drag_source_set(gdk.BUTTON1_MASK | gdk.BUTTON3_MASK,
					      targets,
					      gdk.ACTION_COPY | gdk.ACTION_MOVE)
	
	def save_to_file_in_entry(self):
		"""Call this when the user clicks on an OK button you provide."""
		uri = self.entry.get_text()
		path = get_local_path(escape(uri))

		if path:
			if not self.confirm_new_path(path):
				return
			try:
				self.set_sensitive(False)
				try:
					self.document.save_to_file(path)
				finally:
					self.set_sensitive(True)
				self.set_uri(uri)
				self.save_done()
			except:
				_report_save_error()
		else:
			rox.info(_("Drag the icon to a directory viewer\n"
				   "(or enter a full pathname)"))
	
	def drag_begin(self, drag_box, context):
		self.drag_in_progress = 1
		self.destroy_on_drag_end = 0
		self.using_xds = 0
		self.data_sent = 0

		try:
			pixbuf = self.icon.get_pixbuf()
			if pixbuf:
				drag_box.drag_source_set_icon_pixbuf(pixbuf)
		except:
			# This can happen if we set the broken image...
			import traceback
			traceback.print_exc()

		uri = self.entry.get_text()
		if uri:
			i = uri.rfind('/')
			if (i == -1):
				leaf = uri
			else:
				leaf = uri[i + 1:]
		else:
			leaf = _('Unnamed')
		_write_xds_property(context, leaf)
	
	def drag_data_get(self, widget, context, selection_data, info, time):
		if info == TARGET_RAW:
			try:
				self.set_sensitive(False)
				try:
					self.document.save_to_selection(selection_data)
				finally:
					self.set_sensitive(True)
			except:
				_report_save_error()
				_write_xds_property(context, None)
				return

			self.data_sent = 1
			_write_xds_property(context, None)
			
			if self.drag_in_progress:
				self.destroy_on_drag_end = 1
			else:
				self.save_done()
			return
		elif info != TARGET_XDS:
			_write_xds_property(context, None)
			alert("Bad target requested!")
			return

		# Using XDS:
		#
		# Get the path that the destination app wants us to save to.
		# If it's local, save and return Success
		#			  (or Error if save fails)
		# If it's remote, return Failure (remote may try another method)
		# If no URI is given, return Error
		to_send = 'E'
		uri = _read_xds_property(context, False)
		if uri:
			path = get_local_path(escape(uri))
			if path:
				if not self.confirm_new_path(path):
					to_send = 'E'
				else:
					try:
						self.set_sensitive(False)
						try:
							self.document.save_to_file(path)
						finally:
							self.set_sensitive(True)
						self.data_sent = True
					except:
						_report_save_error()
						self.data_sent = False
					if self.data_sent:
						to_send = 'S'
				# (else Error)
			else:
				to_send = 'F'	# Non-local transfer
		else:
			alert("Remote application wants to use " +
				  "Direct Save, but I can't read the " +
				  "XdndDirectSave0 (type text/plain) " +
				  "property.")

		selection_data.set(selection_data.target, 8, to_send)
	
		if to_send != 'E':
			_write_xds_property(context, None)
			self.set_uri(uri)
		if self.data_sent:
			self.save_done()
	
	def confirm_new_path(self, path):
		"""User wants to save to this path. If it's different to the original path,
		check that it doesn't exist and ask for confirmation if it does.
		If document.save_last_stat is set, compare with os.stat for an existing file
		and warn about changes.
		Returns true to go ahead with the save."""
		if not os.path.exists(path):
			return True
		if path == self.initial_uri:
			if self.document.save_last_stat is None:
				return True		# OK. Nothing to compare with.
			last = self.document.save_last_stat
			stat = os.stat(path)
			msg = []
			if stat.st_mode != last.st_mode:
				msg.append(_("Permissions changed from %o to %o.") % \
						(last.st_mode, stat.st_mode))
			if stat.st_size != last.st_size:
				msg.append(_("Size was %d bytes; now %d bytes.") % \
						(last.st_size, stat.st_size))
			if stat.st_mtime != last.st_mtime:
				msg.append(_("Modification time changed."))
			if not msg:
				return True		# No change detected
			return rox.confirm("File '%s' edited by another program since last load/save. "
					   "Really save (discarding other changes)?\n\n%s" %
					   (path, '\n'.join(msg)), g.STOCK_DELETE)
		return rox.confirm(_("File '%s' already exists -- overwrite it?") % path,
				   g.STOCK_DELETE, _('_Overwrite'))
	
	def set_uri(self, uri):
		"""Data is safely saved somewhere. Update the document's URI and save_last_stat (for local saves).
		URI is not escaped. Internal."""
		path = get_local_path(escape(uri))
		if path is not None:
			self.document.save_last_stat = os.stat(path)	# Record for next time
		self.document.set_uri(path or uri)
	
	def drag_end(self, widget, context):
		self.drag_in_progress = 0
		if self.destroy_on_drag_end:
			self.save_done()

	def save_done(self):
		self.document.save_done()

class SaveBox(g.Dialog):
	"""A SaveBox is a GtkDialog that contains a SaveArea and, optionally, a Discard button.
	Calls rox.toplevel_(un)ref automatically.
	"""
	save_area = None

	def __init__(self, document, uri, type = 'text/plain', discard = False):
		"""See SaveArea.__init__.
		If discard is True then an extra discard button is added to the dialog."""
		g.Dialog.__init__(self)
		self.set_has_separator(False)

		self.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
		self.add_button(g.STOCK_SAVE, g.RESPONSE_OK)
		self.set_default_response(g.RESPONSE_OK)

		if discard:
			discard_area = g.HButtonBox()

			def discard_clicked(event):
				document.discard()
				self.destroy()
			button = rox.ButtonMixed(g.STOCK_DELETE, _('_Discard'))
			discard_area.pack_start(button, False, True, 2)
			button.connect('clicked', discard_clicked)
			button.unset_flags(g.CAN_FOCUS)
			button.set_flags(g.CAN_DEFAULT)
			self.vbox.pack_end(discard_area, False, True, 0)
			self.vbox.reorder_child(discard_area, 0)
			
			discard_area.show_all()

		self.set_title(_('Save As:'))
		self.set_position(g.WIN_POS_MOUSE)
		self.set_wmclass('savebox', 'Savebox')
		self.set_border_width(1)

		# Might as well make use of the new nested scopes ;-)
		self.set_save_in_progress(0)
		class BoxedArea(SaveArea):
			def set_uri(area, uri):
				SaveArea.set_uri(area, uri)
				if discard:
					document.discard()
			def save_done(area):
				document.save_done()
				self.destroy()

			def set_sensitive(area, sensitive):
				if self.window:
					# Might have been destroyed by now...
					self.set_save_in_progress(not sensitive)
					SaveArea.set_sensitive(area, sensitive)
		save_area = BoxedArea(document, uri, type)
		self.save_area = save_area

		save_area.show_all()
		self.build_main_area()

		i = uri.rfind('/')
		i = i + 1
		# Have to do this here, or the selection gets messed up
		save_area.entry.grab_focus()
		g.Editable.select_region(save_area.entry, i, -1) # PyGtk bug
		#save_area.entry.select_region(i, -1)

		def got_response(widget, response):
			if self.save_in_progress:
				try:
					document.save_cancelled()
				except:
					rox.report_exception()
				return
			if response == g.RESPONSE_CANCEL:
				self.destroy()
			elif response == g.RESPONSE_OK:
				self.save_area.save_to_file_in_entry()
			elif response == g.RESPONSE_DELETE_EVENT:
				pass
			else:
				raise Exception('Unknown response!')
		self.connect('response', got_response)

		rox.toplevel_ref()
		self.connect('destroy', lambda w: rox.toplevel_unref())
	
	def set_type(self, type, icon = None):
		"""See SaveArea's method of the same name."""
		self.save_area.set_type(type, icon)

	def build_main_area(self):
		"""Place self.save_area somewhere in self.vbox. Override this
		for more complicated layouts."""
		self.vbox.add(self.save_area)
	
	def set_save_in_progress(self, in_progress):
		"""Called when saving starts and ends. Shade/unshade any widgets as
		required. Make sure you call the default method too!
		Not called if box is destroyed from a recursive mainloop inside
		a save_to_* function."""
		self.set_response_sensitive(g.RESPONSE_OK, not in_progress)
		self.save_in_progress = in_progress

class StringSaver(SaveBox, Saveable):
	"""A very simple SaveBox which saves the string passed to its constructor."""
	def __init__(self, string, name):
		"""'string' is the string to save. 'name' is the default filename"""
		SaveBox.__init__(self, self, name, 'text/plain')
		self.string = string
	
	def save_to_stream(self, stream):
		stream.write(self.string)

class SaveFilter(Saveable):
	"""This Saveable runs a process in the background to generate the
	save data. Any python streams can be used as the input to and
	output from the process.
	
	The output from the subprocess is saved to the output stream (either
	directly, for fileno() streams, or via another temporary file).

	If the process returns a non-zero exit status or writes to stderr,
	the save fails (messages written to stderr are displayed).
	"""

	command = None
	stdin = None

	def set_stdin(self, stream):
		"""Use 'stream' as stdin for the process. If stream is not a
		seekable fileno() stream then it is copied to a temporary file
		at this point. If None, the child process will get /dev/null on
		stdin."""
		if stream is not None:
			if hasattr(stream, 'fileno') and hasattr(stream, 'seek'):
				self.stdin = stream
			else:
				import tempfile
				import shutil
				self.stdin = tempfile.TemporaryFile()
				shutil.copyfileobj(stream, self.stdin)
		else:
			self.stdin = None
	
	def save_to_stream(self, stream):
		from processes import PipeThroughCommand

		assert not hasattr(self, 'child_run')	# No longer supported

		self.process = PipeThroughCommand(self.command, self.stdin, stream)
		self.process.wait()
		self.process = None
	
	def save_cancelled(self):
		"""Send SIGTERM to the child processes."""
		if self.process:
			self.killed = 1
			self.process.kill()




More information about the Thunar-workers mailing list