///////////////////////////////////////////////////////////////////////// // Copyright 2011-2018 Simon Schneegans // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ///////////////////////////////////////////////////////////////////////// using GLib.Math; namespace GnomePie { ///////////////////////////////////////////////////////////////////////// /// A theme of Gnome-Pie. Can be loaded from XML-Files. ///////////////////////////////////////////////////////////////////////// public class Theme : GLib.Object { ///////////////////////////////////////////////////////////////////// /// Properties of a theme. ///////////////////////////////////////////////////////////////////// public string directory {get; private set; default="";} public string name {get; private set; default="";} public string description {get; private set; default="";} public string author {get; private set; default="";} public string email {get; private set; default="";} public double radius {get; private set; default=150;} public double max_zoom {get; private set; default=1.2;} public double zoom_range {get; private set; default=0.2;} public double transition_time {get; private set; default=0.5;} public double wobble {get; private set; default=0.0;} public double fade_in_time {get; private set; default=0.2;} public double fade_out_time {get; private set; default=0.1;} public double fade_in_zoom {get; private set; default=1.0;} public double fade_out_zoom {get; private set; default=1.0;} public double fade_in_rotation {get; private set; default=0.0;} public double fade_out_rotation{get; private set; default=0.0;} public double springiness {get; private set; default=0.0;} public double center_radius {get; private set; default=90.0;} public double active_radius {get; private set; default=45.0;} public double slice_radius {get; private set; default=32.0;} public double visible_slice_radius {get; private set; default=0.0;} public double slice_gap {get; private set; default=14.0;} public bool has_slice_captions {get; private set; default=false;} public bool caption {get; private set; default=false;} public string caption_font {get; private set; default="sans 12";} public int caption_width {get; private set; default=100;} public int caption_height {get; private set; default=100;} public double caption_position {get; private set; default=0.0;} public Color caption_color {get; private set; default=new Color();} public Icon preview_icon {get; private set; default=new Icon("gnome-pie", 36);} public Gee.ArrayList center_layers {get; private set;} public Gee.ArrayList active_slice_layers {get; private set;} public Gee.ArrayList inactive_slice_layers {get; private set;} ///////////////////////////////////////////////////////////////////// /// C'tor, creates a theme object for a given theme directory. This /// directory should contain a theme.xml file. ///////////////////////////////////////////////////////////////////// public Theme(string dir) { this.center_layers = new Gee.ArrayList(); this.active_slice_layers = new Gee.ArrayList(); this.inactive_slice_layers = new Gee.ArrayList(); this.directory = dir; } ///////////////////////////////////////////////////////////////////// /// Loads the theme from its directory. Images have to be loaded /// explicitly. ///////////////////////////////////////////////////////////////////// public bool load() { this.center_layers.clear(); this.active_slice_layers.clear(); this.inactive_slice_layers.clear(); if (!GLib.File.new_for_path(this.directory).query_exists()) { return false; } string config_file = this.directory + "/theme.xml"; if (!GLib.File.new_for_path(config_file).query_exists()) { try { // detect whether theme is one directory deeper string child; bool success = false; // load global themes var d = Dir.open(this.directory); while ((child = d.read_name()) != null && !success) { config_file = this.directory + "/" + child + "/theme.xml"; if (GLib.File.new_for_path(config_file).query_exists()) { this.directory = this.directory + "/" + child; success = true; } } if (!success) { return false; } } catch (Error e) { warning (e.message); return false; } } this.preview_icon = new Icon(this.directory + "/preview.png", 36); Xml.Parser.init(); Xml.Doc* themeXML = Xml.Parser.parse_file(config_file); if (themeXML == null) { warning("Failed to add theme: \"" + config_file + "\" not found!"); return false; } Xml.Node* root = themeXML->get_root_element(); if (root == null) { delete themeXML; warning("Failed to add theme: \"theme.xml\" is empty!"); return false; } this.parse_root(root); delete themeXML; Xml.Parser.cleanup(); this.radius *= max_zoom; return true; } ///////////////////////////////////////////////////////////////////// /// Exports the theme directory to an importable archive. ///////////////////////////////////////////////////////////////////// public void export(string file) { var archive = new ArchiveWriter(); bool success = true; if (!archive.open(file)) { warning("Cannot open file " + file + " for writing!"); success = false; } else if (!archive.add(this.directory)) { warning("Cannot append directory " + this.directory + " to archive!"); success = false; } archive.close(); if (success) { var message = _("Successfully exported the theme \"%s\"!").printf(this.name); var dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, message); dialog.run(); dialog.destroy(); } else { var message = _("An error occured while exporting the theme \"%s\"! Please check the console output.").printf(this.name); var dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, message); dialog.run(); dialog.destroy(); } } ///////////////////////////////////////////////////////////////////// /// Loads all images of the theme. ///////////////////////////////////////////////////////////////////// public void load_images() { foreach (var layer in this.center_layers) layer.load_image(); foreach (var layer in this.active_slice_layers) layer.load_image(); foreach (var layer in this.inactive_slice_layers) layer.load_image(); } ///////////////////////////////////////////////////////////////////// /// Returns true if the theme is installed to the local themes /// directory. ///////////////////////////////////////////////////////////////////// public bool is_local() { return this.directory.has_prefix(Paths.local_themes); } ///////////////////////////////////////////////////////////////////// /// The following methods parse specific parts of the theme file. /// Nothing special here, just some boring code. ///////////////////////////////////////////////////////////////////// private void parse_root(Xml.Node* root) { for (Xml.Attr* attribute = root->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "name": name = attr_content; break; case "description": description = attr_content; break; case "email": email = attr_content; break; case "author": author = attr_content; break; default: warning("Invalid attribute \"" + attr_name + "\" in element!"); break; } } for (Xml.Node* node = root->children; node != null; node = node->next) { if (node->type == Xml.ElementType.ELEMENT_NODE) { parse_pie(node); } } } ///////////////////////////////////////////////////////////////////// /// Parses a element from the theme.xml file. ///////////////////////////////////////////////////////////////////// private void parse_pie(Xml.Node* pie) { for (Xml.Attr* attribute = pie->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "radius": radius = double.parse(attr_content) * Config.global.global_scale; break; case "maxzoom": max_zoom = double.parse(attr_content); break; case "zoomrange": zoom_range = double.parse(attr_content); break; case "transitiontime": transition_time = double.parse(attr_content); break; case "wobble": wobble = double.parse(attr_content); break; case "fadeintime": fade_in_time = double.parse(attr_content); break; case "fadeouttime": fade_out_time = double.parse(attr_content); break; case "fadeinzoom": fade_in_zoom = double.parse(attr_content); break; case "fadeoutzoom": fade_out_zoom = double.parse(attr_content); break; case "fadeinrotation": fade_in_rotation = double.parse(attr_content); break; case "fadeoutrotation": fade_out_rotation = double.parse(attr_content); break; case "springiness": springiness = double.parse(attr_content); break; default: warning("Invalid attribute \"" + attr_name + "\" in element!"); break; } } for (Xml.Node* node = pie->children; node != null; node = node->next) { if (node->type == Xml.ElementType.ELEMENT_NODE) { string element_name = node->name.down(); switch (element_name) { case "center": parse_center(node); break; case "slices": parse_slices(node); break; case "caption": caption = true; parse_caption(node); break; default: warning("Invalid child element \"" + element_name + "\" in element!"); break; } } } } ///////////////////////////////////////////////////////////////////// /// Parses a
element from the theme.xml file. ///////////////////////////////////////////////////////////////////// private void parse_center(Xml.Node* center) { for (Xml.Attr* attribute = center->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "radius": center_radius = double.parse(attr_content) * Config.global.global_scale; break; case "activeradius": active_radius = double.parse(attr_content) * Config.global.global_scale; break; default: warning("Invalid attribute \"" + attr_name + "\" in
element!"); break; } } for (Xml.Node* node = center->children; node != null; node = node->next) { if (node->type == Xml.ElementType.ELEMENT_NODE) { string element_name = node->name.down(); if (element_name == "center_layer") { parse_center_layer(node); } else { warning("Invalid child element \"" + element_name + "\" in
element!"); } } } } ///////////////////////////////////////////////////////////////////// /// Parses a element from the theme.xml file. ///////////////////////////////////////////////////////////////////// private void parse_slices(Xml.Node* slices) { for (Xml.Attr* attribute = slices->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "radius": slice_radius = double.parse(attr_content) * Config.global.global_scale; visible_slice_radius = double.parse(attr_content) * Config.global.global_scale; break; case "mingap": slice_gap = double.parse(attr_content) * Config.global.global_scale; break; default: warning("Invalid attribute \"" + attr_name + "\" in element!"); break; } } for (Xml.Node* node = slices->children; node != null; node = node->next) { if (node->type == Xml.ElementType.ELEMENT_NODE) { string element_name = node->name.down(); if (element_name == "activeslice" || element_name == "inactiveslice") { parse_slice_layers(node); } else { warning("Invalid child element \"" + element_name + "\" in element!"); } } } } ///////////////////////////////////////////////////////////////////// /// Parses a element from the theme.xml file. ///////////////////////////////////////////////////////////////////// private void parse_center_layer(Xml.Node* layer) { string file = ""; double active_rotation_speed = 0.0; double inactive_rotation_speed = 0.0; double active_scale = 1.0; double inactive_scale = 1.0; double active_alpha = 1.0; double inactive_alpha = 1.0; bool active_colorize = false; bool inactive_colorize = false; CenterLayer.RotationMode active_rotation_mode = CenterLayer.RotationMode.AUTO; CenterLayer.RotationMode inactive_rotation_mode = CenterLayer.RotationMode.AUTO; for (Xml.Attr* attribute = layer->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "file": file = attr_content; break; case "active_scale": active_scale = double.parse(attr_content); break; case "active_alpha": active_alpha = double.parse(attr_content); break; case "active_rotationmode": switch (attr_content.down()) { case "auto": active_rotation_mode = CenterLayer.RotationMode.AUTO; break; case "turn_to_active": active_rotation_mode = CenterLayer.RotationMode.TO_ACTIVE; break; case "turn_to_mouse": active_rotation_mode = CenterLayer.RotationMode.TO_MOUSE; break; case "turn_to_hour": case "turn_to_hour_12": active_rotation_mode = CenterLayer.RotationMode.TO_HOUR_12; break; case "turn_to_hour_24": active_rotation_mode = CenterLayer.RotationMode.TO_HOUR_24; break; case "turn_to_minute": active_rotation_mode = CenterLayer.RotationMode.TO_MINUTE; break; case "turn_to_second": active_rotation_mode = CenterLayer.RotationMode.TO_SECOND; break; default: warning("Invalid value \"" + attr_content + "\" for attribute \"" + attr_name + "\" in element!"); break; } break; case "active_rotationspeed": active_rotation_speed = double.parse(attr_content); break; case "active_colorize": active_colorize = bool.parse(attr_content); break; case "inactive_scale": inactive_scale = double.parse(attr_content); break; case "inactive_alpha": inactive_alpha = double.parse(attr_content); break; case "inactive_rotationmode": switch (attr_content.down()) { case "auto": inactive_rotation_mode = CenterLayer.RotationMode.AUTO; break; case "turn_to_active": inactive_rotation_mode = CenterLayer.RotationMode.TO_ACTIVE; break; case "turn_to_mouse": inactive_rotation_mode = CenterLayer.RotationMode.TO_MOUSE; break; case "turn_to_hour": case "turn_to_hour_12": inactive_rotation_mode = CenterLayer.RotationMode.TO_HOUR_12; break; case "turn_to_hour_24": inactive_rotation_mode = CenterLayer.RotationMode.TO_HOUR_24; break; case "turn_to_minute": inactive_rotation_mode = CenterLayer.RotationMode.TO_MINUTE; break; case "turn_to_second": inactive_rotation_mode = CenterLayer.RotationMode.TO_SECOND; break; default: warning("Invalid value \"" + attr_content + "\" for attribute \"" + attr_name + "\" in element!"); break; } break; case "inactive_rotationspeed": inactive_rotation_speed = double.parse(attr_content); break; case "inactive_colorize": inactive_colorize = bool.parse(attr_content); break; default: warning("Invalid attribute \"" + attr_name + "\" in element!"); break; } } double max_scale = GLib.Math.fmax(active_scale, inactive_scale); center_layers.add(new CenterLayer(directory + "/" + file, (int)(center_radius*max_scale), active_scale/max_scale, active_rotation_speed, active_alpha, active_colorize, active_rotation_mode, inactive_scale/max_scale, inactive_rotation_speed, inactive_alpha, inactive_colorize, inactive_rotation_mode)); } ///////////////////////////////////////////////////////////////////// /// Parses a element from the theme.xml file. ///////////////////////////////////////////////////////////////////// private void parse_slice_layers(Xml.Node* slice) { for (Xml.Node* layer = slice->children; layer != null; layer = layer->next) { if (layer->type == Xml.ElementType.ELEMENT_NODE) { string element_name = layer->name.down(); if (element_name == "slice_layer") { string file = ""; double scale = 1.0; SliceLayer.Type type = SliceLayer.Type.FILE; SliceLayer.Visibility visibility = SliceLayer.Visibility.ANY; bool colorize = false; string slice_caption_font = "sans 8"; int slice_caption_width = 50; int slice_caption_height = 20; int pos_x = 0; int pos_y = 0; Color slice_caption_color = new Color.from_rgb(1.0f, 1.0f, 1.0f); for (Xml.Attr* attribute = layer->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "file": file = attr_content; break; case "scale": scale = double.parse(attr_content); break; case "type": if (attr_content == "icon") type = SliceLayer.Type.ICON; else if (attr_content == "caption") type = SliceLayer.Type.CAPTION; else if (attr_content != "file") warning("Invalid attribute content " + attr_content + " for attribute " + attr_name + " in element!"); break; case "colorize": colorize = bool.parse(attr_content); break; case "font": slice_caption_font = attr_content; break; case "width": slice_caption_width = (int)(int.parse(attr_content) * Config.global.global_scale); if (slice_caption_width % 2 == 1) --slice_caption_width; break; case "height": slice_caption_height = (int)(int.parse(attr_content) * Config.global.global_scale); if (slice_caption_height % 2 == 1) --slice_caption_height; break; case "x": pos_x = (int)(double.parse(attr_content) * Config.global.global_scale); break; case "y": pos_y = (int)(double.parse(attr_content) * Config.global.global_scale); break; case "color": slice_caption_color = new Color.from_string(attr_content); break; case "visibility": if (attr_content == "without_caption") visibility = SliceLayer.Visibility.WITHOUT_CAPTION; else if (attr_content == "with_caption") { this.has_slice_captions = true; visibility = SliceLayer.Visibility.WITH_CAPTION; } else if (attr_content != "any") warning("Invalid attribute content " + attr_content + " for attribute " + attr_name + " in element!"); break; default: warning("Invalid attribute \"" + attr_name + "\" in element!"); break; } } if (file != "") file = directory + "/" + file; int size = 2*(int)(slice_radius*scale*max_zoom); this.visible_slice_radius = Math.fmax(slice_radius*scale, this.visible_slice_radius); if (slice->name.down() == "activeslice") { if (type == SliceLayer.Type.ICON) active_slice_layers.add(new SliceLayer.icon(file, size, pos_x, pos_y, colorize, visibility)); else if (type == SliceLayer.Type.CAPTION) active_slice_layers.add(new SliceLayer.caption(slice_caption_font, slice_caption_width, slice_caption_height, pos_x, pos_y, slice_caption_color, colorize, visibility)); else active_slice_layers.add(new SliceLayer.file(file, size, pos_x, pos_y, colorize, visibility)); } else { if (type == SliceLayer.Type.ICON) inactive_slice_layers.add(new SliceLayer.icon(file, size, pos_x, pos_y, colorize, visibility)); else if (type == SliceLayer.Type.CAPTION) inactive_slice_layers.add(new SliceLayer.caption(slice_caption_font, slice_caption_width, slice_caption_height, pos_x, pos_y, slice_caption_color, colorize, visibility)); else inactive_slice_layers.add(new SliceLayer.file(file, size, pos_x, pos_y, colorize, visibility)); } } else { warning("Invalid child element \"" + element_name + "\" in <" + slice->name + "> element!"); } } } } ///////////////////////////////////////////////////////////////////// /// Parses a element from the theme.xml file. ///////////////////////////////////////////////////////////////////// private void parse_caption(Xml.Node* caption) { for (Xml.Attr* attribute = caption->properties; attribute != null; attribute = attribute->next) { string attr_name = attribute->name.down(); string attr_content = attribute->children->content; switch (attr_name) { case "font": caption_font = attr_content; break; case "width": caption_width = (int)(int.parse(attr_content) * Config.global.global_scale); if (caption_width % 2 == 1) --caption_width; break; case "height": caption_height = (int)(int.parse(attr_content) * Config.global.global_scale); if (caption_height % 2 == 1) --caption_height; break; case "position": caption_position = double.parse(attr_content) * Config.global.global_scale; break; case "color": caption_color = new Color.from_string(attr_content); break; default: warning("Invalid attribute \"" + attr_name + "\" in element!"); break; } } } } }