#!/usr/bin/env python # coding=utf-8 ''' gnome-about # Pretty About Dialog for the GNOME Desktop # Copyright (C) 2007 Guillaume Seguin 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 Vincent Untz (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 = "%s" % 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 ("%s" % \ _("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 ("%s" % \ _("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 ("%s : %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, "%s") 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 ()