/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- * * Copyright (C) 2011,2012 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * 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, see . * * Authors: Robert Ancell * Michael Terry */ class BackgroundLoader : Object { public string filename {get; private set;} public Cairo.Surface logo {get; set;} public int[] widths; public int[] heights; public Cairo.Pattern[] patterns; private Cairo.Surface target_surface; private bool draw_grid; private unowned Thread thread; private Gdk.Pixbuf[] images; private bool finished; private uint ready_id; public signal void loaded (); public BackgroundLoader (Cairo.Surface target_surface, string filename, int[] widths, int[] heights, bool draw_grid) { this.target_surface = target_surface; this.filename = filename; this.widths = widths; this.heights = heights; patterns = new Cairo.Pattern[widths.length]; images = new Gdk.Pixbuf[widths.length]; this.draw_grid = draw_grid; } public bool load () { /* Already loaded */ if (finished) return true; /* Currently loading */ if (thread != null) return false; /* No monitor data */ if (widths.length == 0) return false; var text = "Making background %s at %dx%d".printf (filename, widths[0], heights[0]); for (var i = 1; i < widths.length; i++) text += ",%dx%d".printf (widths[i], heights[i]); debug (text); var color = Gdk.RGBA (); if (color.parse (filename)) { var pattern = new Cairo.Pattern.rgba (color.red, color.green, color.blue, color.alpha); for (var i = 0; i < widths.length; i++) patterns[i] = pattern; finished = true; debug ("Render of background %s complete", filename); return true; } else { try { this.ref(); thread = Thread.create (load_and_scale, true); } catch (ThreadError e) { this.unref(); finished = true; return true; } } return false; } public Cairo.Pattern? get_pattern (int width, int height) { for (var i = 0; i < widths.length; i++) { if (widths[i] == width && heights[i] == height) return patterns[i]; } return null; } ~BackgroundLoader () { if (ready_id > 0) Source.remove (ready_id); ready_id = 0; } private bool ready_cb () { debug ("Render of background %s complete", filename); thread.join (); thread = null; finished = true; for (var i = 0; i < widths.length; i++) { patterns[i] = create_pattern (images[i]); images[i] = null; } loaded (); this.unref(); return false; } private void* load_and_scale () { try { var image = new Gdk.Pixbuf.from_file (filename); for (var i = 0; i < widths.length; i++) images[i] = scale (image, widths[i], heights[i]); } catch (Error e) { debug ("Error loading background: %s", e.message); } ready_id = Gdk.threads_add_idle (ready_cb); return null; } private Gdk.Pixbuf? scale (Gdk.Pixbuf? image, int width, int height) { var target_aspect = (double) width / height; var aspect = (double) image.width / image.height; double scale, offset_x = 0, offset_y = 0; if (aspect > target_aspect) { /* Fit height and trim sides */ scale = (double) height / image.height; offset_x = (image.width * scale - width) / 2; } else { /* Fit width and trim top and bottom */ scale = (double) width / image.width; offset_y = (image.height * scale - height) / 2; } var scaled_image = new Gdk.Pixbuf (image.colorspace, image.has_alpha, image.bits_per_sample, width, height); image.scale (scaled_image, 0, 0, width, height, -offset_x, -offset_y, scale, scale, Gdk.InterpType.BILINEAR); return scaled_image; } private Cairo.Pattern? create_pattern (Gdk.Pixbuf image) { var grid_x_offset = get_grid_offset (image.width); var grid_y_offset = get_grid_offset (image.height); /* Create background */ var surface = new Cairo.Surface.similar (target_surface, Cairo.Content.COLOR, image.width, image.height); var bc = new Cairo.Context (surface); Gdk.cairo_set_source_pixbuf (bc, image, 0, 0); bc.paint (); /* Draw logo */ if (logo != null) { bc.save (); var y = (int) (image.height / grid_size - 2) * grid_size + grid_y_offset; bc.translate (grid_x_offset, y); bc.set_source_surface (logo, 0, 0); bc.paint_with_alpha (0.5); bc.restore (); } var pattern = new Cairo.Pattern.for_surface (surface); pattern.set_extend (Cairo.Extend.REPEAT); return pattern; } } public class Monitor { public int x; public int y; public int width; public int height; public Monitor (int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; } } public class Background : Gtk.Fixed { public enum DrawFlags { NONE, GRID, } public string default_background {get; set; default = UGSettings.get_string (UGSettings.KEY_BACKGROUND_COLOR);} public string? current_background {get; set; default = null;} public bool draw_grid {get; set; default = true;} public double alpha {get; private set; default = 1.0;} private Cairo.Surface target_surface; private List monitors = null; private Monitor? active_monitor = null; private AnimateTimer timer; private BackgroundLoader current; private BackgroundLoader old; private HashTable loaders; private Cairo.Surface? version_logo_surface = null; private int version_logo_width; private int version_logo_height; private Cairo.Surface? background_logo_surface = null; private int background_logo_width; private int background_logo_height; public Background (Cairo.Surface target_surface) { this.target_surface = target_surface; timer = new AnimateTimer (AnimateTimer.ease_in_out, 700); timer.animate.connect (animate_cb); loaders = new HashTable (str_hash, str_equal); notify["current-background"].connect (() => {reload ();}); } public void set_logo (string version_logo, string background_logo) { version_logo_surface = load_image (version_logo, out version_logo_width, out version_logo_height); background_logo_surface = load_image (background_logo, out background_logo_width, out background_logo_height); } private Cairo.Surface? load_image (string filename, out int width, out int height) { width = height = 0; try { var image = new Gdk.Pixbuf.from_file (filename); width = image.width; height = image.height; var surface = new Cairo.Surface.similar (target_surface, Cairo.Content.COLOR_ALPHA, image.width, image.height); var c = new Cairo.Context (surface); Gdk.cairo_set_source_pixbuf (c, image, 0, 0); c.paint (); return surface; } catch (Error e) { debug ("Failed to load background component %s: %s", filename, e.message); } return null; } public void set_monitors (List monitors) { this.monitors = new List (); foreach (var m in monitors) this.monitors.append (m); queue_draw (); } public void set_active_monitor (Monitor? monitor) { active_monitor = monitor; } public override void size_allocate (Gtk.Allocation allocation) { var resized = allocation.height != get_allocated_height () || allocation.width != get_allocated_width (); base.size_allocate (allocation); /* Regenerate backgrounds */ if (resized) { debug ("Regenerating backgrounds"); loaders.remove_all (); load_background (null); reload (); } } public override bool draw (Cairo.Context c) { var flags = DrawFlags.NONE; if (draw_grid) flags |= DrawFlags.GRID; draw_full (c, flags); return base.draw (c); } public void draw_full (Cairo.Context c, DrawFlags flags) { c.save (); /* Test whether we ran into an error loading this background */ if (current == null || (current.load () && current.patterns[0] == null)) { /* We couldn't load it, so swap it out for the default background and remember that choice */ var new_background = load_background (null); if (current != null) loaders.insert (current.filename, new_background); if (old == current) old = new_background; current = new_background; } /* Fade to this background when loaded */ if (current.load () && current != old && !timer.is_running) { alpha = 0.0; timer.reset (); } c.set_source_rgba (0.0, 0.0, 0.0, 0.0); var old_painted = false; /* Draw old background */ if (old != null && old.load () && (alpha < 1.0 || !current.load ())) { draw_background (c, old, 1.0); old_painted = true; } /* Draw new background */ if (current.load () && alpha > 0.0) draw_background (c, current, old_painted ? alpha : 1.0); c.restore (); if ((flags & DrawFlags.GRID) != 0) overlay_grid (c); } private void draw_background (Cairo.Context c, BackgroundLoader background, double alpha) { foreach (var monitor in monitors) { var pattern = background.get_pattern (monitor.width, monitor.height); if (pattern == null) continue; c.save (); pattern = background.get_pattern (monitor.width, monitor.height); var matrix = Cairo.Matrix.identity (); matrix.translate (-monitor.x, -monitor.y); pattern.set_matrix (matrix); c.set_source (pattern); c.rectangle (monitor.x, monitor.y, monitor.width, monitor.height); c.clip (); c.paint_with_alpha (alpha); c.restore (); if (monitor != active_monitor && background_logo_surface != null) { var width = background_logo_width; var height = background_logo_height; c.save (); pattern = new Cairo.Pattern.for_surface (background_logo_surface); matrix = Cairo.Matrix.identity (); var x = monitor.x + (monitor.width - width) / 2; var y = monitor.y + (monitor.height - height) / 2; matrix.translate (-x, -y); pattern.set_matrix (matrix); c.set_source (pattern); c.rectangle (x, y, width, height); c.clip (); c.paint_with_alpha (alpha); c.restore (); } } } private void animate_cb (double progress) { alpha = progress; queue_draw (); /* Stop when we get there */ if (alpha >= 1.0) old = current; } private void reload () { if (get_realized ()) { var new_background = load_background (current_background); if (current != new_background) { old = current; current = new_background; alpha = 1.0; timer.stop (); } queue_draw (); } } private BackgroundLoader load_background (string? filename) { if (filename == null) filename = default_background; var b = loaders.lookup (filename); if (b == null) { /* Load required sizes to draw background */ var widths = new int[monitors.length ()]; var heights = new int[monitors.length ()]; var n_sizes = 0; foreach (var monitor in monitors) { if (monitor_is_unique_size (monitor)) { widths[n_sizes] = monitor.width; heights[n_sizes] = monitor.height; n_sizes++; } } widths.resize (n_sizes); heights.resize (n_sizes); b = new BackgroundLoader (target_surface, filename, widths, heights, draw_grid); b.logo = version_logo_surface; b.loaded.connect (() => {reload();}); b.load (); loaders.insert (filename, b); } return b; } /* Check if a monitor has a unique size */ private bool monitor_is_unique_size (Monitor monitor) { foreach (var m in monitors) { if (m == monitor) break; else if (m.width == monitor.width && m.height == monitor.height) return false; } return true; } private void overlay_grid (Cairo.Context c) { var width = get_allocated_width (); var height = get_allocated_height (); var grid_x_offset = get_grid_offset (width); var grid_y_offset = get_grid_offset (height); /* Overlay grid */ var overlay_surface = new Cairo.Surface.similar (target_surface, Cairo.Content.COLOR_ALPHA, grid_size, grid_size); var oc = new Cairo.Context (overlay_surface); oc.rectangle (0, 0, 1, 1); oc.rectangle (grid_size - 1, 0, 1, 1); oc.rectangle (0, grid_size - 1, 1, 1); oc.rectangle (grid_size - 1, grid_size - 1, 1, 1); oc.set_source_rgba (1.0, 1.0, 1.0, 0.25); oc.fill (); var overlay = new Cairo.Pattern.for_surface (overlay_surface); var matrix = Cairo.Matrix.identity (); matrix.translate (-grid_x_offset, -grid_y_offset); overlay.set_matrix (matrix); overlay.set_extend (Cairo.Extend.REPEAT); /* Draw overlay */ c.save (); c.set_source (overlay); c.rectangle (0, 0, width, height); c.fill (); c.restore (); } }