rendered paste body#!/usr/bin/env python
# coding=utf-8
'''
gnome-about
# Pretty About Dialog for the GNOME Desktop #
Copyright (C) 2007 Guillaume Seguin <guillaume@segu.in>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
Authors :
Guillaume Seguin <guillaume@segu.in>
Vincent Untz <vuntz@gnome.org> (get_language_names () helper function)
'''
import pygtk
pygtk.require ('2.0')
import gobject
import gtk
import gnome
import cairo
from math import pi
import os, sys, random, time, gettext
import xml.dom.minidom
GETTEXT_DOMAIN = "gnome-desktop-2.0"
PREFIX = "/usr"
DATADIR = "%s/share/gnome-about" % PREFIX
ICONDIR = "%s/share/pixmaps" % PREFIX
LOCALES_DIR = "%s/share/locale" % PREFIX
VERSION = "2.21"
gettext.install (GETTEXT_DOMAIN, LOCALES_DIR,
unicode = True)
header_links = [
(_("About GNOME"), "http://www.gnome.org/about/"),
(_("News"), "http://news.gnome.org/"),
(_("GNOME Library"), "http://library.gnome.org/"),
(_("GNOME Live!"), "http://live.gnome.org/"),
(_("Friends of GNOME"), "http://www.gnome.org/friends/"),
(_("Contact"), "http://www.gnome.org/contact/"),
]
translated_contributors = [
_("The Mysterious GEGL"),
_("The Squeaky Rubber GNOME"),
_("Wanda The GNOME Fish")
]
def locate_file (program, file):
'''Wrap gnome_program_locate_file to avoid ugly duplication'''
file = program.locate_file (gnome.FILE_DOMAIN_APP_DATADIR, file, True)
if not file:
return False
return file[0]
def cleanup_date (date):
'''Parse a date as found in gnome-version.xml and nicely format it'''
try:
return time.strftime ("%x", time.strptime (date, "%Y-%m-%d"))
except:
return False
# Imported from GNOME's Sabayon
# (sabayon/admin-tool/lockdown/disabledapplets.py)
# There's no wrapper for g_get_language_names (). Ugly workaround:
# Note that we don't handle locale alias...
def get_language_names ():
if "LANGUAGE" in os.environ.keys () and os.environ["LANGUAGE"] != "":
env_lang = os.environ["LANGUAGE"].split ()
elif "LC_ALL" in os.environ.keys () and os.environ["LC_ALL"] != "":
env_lang = os.environ["LC_ALL"].split ()
elif "LC_MESSAGES" in os.environ.keys () and os.environ["LC_MESSAGES"] != "":
env_lang = os.environ["LC_MESSAGES"].split ()
elif "LANG" in os.environ.keys () and os.environ["LANG"] != "":
env_lang = os.environ["LANG"].split ()
else:
env_lang = []
env_lang.reverse ()
languages = []
for language in env_lang:
start_pos = 0
mask = 0
uscore_pos = language.find ("_")
if uscore_pos != -1:
start_pos = uscore_pos
mask += 1 << 2
dot_pos = language.find (".", start_pos)
if dot_pos != -1:
start_pos = dot_pos
mask += 1 << 0
at_pos = language.find ("@", start_pos)
if at_pos != -1:
start_pos = at_pos
mask += 1 << 1
if uscore_pos != -1:
lang = language[:uscore_pos]
elif dot_pos != -1:
lang = language[:dot_pos]
elif at_pos != -1:
lang = language[:at_pos]
else:
lang = language
if uscore_pos != -1:
if dot_pos != -1:
territory = language[uscore_pos:dot_pos]
elif at_pos != -1:
territory = language[uscore_pos:at_pos]
else:
territory = language[uscore_pos:]
else:
territory = ""
if dot_pos != -1:
if at_pos != -1:
codeset = language[dot_pos:at_pos]
else:
codeset = language[dot_pos:]
else:
codeset = ""
if at_pos != -1:
modifier = language[at_pos:]
else:
modifier = ""
for i in range (mask + 1):
if i & ~mask == 0:
newlang = lang
if (i & 1 << 2):
newlang += territory
if (i & 1 << 0):
newlang += codeset
if (i & 1 << 1):
newlang += modifier
languages.insert (0, newlang)
return languages
class GettableList (list):
'''Dumb wrapper around Python list with a get () method to iterate \
the list'''
current = 0
def get (self):
item = self[self.current]
self.current = (self.current + 1) % len (self)
return item
class GnomeContributors (GettableList):
'''Randomized contributors list'''
program = None
current = 0
def __init__ (self, program):
'''Initiate object and load contributors lists'''
super (GnomeContributors, self).__init__ ()
self.program = program
map (self.append, translated_contributors)
self.load_from_file ("contributors.list")
self.load_from_file ("foundation-members.list")
random.shuffle (self) # Randomize list...
def load_from_file (self, file):
'''Load a list of contributors and validate it'''
def validate_contributor (contrib):
try:
contrib.encode ("utf8")
return len (contrib) > 0 and contrib[0] != "#"
except Exception, e:
print e
return False
path = locate_file (self.program, file)
if not path:
raise Warning, _("%s file not found.") % file
f = open (path, "r")
try:
data = f.readlines ()
finally:
f.close ()
'''Cleanup list'''
data = map (lambda s: s.rstrip (), data)
'''Check that the file begins with the correct header'''
if not data or data[0] != "# gnome-about contributors - format 1":
return
'''Filter the contributors list and append it'''
contributors = filter (validate_contributor, data)
map (self.append, contributors)
def get (self):
'''Return a contributor from the currently randomized list. \
If we hit the end of the list, randomize it again'''
contributors_count = len (self)
if not contributors_count:
raise Warning, _("Empty contributors list.")
contributor = self[self.current]
self.current += 1
if self.current > contributors_count:
'''Remove the last item of the list and reinsert it after \
shuffling to make sure it won't get displayed twice in a row'''
self.pop ()
random.shuffle (self)
index = random.randint (contributors_count / 2,
contributors_count)
self.insert (index, contributor)
return contributor
class AnimatedLabel (gtk.Layout):
'''Pretty animated label'''
items = None # items must either be a GettableList object
# or expose a get () method'''
timeout = 0
format = "" # format must be a valid formatting string for a single %s
current = None
label = None
source = None
state = 0 # 0 = appearing ; 1 = landed ; 2 = vanishing
pos = 0.0
width = 0
height = 0
def __init__ (self, items, width, height, timeout, format = "%s"):
'''Initiate object'''
super (AnimatedLabel, self).__init__ ()
self.items = items
self.width = width
self.height = height
self.timeout = timeout
self.format = format
self.set_size_request (width, height)
self.connect ("button-press-event", self.on_button_press)
self.connect ("map", self.reset_animation)
def reset_animation (self, *args):
'''Reset label and fire animation timer'''
self.reset_label ()
self.source = gobject.timeout_add (5, self.animate)
def reset_label (self):
'''Drop current label if any and create the new one'''
if self.current:
self.remove (self.current)
item = self.items.get ()
self.label = gtk.Label ()
self.label.set_justify (gtk.JUSTIFY_FILL)
self.label.set_line_wrap (True)
self.label.set_markup (self.format % item)
self.label.set_selectable (True)
self.state = 0
def on_button_press (self, widget, event):
'''Switch to next item upon left click'''
if event.button != 1:
return
# Remove the current timeout if any to avoid bad side effects
if self.source:
gobject.source_remove (self.source)
self.reset_animation ()
class VertAnimatedLabel (AnimatedLabel):
def animate (self):
if self.state == 0:
if self.pos:
self.pos = max (0, self.pos - 0.02)
label_height = self.label.size_request ()[1]
total_height = self.height + label_height
real_pos = float (self.pos * self.height + label_height) \
/ total_height
self.current.set (0.5, real_pos, 0, 0)
self.source = gobject.timeout_add (5, self.animate)
else:
self.state = 1
self.source = gobject.timeout_add (self.timeout, self.animate)
elif self.state == 1:
self.state = 2
self.pos = 1.0
if self.state == 2:
if self.pos:
self.pos = max (0, self.pos - 0.02)
label_height = self.label.size_request ()[1]
total_height = self.height + label_height
real_pos = float (self.pos * label_height) \
/ total_height
self.current.set (0.5, real_pos, 0, 0)
self.source = gobject.timeout_add (5, self.animate)
else:
self.reset_animation ()
return False
def reset_label (self):
super (VertAnimatedLabel, self).reset_label ()
self.label.set_size_request (self.width, -1)
self.current = gtk.Alignment (0.0, 1.0)
label_height = self.label.size_request ()[1]
height = self.size_request ()[1]
self.current.set_size_request (-1, 2 * label_height + height)
self.current.add (self.label)
self.put (self.current, 0, - label_height)
self.pos = 1.0
self.show_all ()
class HorzAnimatedLabel (AnimatedLabel):
def animate (self):
if self.state == 0:
if self.pos != 0.5:
self.pos = max (0.5, self.pos - 0.02)
self.current.set (self.pos, 0.5, 0, 0)
self.source = gobject.timeout_add (5, self.animate)
else:
self.state = 1
self.source = gobject.timeout_add (self.timeout, self.animate)
elif self.state == 1:
self.state = 2
if self.state == 2:
if self.pos:
self.pos = max (0, self.pos - 0.02)
self.current.set (self.pos, 0.5, 0, 0)
self.source = gobject.timeout_add (5, self.animate)
else:
self.reset_animation ()
return False
def reset_label (self):
super (HorzAnimatedLabel, self).reset_label ()
self.label.set_size_request (-1, self.height)
self.current = gtk.Alignment (1.0, 0.0)
label_width = self.label.size_request ()[0]
width = self.size_request ()[0]
self.current.set_size_request (2 * label_width + width, -1)
self.current.add (self.label)
self.put (self.current, - label_width, 0)
self.pos = 1.0
self.show_all ()
class HyperLink (gtk.Label):
'''Clickable www link label'''
url = ""
menu = None
selection = None
def __init__ (self, label, url):
'''Initialize object and plug all signals'''
super (HyperLink, self).__init__ ()
markup = "<b><u>%s</u></b>" % label
self.set_markup (markup)
self.set_selectable (True)
self.url = url
self.create_menu ()
self.connect ("button-press-event", self.on_button_press)
self.connect ("button-release-event", self.on_button_release)
self.connect ("key-press-event", self.on_key_press)
self.connect ("realize", self.on_realize)
self.connect ("size-allocate", self.on_size_allocate)
self.connect ("map-event", self.on_map)
self.connect ("unmap-event", self.on_unmap)
def open_url (self, *args):
'''Use GNOME API to open the url'''
try:
gnome.url_show (self.url)
except Exception, e:
raise Warning, _('''Could not open the address "%s" : %s''') % \
self.url, e
def copy_url (self, *args):
'''Copy URL to Clipboard'''
clipboard = gtk.clipboard_get ("CLIPBOARD")
clipboard.set_text (self.url)
def create_menu (self):
'''Create the popup menu that will be displayed upon right click'''
self.menu = gtk.Menu ()
open_item = gtk.ImageMenuItem (_("Open URL"))
open_image = gtk.image_new_from_stock (gtk.STOCK_OPEN,
gtk.ICON_SIZE_MENU)
open_item.set_image (open_image)
open_item.connect ("activate", self.open_url)
open_item.show ()
self.menu.append (open_item)
copy_item = gtk.ImageMenuItem (_("Copy URL"))
copy_image = gtk.image_new_from_stock (gtk.STOCK_COPY,
gtk.ICON_SIZE_MENU)
copy_item.set_image (copy_image)
copy_item.connect ("activate", self.copy_url)
copy_item.show ()
self.menu.append (copy_item)
def display_menu (self, button, time):
'''Display utility popup menu'''
self.menu.popup (None, None, None, button, time)
def on_realize (self, *args):
'''Create a custom GDK window with which we will be able to play '''
self.event_window = gtk.gdk.Window (self.get_parent_window (),
self.allocation.width,
self.allocation.height,
gtk.gdk.WINDOW_CHILD,
self.allocation.x,
self.allocation.y)
self.event_window.set_user_data (self)
def on_size_allocate (self, widget, allocation):
'''Move & resize the event window to fit the Label's one'''
self.event_window.move_resize (allocation.x, allocation.y,
allocation.width, allocation.height)
def on_map (self, *args):
'''Show event window and select the HAND2 cursor on map'''
self.event_window.show ()
cursor = gtk.gdk.Cursor (gtk.gdk.HAND2)
self.event_window.set_cursor (cursor)
'''Raise the event window to make sure it is over the Label's one'''
self.event_window.raise_ ()
def on_unmap (self, *args):
'''Hide event window on unmap'''
self.event_window.hide ()
def on_button_press (self, widget, event):
'''Update selection bounds infos or display popup menu'''
if event.button == 1:
self.selection = self.get_selection_bounds ()
elif event.button == 3:
self.display_menu (event.button, event.time)
return True
def on_button_release (self, widget, event):
'''Open url if selection hasn't changed since initial press'''
if event.button == 1:
selection = self.get_selection_bounds ()
if selection == self.selection:
self.open_url ()
def on_key_press (self, widget, event):
'''Open url when Return key is pressed'''
if event.keyval == gtk.keysyms.Return:
self.open_url ()
return True
class GnomeAboutHeader (gtk.Layout):
'''Pretty header for gnome-about'''
program = None
links = []
width = 0
height = 0
def __init__ (self, program, links):
'''Initialize object, plug map signal'''
super (GnomeAboutHeader, self).__init__ ()
self.program = program
self.links = links
self.connect_after ("realize", self.on_realize)
def on_realize (self, *args):
'''Load header and build links'''
current_x = 0
current_y = 0
base_y = 0
header = self.load_header ()
if header:
self.put (header, 0, 0)
current_y = header.get_pixbuf ().get_height ()
base_y = current_y + 4
line = self.create_line ()
image = gtk.Image ()
image.set_from_pixmap (line, None)
self.put (image, 0, current_y)
logo = self.load_logo ()
if logo:
self.put (logo, 10, 10)
current_x += logo.get_pixbuf ().get_width () + 25
current_y = logo.get_pixbuf ().get_height () + 20
dot = self.create_dot ()
def make_link_widget (link):
label = HyperLink (link[0], link[1])
label.show_all ()
return label
widgets = map (make_link_widget, self.links)
put_widgets = 0
for widget in widgets:
if put_widgets > 0:
if dot:
image = gtk.Image ()
image.set_from_pixmap (dot, None)
self.put (image, current_x + 5, base_y + 6)
current_x += 16
self.put (widget, current_x, base_y)
current_x += widget.size_request ()[0]
put_widgets += 1
self.width = current_x + 10
self.height = current_y
self.set_size_request (self.width, self.height)
self.show_all ()
def load_header (self):
'''Load a random header image as a gtk.Image'''
directory = locate_file (self.program, "headers")
if not directory:
raise Warning, _("Header images directory not found.")
return None
try:
headers = os.listdir (directory)
except:
raise Warning, _("Failed to read header images directory.")
return None
headers = filter (lambda s: s[-4:] in (".png", ".gif"), headers)
header = random.choice (headers)
file = os.path.join (directory, header)
try:
pixbuf = gtk.gdk.pixbuf_new_from_file (file)
except:
raise Warning, _("Failed to load header image %s.") % file
return None
image = gtk.Image ()
image.set_from_pixbuf (pixbuf)
return image
def load_logo (self):
'''Load a GNOME foot logo as a gtk.Image'''
file = locate_file (self.program, "gnome-64.png")
if not file:
raise Warning, _("GNOME logo file %s not found.") % "gnome-64.png"
return None
try:
pixbuf = gtk.gdk.pixbuf_new_from_file (file)
except:
raise Warning, _("Failed to load GNOME logo image %s.") % file
return None
image = gtk.Image ()
image.set_from_pixbuf (pixbuf)
return image
def create_dot (self):
'''Create a pixmap containing a simple dot'''
pixmap = gtk.gdk.Pixmap (self.window, 6, 6)
context = pixmap.cairo_create ()
context.set_operator (cairo.OPERATOR_SOURCE)
context.set_source_color (self.style.bg[self.state])
context.paint ()
context.set_operator (cairo.OPERATOR_OVER)
context.set_source_color (self.style.fg[self.state])
context.arc (3, 3, 2.3, 0, 2 * pi)
context.fill ()
return pixmap
def create_line (self, width = 2000, height = 1):
'''Create a pixmap containing a simple line'''
pixmap = gtk.gdk.Pixmap (self.window, width, height)
context = pixmap.cairo_create ()
context.set_operator (cairo.OPERATOR_SOURCE)
context.set_source_rgb (0, 0, 0)
context.paint ()
return pixmap
class GnomeAbout (gtk.Dialog):
'''Super pretty About Dialog for the GNOME Desktop'''
program = None
header = None
contributors = None
description_messages = []
system_infos = []
def __init__ (self):
'''Initialize underlying gnome.Program, Contributors list, UI...'''
super (GnomeAbout, self).__init__ (_("About the GNOME Desktop"),
buttons = (gtk.STOCK_CLOSE,
gtk.RESPONSE_CLOSE))
defs = {gnome.PARAM_APP_DATADIR : DATADIR}
self.program = gnome.program_init ("gnome-about", VERSION,
properties = defs)
self.contributors = GnomeContributors (self.program)
icon_file = ICONDIR + "/gnome-logo-icon-transparent.png"
try:
self.set_icon_from_file (icon_file)
except gobject.GError:
pass
# Immediately fetch system infos to load description messages
self.system_infos = self.get_system_infos ()
self.create_ui ()
self.set_default_response (gtk.RESPONSE_CLOSE)
self.set_position (gtk.WIN_POS_CENTER_ALWAYS)
self.set_properties ("allow-grow", False, "allow-shrink", False)
self.connect ("delete-event", gtk.main_quit)
self.connect ("response", self.response_callback)
def create_ui (self):
'''Fill our Dialog with some lovely widgets'''
main_box = self.get_children ()[0] # Get the internal Dialog VBox
'''Pretty header'''
self.header = GnomeAboutHeader (self.program, header_links)
main_box.pack_start (self.header)
welcome_label = gtk.Label ()
welcome_label.set_markup ("<big><big><b>%s</b></big></big>" % \
_("Welcome to the GNOME Desktop"))
main_box.pack_start (welcome_label)
descriptions_label = VertAnimatedLabel (self.description_messages,
300, 150, 5000, "%s")
alignment = gtk.Alignment (0.5, 0.5)
alignment.set_padding (10, 10, 0, 0)
alignment.add (descriptions_label)
main_box.pack_start (alignment)
by_label = gtk.Label ()
by_label.set_markup ("<big><b>%s</b></big>" % \
_("Brought to you by :"))
main_box.pack_start (by_label)
alignment = gtk.Alignment (0.5, 0.5)
self.header.connect_after ("realize", self.make_contributors_label,
alignment)
main_box.pack_start (alignment)
'''System info labels'''
def make_info_label (info):
if not info[1]:
return False
label = gtk.Label ()
label.set_markup ("<b>%s :</b> %s" % (info[0], info[1]))
label.set_selectable (True)
alignment = gtk.Alignment (0, 0.5)
alignment.set_padding (0, 4, 8, 0)
alignment.add (label)
return alignment
info_labels = map (make_info_label, self.system_infos)
info_labels = filter (lambda l: l <> False, info_labels)
map (lambda w: main_box.pack_start (w, False, False), info_labels)
def make_contributors_label (self, widget, alignment):
width = self.header.width
label = HorzAnimatedLabel (self.contributors, width,
30, 3000, "<b>%s</b>")
label.show_all ()
alignment.add (label)
def get_system_infos (self):
'''Fetch various system infos'''
file = locate_file (self.program, "gnome-version.xml")
if not file:
raise Warning, _("gnome-version.xml file not found.")
return []
f = open (file, "r")
try:
document = xml.dom.minidom.parse (f)
finally:
f.close ()
if document.firstChild.nodeName != "gnome-version":
raise Warning, _("Corrupted gnome-version.xml.")
return []
infos = {
"platform" : "",
"minor" : "",
"micro" : "",
"distributor" : "",
"date" : ""
}
for node in document.firstChild.childNodes:
if node.nodeName in infos:
infos[node.nodeName] = node.firstChild.nodeValue
elif node.nodeName == "description":
self.load_description_messages (node)
'''Format version'''
if not len (infos["minor"]):
version = infos["platform"]
elif not len (infos["micro"]):
version = "%s.%s" % (infos["platform"], infos["minor"])
else:
version = "%s.%s.%s" % (infos["platform"], infos["minor"],
infos["micro"])
return [
(_("Version"), version),
(_("Distributor"), infos["distributor"]),
(_("Build Date"), cleanup_date (infos["date"]))
]
def load_description_messages (self, node):
'''Find the best translation of each description message'''
languages = get_language_names () + [""]
def desc_filter_func (node):
'''Helper filter function for XML descriptions'''
return node.nodeName == "p" \
and node.getAttribute ("xml:lang") in languages
def desc_sort_func (a, b):
'''Helper sorting function to sort translated messages'''
return cmp (languages.index (a), languages.index (b))
descs = filter (desc_filter_func, node.childNodes)
raw_descs = filter (lambda n: n.getAttribute ("xml:lang") == "", descs)
i = -1
translations = []
for desc in raw_descs:
new_i = descs.index (desc)
if i != - 1:
translations.append (descs[i:new_i])
i = new_i
messages = GettableList ()
for block in translations:
sorted_descs = sorted (block, cmp = desc_sort_func,
key = lambda n: n.getAttribute ("xml:lang"))
best = sorted_descs[0].firstChild.nodeValue
messages.append (best)
self.description_messages = messages
def response_callback (self, widget, response):
'''Handle dialog response when Close button is triggered'''
if response == gtk.RESPONSE_CLOSE:
gtk.main_quit ()
if __name__ == "__main__":
GnomeAbout ().show_all ()
gtk.main ()