diff options
Diffstat (limited to 'src')
64 files changed, 14035 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..4d1194f --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,100 @@ +################################################################ +# Actually compile the executable +################################################################ + +# determine source and header files +file(GLOB VALA_SRC RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.vala */*.vala) + +if (${INDICATOR_FOUND}) + LIST(APPEND DEFINES --define HAVE_APPINDICATOR) +endif(${INDICATOR_FOUND}) +if (${INDICATOR3_FOUND}) + LIST(APPEND DEFINES --define HAVE_APPINDICATOR) +endif(${INDICATOR3_FOUND}) + +if (${GMENU3_FOUND}) + LIST(APPEND DEFINES --define HAVE_GMENU_3) +endif (${GMENU3_FOUND}) + +# use valac to compile sources to c files +vala_precompile( + VALA_C + ${VALA_SRC} + PACKAGES + ${VALA_PKGS} + OPTIONS + --thread + ${DEFINES} +) + +# compile c-sources +add_executable(gnome-pie ${VALA_C}) + +# install executable +install( + TARGETS + gnome-pie + RUNTIME DESTINATION + ${CMAKE_INSTALL_PREFIX}/bin +) + +# install credits +install( + FILES + ${CMAKE_SOURCE_DIR}/README.md + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/doc/gnome-pie +) + +# install locales +install( + DIRECTORY + ${CMAKE_SOURCE_DIR}/resources/locale + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share + PATTERN *.po EXCLUDE + PATTERN *.pot EXCLUDE + PATTERN *.sh EXCLUDE +) + +# install themes +install( + DIRECTORY + ${CMAKE_SOURCE_DIR}/resources/themes + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/gnome-pie +) + +# install UI files +install( + DIRECTORY + ${CMAKE_SOURCE_DIR}/resources/ui + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/gnome-pie +) + +# install icons +install( + FILES + ${CMAKE_SOURCE_DIR}/resources/gnome-pie.svg + ${CMAKE_SOURCE_DIR}/resources/gnome-pie-symbolic.svg + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/apps +) + +# desktop file +install( + FILES + ${CMAKE_SOURCE_DIR}/resources/gnome-pie.desktop + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/applications +) + +# install manpage +install( + FILES + ${CMAKE_SOURCE_DIR}/resources/gnome-pie.1 + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/man/man1 +) + diff --git a/src/actionGroups/actionGroup.vala b/src/actionGroups/actionGroup.vala new file mode 100644 index 0000000..85488ad --- /dev/null +++ b/src/actionGroups/actionGroup.vala @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +// A base class storing a set of Actions. Derived classes may define +// how these Actions are created. This base class serves for custom +// actions, defined by the user. +///////////////////////////////////////////////////////////////////////// + +public class ActionGroup : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// A list of all stored actions. + ///////////////////////////////////////////////////////////////////// + + public Gee.ArrayList<Action?> actions { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// The ID of the pie to which this group is attached. + ///////////////////////////////////////////////////////////////////// + + public string parent_id { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public ActionGroup(string parent_id) { + GLib.Object(parent_id : parent_id); + } + + construct { + this.actions = new Gee.ArrayList<Action?>(); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is deleted. + ///////////////////////////////////////////////////////////////////// + + public virtual void on_remove() {} + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is saved. + ///////////////////////////////////////////////////////////////////// + + public virtual void on_save(Xml.TextWriter writer) { + writer.write_attribute("type", GroupRegistry.descriptions[this.get_type().name()].id); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is loaded. + ///////////////////////////////////////////////////////////////////// + + public virtual void on_load(Xml.Node* data) {} + + ///////////////////////////////////////////////////////////////////// + /// Adds a new Action to the group. + ///////////////////////////////////////////////////////////////////// + + public void add_action(Action new_action) { + this.actions.add(new_action); + } + + ///////////////////////////////////////////////////////////////////// + /// Removes all Actions from the group. + ///////////////////////////////////////////////////////////////////// + + public void delete_all() { + actions.clear(); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes all contained Slices no Quick Actions. + ///////////////////////////////////////////////////////////////////// + + public void disable_quickactions() { + foreach (var action in actions) { + action.is_quickaction = false; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true, if one o the contained Slices is a Quick Action + ///////////////////////////////////////////////////////////////////// + + public bool has_quickaction() { + foreach (var action in actions) { + if (action.is_quickaction) { + return true; + } + } + + return false; + } +} + +} diff --git a/src/actionGroups/bookmarkGroup.vala b/src/actionGroups/bookmarkGroup.vala new file mode 100644 index 0000000..791d609 --- /dev/null +++ b/src/actionGroups/bookmarkGroup.vala @@ -0,0 +1,151 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A group of Actions, which represent the users gtk-bookmarks, his home +/// directory, desktop and trash. It stay up-to-date, even if the +/// bookmarks change. +///////////////////////////////////////////////////////////////////////// + +public class BookmarkGroup : ActionGroup { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of ActionGroup. It sets the display + /// name for this ActionGroup, it's icon name and the string used in + /// the pies.conf file for this kind of ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public static GroupRegistry.TypeDescription register() { + var description = new GroupRegistry.TypeDescription(); + description.name = _("Group: Bookmarks"); + description.icon = "user-bookmarks"; + description.description = _("Shows a Slice for each of your directory Bookmarks."); + description.id = "bookmarks"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// Two members needed to avoid useless, frequent changes of the + /// stored Actions. + ///////////////////////////////////////////////////////////////////// + + private bool changing = false; + private bool changed_again = false; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public BookmarkGroup(string parent_id) { + GLib.Object(parent_id : parent_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Construct block loads the bookmarks of the user and adds a file + /// monitor in order to update the BookmarkGroup when the bookmarks + /// of the user change. + ///////////////////////////////////////////////////////////////////// + + construct { + this.load(); + + // add monitor + var bookmark_file = GLib.File.new_for_path( + GLib.Environment.get_home_dir()).get_child(".gtk-bookmarks"); + + if (bookmark_file.query_exists()) { + try { + var monitor = bookmark_file.monitor(GLib.FileMonitorFlags.NONE); + monitor.changed.connect(this.reload); + } catch (GLib.Error e) { + warning(e.message); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Adds Actions for each gtk-bookmark of the user and for his home + /// folder, desktop and trash. + ///////////////////////////////////////////////////////////////////// + + private void load() { + // add home folder + this.add_action(ActionRegistry.new_for_uri("file://" + GLib.Environment.get_home_dir())); + + // add .gtk-bookmarks + var bookmark_file = GLib.File.new_for_path( + GLib.Environment.get_home_dir()).get_child(".gtk-bookmarks"); + + if (!bookmark_file.query_exists()) { + warning("Failed to find file \".gtk-bookmarks\"!"); + return; + } + + try { + var dis = new DataInputStream(bookmark_file.read()); + string line; + while ((line = dis.read_line(null)) != null) { + var parts = line.split(" "); + + string uri = parts[0]; + string name = parts[1]; + + this.add_action(ActionRegistry.new_for_uri(uri, name)); + } + } catch (Error e) { + error ("%s", e.message); + } + + // add trash + this.add_action(ActionRegistry.new_for_uri("trash://")); + + // add desktop + this.add_action(ActionRegistry.new_for_uri("file://" + GLib.Environment.get_user_special_dir(GLib.UserDirectory.DESKTOP))); + } + + ///////////////////////////////////////////////////////////////////// + /// Reloads all Bookmarks. Is called when the user's gtk-bookmarks + /// file changes. + ///////////////////////////////////////////////////////////////////// + + private void reload() { + // avoid too frequent changes... + if (!this.changing) { + this.changing = true; + Timeout.add(200, () => { + if (this.changed_again) { + this.changed_again = false; + return true; + } + + // reload + message("Bookmarks changed..."); + this.delete_all(); + this.load(); + + this.changing = false; + return false; + }); + } else { + this.changed_again = true; + } + } +} + +} diff --git a/src/actionGroups/clipboardGroup.vala b/src/actionGroups/clipboardGroup.vala new file mode 100644 index 0000000..58409de --- /dev/null +++ b/src/actionGroups/clipboardGroup.vala @@ -0,0 +1,194 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This Group keeps a history of the last used Clipboard entries. +/// Experimental. Not enabled. +///////////////////////////////////////////////////////////////////////// + +public class ClipboardGroup : ActionGroup { + + ///////////////////////////////////////////////////////////////////// + + private class ClipboardItem : GLib.Object { + + public string name { get; protected set; } + public string icon { get; protected set; } + + protected Gtk.Clipboard clipboard { get; set; } + protected static Key paste_key = new Key.from_string("<Control>v"); + + public virtual void paste() {} + } + + ///////////////////////////////////////////////////////////////////// + + private class TextClipboardItem : ClipboardItem { + + public TextClipboardItem(Gtk.Clipboard clipboard) { + GLib.Object(clipboard : clipboard, + name : clipboard.wait_for_text(), + icon : "edit-paste"); + + // check whether a file has been copied and search for a cool icon + var first_line = this.name.substring(0, this.name.index_of("\n")); + var file = GLib.File.new_for_path(first_line); + + if (file.query_exists()) { + try { + var info = file.query_info("standard::icon", 0); + this.icon = Icon.get_icon_name(info.get_icon()); + } catch (Error e) { + warning("Failed to generate icon for ClipboardGroupItem."); + } + } + } + + public override void paste() { + clipboard.set_text(name, name.length); + paste_key.press(); + } + } + + ///////////////////////////////////////////////////////////////////// + + private class ImageClipboardItem : ClipboardItem { + + private Gdk.Pixbuf image { get; set; } + + public ImageClipboardItem(Gtk.Clipboard clipboard) { + GLib.Object(clipboard : clipboard, + name : _("Image data"), + icon : "image-viewer"); + this.image = clipboard.wait_for_image(); + } + + public override void paste() { + clipboard.set_image(image); + paste_key.press(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// The maximum remembered items of the clipboard. + ///////////////////////////////////////////////////////////////////// + + public int max_items {get; set; default=8; } + + ///////////////////////////////////////////////////////////////////// + + public ClipboardGroup(string parent_id) { + GLib.Object(parent_id : parent_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of ActionGroup. It sets the display + /// name for this ActionGroup, it's icon name and the string used in + /// the pies.conf file for this kind of ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public static GroupRegistry.TypeDescription register() { + var description = new GroupRegistry.TypeDescription(); + description.name = _("Group: Clipboard"); + description.icon = "edit-paste"; + description.description = _("Manages your Clipboard."); + description.id = "clipboard"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// The clipboard to be monitored. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Clipboard clipboard; + + private bool ignore_next_change = false; + + private Gee.ArrayList<ClipboardItem?> items; + + construct { + this.items = new Gee.ArrayList<ClipboardItem?>(); + this.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD); + this.clipboard.owner_change.connect(this.on_change); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is saved. + ///////////////////////////////////////////////////////////////////// + + public override void on_save(Xml.TextWriter writer) { + base.on_save(writer); + writer.write_attribute("max_items", this.max_items.to_string()); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is loaded. + ///////////////////////////////////////////////////////////////////// + + public override void on_load(Xml.Node* data) { + for (Xml.Attr* attribute = data->properties; attribute != null; attribute = attribute->next) { + string attr_name = attribute->name.down(); + string attr_content = attribute->children->content; + + if (attr_name == "max_items") { + this.max_items = int.parse(attr_content); + } + } + } + + private void on_change() { + if (ignore_next_change) { + ignore_next_change = false; + return; + } + + if (this.clipboard.wait_is_text_available()) { + if (clipboard.wait_for_text() != null) { + add_item(new TextClipboardItem(this.clipboard)); + } + } else if (this.clipboard.wait_is_image_available()) { + add_item(new ImageClipboardItem(this.clipboard)); + } + } + + private void add_item(ClipboardItem item) { + + // remove one item if there are too many + if (this.items.size == this.max_items) { + this.items.remove_at(0); + } + + this.items.add(item); + + // update slices + this.delete_all(); + + for (int i=this.items.size-1; i>=0; --i) { + var action = new SigAction(items[i].name, items[i].icon, i.to_string()); + action.activated.connect(() => { + ignore_next_change = true; + this.items[int.parse(action.real_command)].paste(); + }); + this.add_action(action); + } + + } +} + +} diff --git a/src/actionGroups/devicesGroup.vala b/src/actionGroups/devicesGroup.vala new file mode 100644 index 0000000..e18f4c0 --- /dev/null +++ b/src/actionGroups/devicesGroup.vala @@ -0,0 +1,124 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////// +/// An ActionGroup which contains all currently plugged-in devices, +/// such as CD-ROM's or USB-sticks. +///////////////////////////////////////////////////////////////////// + +public class DevicesGroup : ActionGroup { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of ActionGroup. It sets the display + /// name for this ActionGroup, it's icon name and the string used in + /// the pies.conf file for this kind of ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public static GroupRegistry.TypeDescription register() { + var description = new GroupRegistry.TypeDescription(); + description.name = _("Group: Devices"); + description.icon = "drive-harddisk"; + description.description = _("Shows a Slice for each plugged in devices, like USB-Sticks."); + description.id = "devices"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// Two members needed to avoid useless, frequent changes of the + /// stored Actions. + ///////////////////////////////////////////////////////////////////// + + private bool changing = false; + private bool changed_again = false; + + ///////////////////////////////////////////////////////////////////// + /// The VolumeMonitor used to check for added or removed devices. + ///////////////////////////////////////////////////////////////////// + + private GLib.VolumeMonitor monitor; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public DevicesGroup(string parent_id) { + GLib.Object(parent_id : parent_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Construct block loads all currently plugged-in devices and + /// connects signal handlers to the VolumeMonitor. + ///////////////////////////////////////////////////////////////////// + + construct { + this.monitor = GLib.VolumeMonitor.get(); + + this.load(); + + // add monitor + this.monitor.mount_added.connect(this.reload); + this.monitor.mount_removed.connect(this.reload); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all currently plugged-in devices. + ///////////////////////////////////////////////////////////////////// + + private void load() { + // add root device + this.add_action(new UriAction(_("Root"), "drive-harddisk", "file:///")); + + // add all other devices + foreach(var mount in this.monitor.get_mounts()) { + // get icon + var icon = mount.get_icon(); + + this.add_action(new UriAction(mount.get_name(), Icon.get_icon_name(icon), mount.get_root().get_uri())); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Reloads all devices. Is called when the VolumeMonitor changes. + ///////////////////////////////////////////////////////////////////// + + private void reload() { + // avoid too frequent changes... + if (!this.changing) { + this.changing = true; + Timeout.add(200, () => { + if (this.changed_again) { + this.changed_again = false; + return true; + } + + // reload + message("Devices changed..."); + this.delete_all(); + this.load(); + + this.changing = false; + return false; + }); + } else { + this.changed_again = true; + } + } +} + +} diff --git a/src/actionGroups/groupRegistry.vala b/src/actionGroups/groupRegistry.vala new file mode 100644 index 0000000..c97cf95 --- /dev/null +++ b/src/actionGroups/groupRegistry.vala @@ -0,0 +1,110 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A which has knowledge on all possible acion group types. +///////////////////////////////////////////////////////////////////////// + +public class GroupRegistry : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// A list containing all available ActionGroup types. + ///////////////////////////////////////////////////////////////////// + + public static Gee.ArrayList<string> types { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// A map associating a displayable name for each ActionGroup, + /// an icon name and a name for the pies.conf file with it's type. + ///////////////////////////////////////////////////////////////////// + + public static Gee.HashMap<string, TypeDescription?> descriptions { get; private set; } + + public class TypeDescription { + public string name { get; set; default=""; } + public string icon { get; set; default=""; } + public string description { get; set; default=""; } + public string id { get; set; default=""; } + } + + ///////////////////////////////////////////////////////////////////// + /// Registers all ActionGroup types. + ///////////////////////////////////////////////////////////////////// + + public static void init() { + types = new Gee.ArrayList<string>(); + descriptions = new Gee.HashMap<string, TypeDescription?>(); + + TypeDescription type_description; + + type_description = BookmarkGroup.register(); + types.add(typeof(BookmarkGroup).name()); + descriptions.set(typeof(BookmarkGroup).name(), type_description); + + type_description = ClipboardGroup.register(); + types.add(typeof(ClipboardGroup).name()); + descriptions.set(typeof(ClipboardGroup).name(), type_description); + + type_description = DevicesGroup.register(); + types.add(typeof(DevicesGroup).name()); + descriptions.set(typeof(DevicesGroup).name(), type_description); + + type_description = MenuGroup.register(); + types.add(typeof(MenuGroup).name()); + descriptions.set(typeof(MenuGroup).name(), type_description); + + type_description = SessionGroup.register(); + types.add(typeof(SessionGroup).name()); + descriptions.set(typeof(SessionGroup).name(), type_description); + + type_description = WindowListGroup.register(); + types.add(typeof(WindowListGroup).name()); + descriptions.set(typeof(WindowListGroup).name(), type_description); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a Group for a given type name. + ///////////////////////////////////////////////////////////////////// + + public static ActionGroup? create_group(string type_id, string parent_id) { + switch (type_id) { + case "bookmarks": + return new BookmarkGroup(parent_id); + case "clipboard": + return new ClipboardGroup(parent_id); + case "devices": + return new DevicesGroup(parent_id); + case "menu": + return new MenuGroup(parent_id); + case "session": + return new SessionGroup(parent_id); + case "window_list": + return new WindowListGroup(parent_id); + // deprecated + case "workspace_window_list": + var group = new WindowListGroup(parent_id); + group.current_workspace_only = true; + return group; + } + + return null; + } +} + +} diff --git a/src/actionGroups/menuGroup.vala b/src/actionGroups/menuGroup.vala new file mode 100644 index 0000000..ccb5407 --- /dev/null +++ b/src/actionGroups/menuGroup.vala @@ -0,0 +1,250 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// An ActionGroup which displays the user's main menu. It's a bit ugly, +/// but it supports both, an older version and libgnome-menus-3 at the +/// same time. +///////////////////////////////////////////////////////////////////////// + +public class MenuGroup : ActionGroup { + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of ActionGroup. It sets the display + /// name for this ActionGroup, it's icon name and the string used in + /// the pies.conf file for this kind of ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public static GroupRegistry.TypeDescription register() { + var description = new GroupRegistry.TypeDescription(); + description.name = _("Group: Main menu"); + description.icon = "start-here"; + description.description = _("Displays your main menu structure."); + description.id = "menu"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// True, if this MenuGroup is the top most menu. + ///////////////////////////////////////////////////////////////////// + + public bool is_toplevel {get; construct set; default = true;} + + ///////////////////////////////////////////////////////////////////// + /// The menu tree displayed by the MenuGroup. Only set for the + /// toplevel MenuGroup. + ///////////////////////////////////////////////////////////////////// + + private GMenu.Tree menu = null; + + ///////////////////////////////////////////////////////////////////// + /// A list of all sub menus of this MenuGroup. + ///////////////////////////////////////////////////////////////////// + + private Gee.ArrayList<MenuGroup?> childs; + + ///////////////////////////////////////////////////////////////////// + /// Two members needed to avoid useless, frequent changes of the + /// stored Actions. + ///////////////////////////////////////////////////////////////////// + + private bool changing = false; + private bool changed_again = false; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. Used for the toplevel menu. + ///////////////////////////////////////////////////////////////////// + + public MenuGroup(string parent_id) { + GLib.Object(parent_id : parent_id, is_toplevel : true); + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. Used for sub menus. + ///////////////////////////////////////////////////////////////////// + + public MenuGroup.sub_menu(string parent_id) { + GLib.Object(parent_id : parent_id, is_toplevel : false); + } + + construct { + this.childs = new Gee.ArrayList<MenuGroup?>(); + + if (this.is_toplevel) { + #if HAVE_GMENU_3 + this.menu = new GMenu.Tree("applications.menu", GMenu.TreeFlags.INCLUDE_EXCLUDED); + this.menu.changed.connect(this.reload); + #endif + + this.load_toplevel(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Starts to load the menu. + ///////////////////////////////////////////////////////////////////// + + private void load_toplevel() { + #if HAVE_GMENU_3 + try { + if (this.menu.load_sync()) { + this.load_contents(this.menu.get_root_directory(), this.parent_id); + } + } catch (GLib.Error e) { + warning(e.message); + } + #else + this.menu = GMenu.Tree.lookup ("applications.menu", GMenu.TreeFlags.INCLUDE_EXCLUDED); + this.menu.add_monitor(this.reload); + var dir = this.menu.get_root_directory(); + this.load_contents(dir, this.parent_id); + #endif + } + + ///////////////////////////////////////////////////////////////////// + /// Parses the main menu recursively. + ///////////////////////////////////////////////////////////////////// + + private void load_contents(GMenu.TreeDirectory dir, string parent_id) { + #if HAVE_GMENU_3 + var item = dir.iter(); + + while (true) { + var type = item.next(); + if (type == GMenu.TreeItemType.INVALID) + break; + + if (type == GMenu.TreeItemType.DIRECTORY && !item.get_directory().get_is_nodisplay()) { + // create a MenuGroup for sub menus + + // get icon + var icon = item.get_directory().get_icon(); + + var sub_menu = PieManager.create_dynamic_pie(item.get_directory().get_name(), Icon.get_icon_name(icon)); + var group = new MenuGroup.sub_menu(sub_menu.id); + group.add_action(new PieAction(parent_id, true)); + group.load_contents(item.get_directory(), sub_menu.id); + childs.add(group); + + sub_menu.add_group(group); + + this.add_action(new PieAction(sub_menu.id)); + + } else if (type == GMenu.TreeItemType.ENTRY ) { + // create an AppAction for entries + if (!item.get_entry().get_is_excluded()) { + this.add_action(ActionRegistry.new_for_app_info(item.get_entry().get_app_info())); + } + } + } + #else + foreach (var item in dir.get_contents()) { + switch(item.get_type()) { + case GMenu.TreeItemType.DIRECTORY: + // create a MenuGroup for sub menus + if (!((GMenu.TreeDirectory)item).get_is_nodisplay()) { + var sub_menu = PieManager.create_dynamic_pie( + ((GMenu.TreeDirectory)item).get_name(), + ((GMenu.TreeDirectory)item).get_icon()); + var group = new MenuGroup.sub_menu(sub_menu.id); + group.add_action(new PieAction(parent_id, true)); + group.load_contents((GMenu.TreeDirectory)item, sub_menu.id); + childs.add(group); + + sub_menu.add_group(group); + + this.add_action(new PieAction(sub_menu.id)); + } + break; + + case GMenu.TreeItemType.ENTRY: + // create an AppAction for entries + if (!((GMenu.TreeEntry)item).get_is_nodisplay() && !((GMenu.TreeEntry)item).get_is_excluded()) { + this.add_action(new AppAction(((GMenu.TreeEntry)item).get_name(), + ((GMenu.TreeEntry)item).get_icon(), + ((GMenu.TreeEntry)item).get_exec())); + } + break; + } + } + #endif + } + + ///////////////////////////////////////////////////////////////////// + /// Reloads the menu. + ///////////////////////////////////////////////////////////////////// + + private void reload() { + // avoid too frequent changes... + if (!this.changing) { + this.changing = true; + Timeout.add(500, () => { + if (this.changed_again) { + this.changed_again = false; + return true; + } + + // reload + message("Main menu changed..."); + #if !HAVE_GMENU_3 + this.menu.remove_monitor(this.reload); + #endif + + this.clear(); + this.load_toplevel(); + + this.changing = false; + return false; + }); + } else { + this.changed_again = true; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Deletes all generated Pies, when the toplevel menu is deleted. + ///////////////////////////////////////////////////////////////////// + + public override void on_remove() { + if (this.is_toplevel) + this.clear(); + } + + ///////////////////////////////////////////////////////////////////// + /// Clears this ActionGroup recursively. + ///////////////////////////////////////////////////////////////////// + + private void clear() { + foreach (var child in childs) + child.clear(); + + if (!this.is_toplevel) + PieManager.remove_pie(this.parent_id); + + this.delete_all(); + + this.childs.clear(); + + #if !HAVE_GMENU_3 + this.menu = null; + #endif + + } +} + +} diff --git a/src/actionGroups/sessionGroup.vala b/src/actionGroups/sessionGroup.vala new file mode 100644 index 0000000..42afafc --- /dev/null +++ b/src/actionGroups/sessionGroup.vala @@ -0,0 +1,76 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////// +/// An ActionGroup which has three Actions: Logout, Shutdown and +/// Reboot. +///////////////////////////////////////////////////////////////////// + +public class SessionGroup : ActionGroup { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of ActionGroup. It sets the display + /// name for this ActionGroup, it's icon name and the string used in + /// the pies.conf file for this kind of ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public static GroupRegistry.TypeDescription register() { + var description = new GroupRegistry.TypeDescription(); + description.name = _("Group: Session Control"); + description.icon = "system-log-out"; + description.description = _("Shows a Slice for Shutdown, Reboot, and Hibernate."); + description.id = "session"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public SessionGroup(string parent_id) { + GLib.Object(parent_id : parent_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Construct block adds the three Actions. + ///////////////////////////////////////////////////////////////////// + + construct { +// string iface = GLib.Bus.get_proxy_sync(GLib.BusType.SESSION, "org.gnome.SessionManager", "/org/gnome/SessionManager"); +// iface = GLib.Bus.get_proxy_sync(GLib.BusType.SESSION, "org.freedesktop.Hal", "/org/freedesktop/Hal/devices/computer"); +// iface = GLib.Bus.get_proxy_sync(GLib.BusType.SESSION, "org.kde.ksmserver", "/KSMServer"); +// iface = GLib.Bus.get_proxy_sync(GLib.BusType.SESSION, "org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager"); + + this.add_action(new AppAction(_("Shutdown"), "system-shutdown", + "dbus-send --print-reply --dest=org.gnome.SessionManager /org/gnome/SessionManager org.gnome.SessionManager.Shutdown")); + + this.add_action(new AppAction(_("Logout"), "system-log-out", + "dbus-send --print-reply --dest=org.gnome.SessionManager /org/gnome/SessionManager org.gnome.SessionManager.Logout uint32:1")); + + this.add_action(new AppAction(_("Reboot"), "view-refresh", + "dbus-send --print-reply --dest=org.gnome.SessionManager /org/gnome/SessionManager org.gnome.SessionManager.Reboot")); + } + + // TODO: check for available interfaces --- these may work too: + // dbus-send --print-reply --dest=org.freedesktop.Hal /org/freedesktop/Hal/devices/computer org.freedesktop.Hal.Device.SystemPowerManagement.Shutdown + // dbus-send --print-reply --dest=org.kde.ksmserver /KSMServer org.kde.KSMServerInterface.logout 0 2 2 + // dbus-send --print-reply --dest="org.freedesktop.ConsoleKit" /org/freedesktop/ConsoleKit/Manager org.freedesktop.ConsoleKit.Manager.Stop +} + +} diff --git a/src/actionGroups/windowListGroup.vala b/src/actionGroups/windowListGroup.vala new file mode 100644 index 0000000..69029a7 --- /dev/null +++ b/src/actionGroups/windowListGroup.vala @@ -0,0 +1,193 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////// +/// This group displays a list of all running application windows. +///////////////////////////////////////////////////////////////////// + +public class WindowListGroup : ActionGroup { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of ActionGroup. It sets the display + /// name for this ActionGroup, it's icon name and the string used in + /// the pies.conf file for this kind of ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public static GroupRegistry.TypeDescription register() { + var description = new GroupRegistry.TypeDescription(); + description.name = _("Group: Window List"); + description.icon = "preferences-system-windows"; + description.description = _("Shows a Slice for each of your opened Windows. Almost like Alt-Tab."); + description.id = "window_list"; + return description; + } + + public bool current_workspace_only { get; set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// Two members needed to avoid useless, frequent changes of the + /// stored Actions. + ///////////////////////////////////////////////////////////////////// + + private bool changing = false; + private bool changed_again = false; + + private Wnck.Screen screen; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public WindowListGroup(string parent_id) { + GLib.Object(parent_id : parent_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all windows. + ///////////////////////////////////////////////////////////////////// + + construct { + this.screen = Wnck.Screen.get_default(); + + this.screen.window_opened.connect(reload); + this.screen.window_closed.connect(reload); + this.screen.active_workspace_changed.connect(reload); + + this.update(); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is saved. + ///////////////////////////////////////////////////////////////////// + + public override void on_save(Xml.TextWriter writer) { + base.on_save(writer); + writer.write_attribute("current_workspace_only", this.current_workspace_only.to_string()); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the ActionGroup is loaded. + ///////////////////////////////////////////////////////////////////// + + public override void on_load(Xml.Node* data) { + for (Xml.Attr* attribute = data->properties; attribute != null; attribute = attribute->next) { + string attr_name = attribute->name.down(); + string attr_content = attribute->children->content; + + if (attr_name == "current_workspace_only") { + this.current_workspace_only = bool.parse(attr_content); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all currently opened windows and creates actions for them. + ///////////////////////////////////////////////////////////////////// + + private void update() { + unowned GLib.List<Wnck.Window?> windows = this.screen.get_windows(); + + var matcher = Bamf.Matcher.get_default(); + + foreach (var window in windows) { + if (window.get_window_type() == Wnck.WindowType.NORMAL + && !window.is_skip_pager() && !window.is_skip_tasklist() + && (!current_workspace_only || (window.get_workspace() != null + && window.get_workspace() == this.screen.get_active_workspace()))) { + + var application = window.get_application(); + var bamf_app = matcher.get_application_for_xid((uint32)window.get_xid()); + + string name = window.get_name(); + + if (name.length > 30) + name = name.substring(0, 30) + "..."; + + var action = new SigAction( + name, + (bamf_app == null) ? application.get_icon_name().down() : bamf_app.get_icon(), + "%lu".printf(window.get_xid()) + ); + action.activated.connect((time_stamp) => { + Wnck.Screen.get_default().force_update(); + + var xid = (X.Window)uint64.parse(action.real_command); + var win = Wnck.Window.get(xid); + + if (win.get_workspace() != null) { + //select the workspace + if (win.get_workspace() != win.get_screen().get_active_workspace()) { + win.get_workspace().activate(time_stamp); + } + + //select the viewport inside the workspace + if (!win.is_in_viewport(win.get_workspace()) ) { + int xp, yp, widthp, heightp, scx, scy, nx, ny, wx, wy; + win.get_geometry (out xp, out yp, out widthp, out heightp); + scx = win.get_screen().get_width(); + scy = win.get_screen().get_height(); + wx = win.get_workspace().get_viewport_x(); + wy = win.get_workspace().get_viewport_y(); + if (scx > 0 && scy > 0) { + nx= ((wx+xp) / scx) * scx; + ny= ((wy+yp) / scy) * scy; + win.get_screen().move_viewport(nx, ny); + } + } + } + + if (win.is_minimized()) { + win.unminimize(time_stamp); + } + + win.activate_transient(time_stamp); + }); + this.add_action(action); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Reloads all running applications. + ///////////////////////////////////////////////////////////////////// + + private void reload() { + // avoid too frequent changes... + if (!this.changing) { + this.changing = true; + Timeout.add(500, () => { + if (this.changed_again) { + this.changed_again = false; + return true; + } + + // reload + this.delete_all(); + this.update(); + + this.changing = false; + return false; + }); + } else { + this.changed_again = true; + } + } +} + +} diff --git a/src/actions/action.vala b/src/actions/action.vala new file mode 100644 index 0000000..1e6437e --- /dev/null +++ b/src/actions/action.vala @@ -0,0 +1,77 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A base class for actions, which are executed when the user +/// activates a pie's slice. +///////////////////////////////////////////////////////////////////////// + +public abstract class Action : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The command which gets executed when user activates the Slice. + /// It may be anything but has to be representable with a string. + ///////////////////////////////////////////////////////////////////// + + public abstract string real_command { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// The command displayed to the user. It should be a bit more + /// beautiful than the real_command. + ///////////////////////////////////////////////////////////////////// + + public abstract string display_command { get; } + + ///////////////////////////////////////////////////////////////////// + /// The name of the Action. + ///////////////////////////////////////////////////////////////////// + + public virtual string name { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// The name of the icon of this Action. It should be in the users + /// current icon theme. + ///////////////////////////////////////////////////////////////////// + + public virtual string icon { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// True, if this Action is the quickAction of the associated Pie. + /// The quickAction of a Pie gets executed when the users clicks on + /// the center of a Pie. + ///////////////////////////////////////////////////////////////////// + + public virtual bool is_quickaction { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public Action(string name, string icon, bool is_quickaction) { + GLib.Object(name : name, icon : icon, is_quickaction : is_quickaction); + } + + ///////////////////////////////////////////////////////////////////// + /// This one is called, when the user activates the Slice. + ///////////////////////////////////////////////////////////////////// + + public abstract void activate(uint32 time_stamp); +} + +} diff --git a/src/actions/actionRegistry.vala b/src/actions/actionRegistry.vala new file mode 100644 index 0000000..9a22cc7 --- /dev/null +++ b/src/actions/actionRegistry.vala @@ -0,0 +1,219 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A which has knowledge on all possible acion types. +///////////////////////////////////////////////////////////////////////// + +public class ActionRegistry : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// A list containing all available Action types. + ///////////////////////////////////////////////////////////////////// + + public static Gee.ArrayList<string> types { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// A map associating a displayable name for each Action, + /// whether it has a custom icon and a name for the pies.conf + /// file with it's type. + ///////////////////////////////////////////////////////////////////// + + public static Gee.HashMap<string, TypeDescription?> descriptions { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// A helper class storing information on a Action type. + ///////////////////////////////////////////////////////////////////// + + public class TypeDescription { + public string name { get; set; default=""; } + public string icon { get; set; default=""; } + public string description { get; set; default=""; } + public string id { get; set; default=""; } + public bool icon_name_editable { get; set; default=false; } + } + + ///////////////////////////////////////////////////////////////////// + /// Registers all Action types. + ///////////////////////////////////////////////////////////////////// + + public static void init() { + types = new Gee.ArrayList<string>(); + descriptions = new Gee.HashMap<string, TypeDescription?>(); + + TypeDescription type_description; + + types.add(typeof(AppAction).name()); + type_description = AppAction.register(); + descriptions.set(typeof(AppAction).name(), type_description); + + types.add(typeof(KeyAction).name()); + type_description = KeyAction.register(); + descriptions.set(typeof(KeyAction).name(), type_description); + + types.add(typeof(PieAction).name()); + type_description = PieAction.register(); + descriptions.set(typeof(PieAction).name(), type_description); + + types.add(typeof(UriAction).name()); + type_description = UriAction.register(); + descriptions.set(typeof(UriAction).name(), type_description); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a new Action from the given type name. + ///////////////////////////////////////////////////////////////////// + + public static Action? create_action(string type_id, string name, string icon, string command, bool quickaction) { + switch (type_id) { + case "app": return new AppAction(name, icon, command, quickaction); + case "key": return new KeyAction(name, icon, command, quickaction); + case "uri": return new UriAction(name, icon, command, quickaction); + case "pie": return new PieAction(command, quickaction); + } + + return null; + } + + ///////////////////////////////////////////////////////////////////// + /// A helper method which creates an Action, appropriate for the + /// given URI. This can result in an UriAction or in an AppAction, + /// depending on the Type of the URI. + ///////////////////////////////////////////////////////////////////// + + public static Action? new_for_uri(string uri, string? name = null) { + var file = GLib.File.new_for_uri(uri); + var scheme = file.get_uri_scheme(); + + string final_icon = ""; + string final_name = file.get_basename(); + + switch (scheme) { + case "application": + var file_name = uri.split("//")[1]; + + var desktop_file = GLib.File.new_for_path("/usr/share/applications/" + file_name); + if (desktop_file.query_exists()) + return new_for_desktop_file(desktop_file.get_path()); + + break; + + case "trash": + final_icon = "user-trash"; + final_name = _("Trash"); + break; + + case "http": case "https": + final_icon = "www"; + final_name = get_domain_name(uri); + break; + + case "ftp": case "sftp": + final_icon = "folder-remote"; + final_name = get_domain_name(uri); + break; + + default: + try { + var info = file.query_info("*", GLib.FileQueryInfoFlags.NONE); + + if (info.get_content_type() == "application/x-desktop") + return new_for_desktop_file(file.get_parse_name()); + + // search for an appropriate icon + var icon = info.get_icon(); + final_icon = Icon.get_icon_name(icon); + + } catch (GLib.Error e) { + warning(e.message); + } + + break; + } + + if (!Gtk.IconTheme.get_default().has_icon(final_icon)) + final_icon = "stock_unknown"; + + if (name != null) + final_name = name; + + return new UriAction(final_name, final_icon, uri); + } + + ///////////////////////////////////////////////////////////////////// + /// A helper method which creates an AppAction for given AppInfo. + ///////////////////////////////////////////////////////////////////// + + public static Action? new_for_app_info(GLib.AppInfo info) { + // get icon + var icon = info.get_icon(); + + return new AppAction(info.get_display_name(), Icon.get_icon_name(icon), info.get_commandline()); + } + + ///////////////////////////////////////////////////////////////////// + /// A helper method which creates an AppAction for given *.desktop + /// file. + ///////////////////////////////////////////////////////////////////// + + public static Action? new_for_desktop_file(string file_name) { + // check whether its a desktop file to open one of Gnome-Pie's pies + if (file_name.has_prefix(Paths.launchers)) { + string id = file_name.substring((long)file_name.length - 11, 3); + return new PieAction(id); + } + + var info = new DesktopAppInfo.from_filename(file_name); + return new_for_app_info(info); + } + + ///////////////////////////////////////////////////////////////////// + /// A helper method which creates an AppAction for given mime type. + ///////////////////////////////////////////////////////////////////// + + public static Action? default_for_mime_type(string type) { + var info = AppInfo.get_default_for_type(type, false); + return new_for_app_info(info); + } + + ///////////////////////////////////////////////////////////////////// + /// A helper method which creates an AppAction for given uri scheme. + ///////////////////////////////////////////////////////////////////// + + public static Action? default_for_uri(string uri) { + var info = AppInfo.get_default_for_uri_scheme(uri); + return new_for_app_info(info); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns for example www.google.com when http://www.google.de/?q=h + /// is given. + ///////////////////////////////////////////////////////////////////// + + private static string get_domain_name(string url) { + int domain_end = url.index_of_char('/', 7); + int domain_begin = url.index_of_char('/', 0) + 2; + + if (domain_begin < domain_end) return url.substring(domain_begin, domain_end-domain_begin); + + return url; + } +} + +} diff --git a/src/actions/appAction.vala b/src/actions/appAction.vala new file mode 100644 index 0000000..e1ca3a2 --- /dev/null +++ b/src/actions/appAction.vala @@ -0,0 +1,76 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This type of Action launches an application or a custom command. +///////////////////////////////////////////////////////////////////////// + +public class AppAction : Action { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of Action. It sets the display name + /// for this Action, whether it has a custom Icon/Name and the string + /// used in the pies.conf file for this kind of Actions. + ///////////////////////////////////////////////////////////////////// + + public static ActionRegistry.TypeDescription register() { + var description = new ActionRegistry.TypeDescription(); + description.name = _("Launch application"); + description.icon = "application-x-executable"; + description.description = _("Executes the given command."); + description.icon_name_editable = true; + description.id = "app"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// Stores the command line. + ///////////////////////////////////////////////////////////////////// + + public override string real_command { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// Simply returns the real_command. No beautification. + ///////////////////////////////////////////////////////////////////// + + public override string display_command { get {return real_command;} } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public AppAction(string name, string icon, string command, bool is_quickaction = false) { + GLib.Object(name : name, icon : icon, real_command : command, is_quickaction : is_quickaction); + } + + ///////////////////////////////////////////////////////////////////// + /// Launches the desired command. + ///////////////////////////////////////////////////////////////////// + + public override void activate(uint32 time_stamp) { + try{ + var item = GLib.AppInfo.create_from_commandline(this.real_command, null, GLib.AppInfoCreateFlags.NONE); + item.launch(null, null); + } catch (Error e) { + warning(e.message); + } + } +} + +} diff --git a/src/actions/keyAction.vala b/src/actions/keyAction.vala new file mode 100644 index 0000000..cbe8b6e --- /dev/null +++ b/src/actions/keyAction.vala @@ -0,0 +1,81 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This type of Action "presses" a key stroke. +///////////////////////////////////////////////////////////////////////// + +public class KeyAction : Action { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of Action. It sets the display name + /// for this Action, whether it has a custom Icon/Name and the string + /// used in the pies.conf file for this kind of Actions. + ///////////////////////////////////////////////////////////////////// + + public static ActionRegistry.TypeDescription register() { + var description = new ActionRegistry.TypeDescription(); + description.name = _("Press hotkey"); + description.icon = "preferences-desktop-keyboard-shortcuts"; + description.description = _("Simulates the activation of a hotkey."); + description.icon_name_editable = true; + description.id = "key"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// Stores the accelerator of this action. + ///////////////////////////////////////////////////////////////////// + + public override string real_command { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// Returns a human readable form of the accelerator. + ///////////////////////////////////////////////////////////////////// + + public override string display_command { get {return key.label;} } + + ///////////////////////////////////////////////////////////////////// + /// The simulated key which gets 'pressed' on execution. + ///////////////////////////////////////////////////////////////////// + + public Key key { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public KeyAction(string name, string icon, string command, bool is_quickaction = false) { + GLib.Object(name : name, icon : icon, real_command : command, is_quickaction : is_quickaction); + } + + construct { + this.key = new Key.from_string(real_command); + } + + ///////////////////////////////////////////////////////////////////// + /// Presses the desired key. + ///////////////////////////////////////////////////////////////////// + + public override void activate(uint32 time_stamp) { + key.press(); + } +} + +} diff --git a/src/actions/pieAction.vala b/src/actions/pieAction.vala new file mode 100644 index 0000000..931c9d3 --- /dev/null +++ b/src/actions/pieAction.vala @@ -0,0 +1,103 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This Action opens another pie. +///////////////////////////////////////////////////////////////////////// + +public class PieAction : Action { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of Action. It sets the display name + /// for this Action, whether it has a custom Icon/Name and the string + /// used in the pies.conf file for this kind of Actions. + ///////////////////////////////////////////////////////////////////// + + public static ActionRegistry.TypeDescription register() { + var description = new ActionRegistry.TypeDescription(); + description.name = _("Open Pie"); + description.icon = "gnome-pie"; + description.description = _("Opens another Pie of Gnome-Pie. You may create sub menus this way."); + description.icon_name_editable = false; + description.id = "pie"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// Stores the ID of the referenced Pie. + ///////////////////////////////////////////////////////////////////// + + public override string real_command { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// Returns the name of the referenced Pie. + ///////////////////////////////////////////////////////////////////// + + public override string display_command { get {return name;} } + + ///////////////////////////////////////////////////////////////////// + /// Returns the name of the referenced Pie. + ///////////////////////////////////////////////////////////////////// + + public override string name { + get { + var referee = PieManager.all_pies[real_command]; + if (referee != null) { + owned_name = "↪" + referee.name; + return owned_name; + } + return ""; + } + protected set {} + } + + private string owned_name; + + ///////////////////////////////////////////////////////////////////// + /// Returns the icon of the referenced Pie. + ///////////////////////////////////////////////////////////////////// + + public override string icon { + get { + var referee = PieManager.all_pies[real_command]; + if (referee != null) + return referee.icon; + return ""; + } + protected set {} + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public PieAction(string id, bool is_quickaction = false) { + GLib.Object(name : "", icon : "", real_command : id, is_quickaction : is_quickaction); + } + + ///////////////////////////////////////////////////////////////////// + /// Opens the desired Pie. + ///////////////////////////////////////////////////////////////////// + + public override void activate(uint32 time_stamp) { + PieManager.open_pie(real_command); + } +} + +} diff --git a/src/actions/sigAction.vala b/src/actions/sigAction.vala new file mode 100644 index 0000000..bf9374d --- /dev/null +++ b/src/actions/sigAction.vala @@ -0,0 +1,63 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This type of Action can't be selected by the user, therefore there is +/// no register() method for this class. But it may be useful for +/// ActionGroups: It emits a signal on activation. +///////////////////////////////////////////////////////////////////////// + +public class SigAction : Action { + + ///////////////////////////////////////////////////////////////////// + /// This signal is emitted on activation. + ///////////////////////////////////////////////////////////////////// + + public signal void activated(uint32 time_stamp); + + ///////////////////////////////////////////////////////////////////// + /// This may store something useful. + ///////////////////////////////////////////////////////////////////// + + public override string real_command { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// Only for inheritance... Greetings to Liskov. + ///////////////////////////////////////////////////////////////////// + + public override string display_command { get {return real_command;} } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public SigAction(string name, string icon, string command, bool is_quickaction = false) { + GLib.Object(name : name, icon : icon, real_command : command, is_quickaction : is_quickaction); + } + + ///////////////////////////////////////////////////////////////////// + /// Emits the signal on activation. + ///////////////////////////////////////////////////////////////////// + + public override void activate(uint32 time_stamp) { + this.activated(time_stamp); + } +} + +} diff --git a/src/actions/uriAction.vala b/src/actions/uriAction.vala new file mode 100644 index 0000000..d0a41b8 --- /dev/null +++ b/src/actions/uriAction.vala @@ -0,0 +1,77 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This type of Action opens the default application for an URI. +///////////////////////////////////////////////////////////////////////// + +public class UriAction : Action { + + ///////////////////////////////////////////////////////////////////// + /// Used to register this type of Action. It sets the display name + /// for this Action, whether it has a custom Icon/Name and the string + /// used in the pies.conf file for this kind of Actions. + ///////////////////////////////////////////////////////////////////// + + public static ActionRegistry.TypeDescription register() { + var description = new ActionRegistry.TypeDescription(); + description.name = _("Open URI"); + description.icon = "web-browser"; + description.description = _("Opens a given location. You may use URL's or files paths."); + description.icon_name_editable = true; + description.id = "uri"; + return description; + } + + ///////////////////////////////////////////////////////////////////// + /// The URI of this Action. + ///////////////////////////////////////////////////////////////////// + + public override string real_command { get; construct set; } + + ///////////////////////////////////////////////////////////////////// + /// Returns only the real URI. An URI can't be beautified. + ///////////////////////////////////////////////////////////////////// + + public override string display_command { get {return real_command;} } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public UriAction(string name, string icon, string command, bool is_quickaction = false) { + GLib.Object(name : name, icon : icon, + real_command : command.has_prefix("www") ? "http://" + command : command, + is_quickaction : is_quickaction); + } + + ///////////////////////////////////////////////////////////////////// + /// Opens the default application for the URI. + ///////////////////////////////////////////////////////////////////// + + public override void activate(uint32 time_stamp) { + try{ + GLib.AppInfo.launch_default_for_uri(real_command, null); + } catch (Error e) { + warning(e.message); + } + } +} + +} diff --git a/src/daemon.vala b/src/daemon.vala new file mode 100644 index 0000000..c5912b3 --- /dev/null +++ b/src/daemon.vala @@ -0,0 +1,262 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class runs in the background. It has an Indicator sitting in the +/// user's panel. It initializes everything and guarantees that there is +/// only one instance of Gnome-Pie running. +///////////////////////////////////////////////////////////////////////// + +public class Daemon : GLib.Application { + + ///////////////////////////////////////////////////////////////////// + /// The current version of Gnome-Pie + ///////////////////////////////////////////////////////////////////// + + public static string version; + + ///////////////////////////////////////////////////////////////////// + /// Varaibles set by the commend line parser. + ///////////////////////////////////////////////////////////////////// + + public static bool disable_header_bar = false; + public static bool disable_stack_switcher = false; + + + ///////////////////////////////////////////////////////////////////// + /// true if init_pies() has been called already + ///////////////////////////////////////////////////////////////////// + private bool initialized = false; + + ///////////////////////////////////////////////////////////////////// + /// The beginning of everything. + ///////////////////////////////////////////////////////////////////// + + public static int main(string[] args) { + version = "0.6.8"; + + // disable overlay scrollbar --- hacky workaround for black / + // transparent background + GLib.Environment.set_variable("LIBOVERLAY_SCROLLBAR", "0", true); + + Logger.init(); + Gtk.init(ref args); + Paths.init(); + + // create the Daemon and run it + var deamon = new GnomePie.Daemon(); + deamon.run(args); + + return 0; + } + + ///////////////////////////////////////////////////////////////////// + /// The AppIndicator of Gnome-Pie. + ///////////////////////////////////////////////////////////////////// + + private Indicator indicator = null; + + ///////////////////////////////////////////////////////////////////// + /// Varaibles set by the commend line parser. + ///////////////////////////////////////////////////////////////////// + + private static string open_pie = null; + private static bool reset = false; + private static bool print_ids = false; + + private static bool handled_local_args = false; + + ///////////////////////////////////////////////////////////////////// + /// Available command line options. + ///////////////////////////////////////////////////////////////////// + + private const GLib.OptionEntry[] options = { + { "open", 'o', 0, GLib.OptionArg.STRING, + out open_pie, + "Open the Pie with the given ID", "ID" }, + { "reset", 'r', 0, GLib.OptionArg.NONE, + out reset, + "Reset all options to default values" }, + { "no-header-bar", 'b', 0, GLib.OptionArg.NONE, + out disable_header_bar, + "Disables the usage of GTK.HeaderBar" }, + { "no-stack-switcher", 's', 0, GLib.OptionArg.NONE, + out disable_stack_switcher, + "Disables the usage of GTK.StackSwitcher" }, + { "print-ids", 'p', 0, GLib.OptionArg.NONE, + out print_ids, + "Prints all Pie names with their according IDs" }, + { null } + }; + + ///////////////////////////////////////////////////////////////////// + /// C'tor of the Daemon. It checks whether it's the first running + /// instance of Gnome-Pie. + ///////////////////////////////////////////////////////////////////// + + public Daemon() { + Object(application_id: "org.gnome.gnomepie", + flags: GLib.ApplicationFlags.HANDLES_COMMAND_LINE); + + // init locale support + Intl.bindtextdomain("gnomepie", Paths.locales); + Intl.textdomain("gnomepie"); + + // connect SigHandlers + Posix.signal(Posix.SIGINT, sig_handler); + Posix.signal(Posix.SIGTERM, sig_handler); + + this.startup.connect(()=>{ + + message("Welcome to Gnome-Pie " + version + "!"); + + this.init_pies(); + + // launch the indicator + this.indicator = new Indicator(); + + if (open_pie != null && open_pie != "") { + PieManager.open_pie(open_pie); + open_pie = ""; + } + + // finished loading... so run the prog! + message("Started happily..."); + hold(); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Call handle_command_line on program launch. + ///////////////////////////////////////////////////////////////////// + + protected override bool local_command_line( + ref unowned string[] args, out int exit_status) { + + exit_status = 0; + + // copy command line + string*[] _args = new string[args.length]; + for (int i = 0; i < args.length; i++) { + _args[i] = args[i]; + } + return handle_command_line(_args, false); + } + + ///////////////////////////////////////////////////////////////////// + /// Call handle_command_line when a remote instance was launched. + ///////////////////////////////////////////////////////////////////// + + protected override int command_line(GLib.ApplicationCommandLine cmd) { + if (handled_local_args) { + string[] tmp = cmd.get_arguments(); + unowned string[] remote_args = tmp; + handle_command_line(remote_args, true); + } + handled_local_args = true; + return 0; + } + + ///////////////////////////////////////////////////////////////////// + /// Print a nifty message when the prog is killed. + ///////////////////////////////////////////////////////////////////// + + private static void sig_handler(int sig) { + stdout.printf("\n"); + message("Caught signal (%d), bye!".printf(sig)); + GLib.Application.get_default().release(); + } + + ///////////////////////////////////////////////////////////////////// + /// Print a nifty message when the prog is killed. + ///////////////////////////////////////////////////////////////////// + + private void init_pies() { + if (!this.initialized) { + + // init static stuff + ActionRegistry.init(); + GroupRegistry.init(); + + // load all pies + PieManager.init(); + + // initialize icon cache + Icon.init(); + + this.initialized = true; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Handles command line parameters. + ///////////////////////////////////////////////////////////////////// + + private bool handle_command_line(string[] args, bool called_from_remote) { + + var context = new GLib.OptionContext(" - Launches the pie menu for linux."); + context.add_main_entries(options, null); + context.add_group(Gtk.get_option_group(false)); + + try { + context.parse(ref args); + } catch(GLib.OptionError error) { + warning(error.message); + message("Run '%s' to launch Gnome-Pie or run '%s --help' to" + + " see a full list of available command line options.\n", + args[0], args[0]); + } + + if (reset) { + if (GLib.FileUtils.remove(Paths.pie_config) == 0) { + message("Removed file \"%s\"", Paths.pie_config); + } + if (GLib.FileUtils.remove(Paths.settings) == 0) { + message("Removed file \"%s\"", Paths.settings); + } + + // do not notify the already running instance (if any) + return true; + } + + if (print_ids) { + this.init_pies(); + PieManager.print_ids(); + print_ids = false; + + // do not notify the already running instance (if any) + return true; + } + + + if (called_from_remote) { + if (open_pie != null && open_pie != "") { + PieManager.open_pie(open_pie); + open_pie = ""; + } else { + this.indicator.show_preferences(); + } + } + + // notify the already running instance (if any) + return false; + } +} + +} diff --git a/src/gui/aboutWindow.vala b/src/gui/aboutWindow.vala new file mode 100644 index 0000000..73fb1be --- /dev/null +++ b/src/gui/aboutWindow.vala @@ -0,0 +1,84 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A simple about dialog. +///////////////////////////////////////////////////////////////////////// + +public class AboutWindow: Gtk.AboutDialog { + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new about dialog. The entries are sorted alpha- + /// betically. + ///////////////////////////////////////////////////////////////////// + + public AboutWindow () { + string[] devs = { + "Simon Schneegans <code@simonschneegans.de>", + "Gabriel Dubatti <gdubatti@gmail.com>", + "Francesco Piccinno <stack.box@gmail.com>", + "György Balló <ballogyor@gmail.com>" + }; + string[] artists = { + "Simon Schneegans <code@simonschneegans.de>" + }; + string[] translators = { + "Simon Schneegans <code@simonschneegans.de> (DE, EN)", + "Riccardo Traverso <gr3yfox.fw@gmail.com> (IT)", + "Magnun Leno <magnun@codecommunity.org> (PT-BR)", + "Kim Boram <Boramism@gmail.com> (KO)", + "Eduardo Anabalon <lalo1412@gmail.com> (ES)", + "Moo <hazap@hotmail.com> (LT)", + "Gabriel Dubatti <gdubatti@gmail.com> (ES)", + "Grégoire Bellon-Gervais <greggbg@gmail.com> (FR)", + "Raphaël Rochet <raphael@rri.fr> (FR)", + "Alex Maxime <cad.maxime@gmail.com> (FR)", + "Eugene Roskin <pams@imail.ru> (RU)", + "Ting Zhou <tzhou@haverford.edu> (ZH-CN)", + "Martin Dinov <martindinov@yahoo.com> (BG)" + }; + + // sort translators + GLib.List<string> translator_list = new GLib.List<string>(); + foreach (var translator in translators) + translator_list.append(translator); + + translator_list.sort((a, b) => { + return a.ascii_casecmp(b); + }); + + string translator_string = ""; + foreach (var translator in translator_list) + translator_string += translator + "\n"; + + GLib.Object ( + artists : artists, + authors : devs, + translator_credits : translator_string, + copyright : "Copyright (C) 2011-2015 Simon Schneegans <code@simonschneegans.de>", + program_name: "Gnome-Pie", + logo_icon_name: "gnome-pie", + website: "http://simmesimme.github.io/gnome-pie.html", + website_label: "Homepage", + version: Daemon.version + ); + } +} + +} diff --git a/src/gui/iconSelectWindow.vala b/src/gui/iconSelectWindow.vala new file mode 100644 index 0000000..ce610ea --- /dev/null +++ b/src/gui/iconSelectWindow.vala @@ -0,0 +1,450 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A window which allows selection of an Icon of the user's current icon +/// theme. Custom icons/images can be selested as well. Loading of icons +/// happens in an extra thread and a spinner is displayed while loading. +///////////////////////////////////////////////////////////////////////// + +public class IconSelectWindow : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// This signal gets emitted when the user selects a new icon. + ///////////////////////////////////////////////////////////////////// + + public signal void on_ok(string icon_name); + + ///////////////////////////////////////////////////////////////////// + /// Stores the currently selected icon. + ///////////////////////////////////////////////////////////////////// + + private string active_icon = ""; + + ///////////////////////////////////////////////////////////////////// + /// The ListStore storing all theme-icons. + ///////////////////////////////////////////////////////////////////// + + private static Gtk.ListStore icon_list = null; + + ///////////////////////////////////////////////////////////////////// + /// True, if the icon theme is currently reloaded. + ///////////////////////////////////////////////////////////////////// + + private static bool loading = false; + + ///////////////////////////////////////////////////////////////////// + /// If set to true, the icon list will be reloaded next time the + /// window opens. + ///////////////////////////////////////////////////////////////////// + + private static bool need_reload = true; + + ///////////////////////////////////////////////////////////////////// + /// Icons of these contexts won't appear in the list. + ///////////////////////////////////////////////////////////////////// + + private const string disabled_contexts = "Animations, FileSystems"; + + ///////////////////////////////////////////////////////////////////// + /// The list of icons, filtered according to the chosen type and + /// filter string. + ///////////////////////////////////////////////////////////////////// + + private Gtk.TreeModelFilter icon_list_filtered = null; + + ///////////////////////////////////////////////////////////////////// + /// The Gtk widget displaying the icons. + ///////////////////////////////////////////////////////////////////// + + private Gtk.IconView icon_view = null; + + ///////////////////////////////////////////////////////////////////// + /// This spinner is displayed when the icons are loaded. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Spinner spinner = null; + + ///////////////////////////////////////////////////////////////////// + /// A Gtk widget used for custom icon/image selection. + ///////////////////////////////////////////////////////////////////// + + private Gtk.FileChooserWidget file_chooser = null; + + ///////////////////////////////////////////////////////////////////// + /// The notebook containing the different icon choice possibilities: + /// from the theme or custom. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Notebook tabs = null; + + ///////////////////////////////////////////////////////////////////// + /// The main window. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Window window = null; + + ///////////////////////////////////////////////////////////////////// + /// A little structure containing data for one icon in the icon_view. + ///////////////////////////////////////////////////////////////////// + + private class ListEntry { + public string name; + public IconContext context; + public Gdk.Pixbuf pixbuf; + } + + ///////////////////////////////////////////////////////////////////// + /// This queue is used for icon loading. A loading thread pushes + /// icons into it --- the main thread updates the icon_view + /// accordingly. + ///////////////////////////////////////////////////////////////////// + + private GLib.AsyncQueue<ListEntry?> load_queue; + + ///////////////////////////////////////////////////////////////////// + /// Possible icon types. + ///////////////////////////////////////////////////////////////////// + + private enum IconContext { + ALL, + APPS, + ACTIONS, + PLACES, + FILES, + EMOTES, + OTHER + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new IconSelectWindow. + ///////////////////////////////////////////////////////////////////// + + public IconSelectWindow(Gtk.Window parent) { + try { + this.load_queue = new GLib.AsyncQueue<ListEntry?>(); + + if (IconSelectWindow.icon_list == null) { + IconSelectWindow.icon_list = new Gtk.ListStore(3, typeof(string), // icon name + typeof(IconContext), // icon type + typeof(Gdk.Pixbuf)); // the icon itself + + // disable sorting until all icons are loaded + // else loading becomes horribly slow + IconSelectWindow.icon_list.set_default_sort_func(() => {return 0;}); + + // reload if icon theme changes + Gtk.IconTheme.get_default().changed.connect(() => { + if (this.window.visible) load_icons(); + else IconSelectWindow.need_reload = true; + }); + } + + // make the icon_view filterable + this.icon_list_filtered = new Gtk.TreeModelFilter(IconSelectWindow.icon_list, null); + + Gtk.Builder builder = new Gtk.Builder(); + + builder.add_from_file (Paths.ui_files + "/icon_select.ui"); + + this.window = builder.get_object("window") as Gtk.Window; + this.window.set_transient_for(parent); + this.window.set_modal(true); + + this.tabs = builder.get_object("tabs") as Gtk.Notebook; + + this.spinner = builder.get_object("spinner") as Gtk.Spinner; + this.spinner.start(); + + (builder.get_object("ok-button") as Gtk.Button).clicked.connect(on_ok_button_clicked); + (builder.get_object("cancel-button") as Gtk.Button).clicked.connect(on_cancel_button_clicked); + + var combo_box = builder.get_object("combo-box") as Gtk.Box; + + // context combo + var context_combo = new Gtk.ComboBoxText(); + context_combo.append_text(_("All icons")); + context_combo.append_text(_("Applications")); + context_combo.append_text(_("Actions")); + context_combo.append_text(_("Places")); + context_combo.append_text(_("File types")); + context_combo.append_text(_("Emotes")); + context_combo.append_text(_("Miscellaneous")); + + context_combo.set_active(0); + + context_combo.changed.connect(() => { + this.icon_list_filtered.refilter(); + }); + + combo_box.pack_start(context_combo, false, false); + + // string filter entry + var filter = builder.get_object("filter-entry") as Gtk.Entry; + + // only display items which have the selected type + // and whose name contains the text entered in the entry + this.icon_list_filtered.set_visible_func((model, iter) => { + string name = ""; + IconContext context = IconContext.ALL; + model.get(iter, 0, out name); + model.get(iter, 1, out context); + + if (name == null) return false; + + return (context_combo.get_active() == context || + context_combo.get_active() == IconContext.ALL) && + name.down().contains(filter.text.down()); + }); + + // clear when the users clicks on the "clear" icon + filter.icon_release.connect((pos, event) => { + if (pos == Gtk.EntryIconPosition.SECONDARY) + filter.text = ""; + }); + + // refilter on input + filter.notify["text"].connect(() => { + this.icon_list_filtered.refilter(); + }); + + // container for the icon_view + var scroll = builder.get_object("icon-scrolledwindow") as Gtk.ScrolledWindow; + + // displays the filtered icons + this.icon_view = new Gtk.IconView.with_model(this.icon_list_filtered); + this.icon_view.item_width = 32; + this.icon_view.item_padding = 2; + this.icon_view.pixbuf_column = 2; + this.icon_view.tooltip_column = 0; + + // set active_icon if selection changes + this.icon_view.selection_changed.connect(() => { + foreach (var path in this.icon_view.get_selected_items()) { + Gtk.TreeIter iter; + this.icon_list_filtered.get_iter(out iter, path); + this.icon_list_filtered.get(iter, 0, out this.active_icon); + } + }); + + // hide this window when the user activates an icon + this.icon_view.item_activated.connect((path) => { + Gtk.TreeIter iter; + this.icon_list_filtered.get_iter(out iter, path); + this.icon_list_filtered.get(iter, 0, out this.active_icon); + this.on_ok(this.active_icon); + this.window.hide(); + }); + + scroll.add(this.icon_view); + + // file chooser widget + this.file_chooser = builder.get_object("filechooser") as Gtk.FileChooserWidget; + var file_filter = new Gtk.FileFilter(); + file_filter.add_pixbuf_formats(); + file_filter.set_filter_name(_("All supported image formats")); + file_chooser.add_filter(file_filter); + + // set active_icon if the user selected a file + file_chooser.selection_changed.connect(() => { + if (file_chooser.get_filename() != null && + GLib.FileUtils.test(file_chooser.get_filename(), + GLib.FileTest.IS_REGULAR)) + + this.active_icon = file_chooser.get_filename(); + }); + + // hide this window when the user activates a file + file_chooser.file_activated.connect(() => { + this.active_icon = file_chooser.get_filename(); + this.on_ok(this.active_icon); + this.window.hide(); + }); + + this.window.set_focus(this.icon_view); + this.window.delete_event.connect(this.window.hide_on_delete); + + } catch (GLib.Error e) { + error("Could not load UI: %s\n", e.message); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Displays the window. The icons are reloaded if neccessary. + ///////////////////////////////////////////////////////////////////// + + public void show() { + this.window.show_all(); + this.spinner.hide(); + + if (IconSelectWindow.need_reload) { + IconSelectWindow.need_reload = false; + this.load_icons(); + } + } + + public static void clear_icons() { + if (IconSelectWindow.icon_list != null) { + IconSelectWindow.need_reload = true; + IconSelectWindow.icon_list.clear(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Makes the window select the icon of the given Pie. + ///////////////////////////////////////////////////////////////////// + + public void set_icon(string icon) { + this.active_icon = icon; + + if (icon.contains("/")) { + this.file_chooser.set_filename(icon); + this.tabs.set_current_page(1); + } else { + this.icon_list_filtered.foreach((model, path, iter) => { + string name = ""; + model.get(iter, 0, out name); + + if (name == icon) { + this.icon_view.select_path(path); + this.icon_view.scroll_to_path(path, true, 0.5f, 0.0f); + this.icon_view.set_cursor(path, null, false); + } + return (name == icon); + }); + + this.tabs.set_current_page(0); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user clicks the ok button. + ///////////////////////////////////////////////////////////////////// + + private void on_ok_button_clicked() { + this.on_ok(this.active_icon); + this.window.hide(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user clicks the cancel button. + ///////////////////////////////////////////////////////////////////// + + private void on_cancel_button_clicked() { + this.window.hide(); + } + + ///////////////////////////////////////////////////////////////////// + /// (Re)load all icons. + ///////////////////////////////////////////////////////////////////// + + private void load_icons() { + // only if it's not loading currently + if (!IconSelectWindow.loading) { + IconSelectWindow.loading = true; + IconSelectWindow.icon_list.clear(); + + // display the spinner + if (spinner != null) + this.spinner.visible = true; + + // disable sorting of the icon_view - else it's horribly slow + IconSelectWindow.icon_list.set_sort_column_id(-1, Gtk.SortType.ASCENDING); + + this.load_all.begin(); + + // insert loaded icons every 200 ms + Timeout.add(200, () => { + while (this.load_queue.length() > 0) { + var new_entry = this.load_queue.pop(); + Gtk.TreeIter current; + IconSelectWindow.icon_list.append(out current); + IconSelectWindow.icon_list.set(current, 0, new_entry.name, + 1, new_entry.context, + 2, new_entry.pixbuf); + } + + // enable sorting of the icon_view if loading finished + if (!IconSelectWindow.loading) { + IconSelectWindow.icon_list.set_sort_column_id(0, Gtk.SortType.ASCENDING); + } + + return loading; + }); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all icons of an icon theme and pushes them into the + /// load_queue. + ///////////////////////////////////////////////////////////////////// + + private async void load_all() { + var icon_theme = Gtk.IconTheme.get_default(); + + foreach (var context in icon_theme.list_contexts()) { + if (!disabled_contexts.contains(context)) { + foreach (var icon in icon_theme.list_icons(context)) { + + IconContext icon_context = IconContext.OTHER; + switch(context) { + case "Apps": case "Applications": + icon_context = IconContext.APPS; break; + case "Emotes": + icon_context = IconContext.EMOTES; break; + case "Places": case "Devices": + icon_context = IconContext.PLACES; break; + case "Mimetypes": + icon_context = IconContext.FILES; break; + case "Actions": + icon_context = IconContext.ACTIONS; break; + default: break; + } + + Idle.add(load_all.callback); + yield; + + try { + // create a new entry for the queue + var new_entry = new ListEntry(); + new_entry.name = icon; + new_entry.context = icon_context; + new_entry.pixbuf = icon_theme.load_icon(icon, 32, 0); + + // some icons have only weird sizes... do not include them + if (new_entry.pixbuf.width == 32) + this.load_queue.push(new_entry); + + } catch (GLib.Error e) { + warning("Failed to load image " + icon); + } + } + } + } + + // finished loading + IconSelectWindow.loading = false; + + // hide the spinner + if (spinner != null) + spinner.visible = false; + } +} + +} diff --git a/src/gui/indicator.vala b/src/gui/indicator.vala new file mode 100644 index 0000000..ddb85e6 --- /dev/null +++ b/src/gui/indicator.vala @@ -0,0 +1,180 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// An appindicator sitting in the panel. It owns the settings menu. +///////////////////////////////////////////////////////////////////////// + +public class Indicator : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The internally used indicator. + ///////////////////////////////////////////////////////////////////// + + #if HAVE_APPINDICATOR + private AppIndicator.Indicator indicator { private get; private set; } + #else + private Gtk.StatusIcon indicator {private get; private set; } + private Gtk.Menu menu {private get; private set; } + #endif + + ///////////////////////////////////////////////////////////////////// + /// The Preferences Menu of Gnome-Pie. + ///////////////////////////////////////////////////////////////////// + + private PreferencesWindow prefs { private get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Returns true, when the indicator is currently visible. + ///////////////////////////////////////////////////////////////////// + + public bool active { + get { + #if HAVE_APPINDICATOR + return indicator.get_status() == AppIndicator.IndicatorStatus.ACTIVE; + #else + return indicator.get_visible(); + #endif + } + set { + #if HAVE_APPINDICATOR + if (value) indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); + else indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE); + #else + indicator.set_visible(value); + #endif + } + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs a new Indicator, residing in the user's panel. + ///////////////////////////////////////////////////////////////////// + + public Indicator() { + string icon = "gnome-pie-symbolic"; + var screen = (Gdk.X11.Screen)Gdk.Screen.get_default(); + bool gnome_shell = false; + + if (screen.get_window_manager_name() == "GNOME Shell") { + icon = "gnome-pie"; + gnome_shell = true; + } + + #if HAVE_APPINDICATOR + + string path = ""; + try { + path = GLib.Path.get_dirname(GLib.FileUtils.read_link("/proc/self/exe"))+"/resources"; + } catch (GLib.FileError e) { + warning("Failed to get path of executable!"); + } + + if (gnome_shell) { + + if (GLib.File.new_for_path(path).query_exists()) { + this.indicator = new AppIndicator.Indicator("Gnome-Pie", path + "/" + icon + ".svg", + AppIndicator.IndicatorCategory.APPLICATION_STATUS); + } else { + this.indicator = new AppIndicator.Indicator("Gnome-Pie", icon, + AppIndicator.IndicatorCategory.APPLICATION_STATUS); + } + } else { + this.indicator = new AppIndicator.Indicator.with_path("Gnome-Pie", icon, + AppIndicator.IndicatorCategory.APPLICATION_STATUS, path); + } + var menu = new Gtk.Menu(); + #else + this.indicator = new Gtk.StatusIcon(); + try { + var file = GLib.File.new_for_path(GLib.Path.build_filename( + GLib.Path.get_dirname(GLib.FileUtils.read_link("/proc/self/exe"))+"/resources", + icon + ".svg" + )); + + if (!file.query_exists()) + this.indicator.set_from_icon_name(icon); + else + this.indicator.set_from_file(file.get_path()); + } catch (GLib.FileError e) { + warning("Failed to get path of executable!"); + this.indicator.set_from_icon_name(icon); + } + + this.menu = new Gtk.Menu(); + var menu = this.menu; + #endif + + this.prefs = new PreferencesWindow(); + + // preferences item + var item = new Gtk.ImageMenuItem.with_mnemonic(_("_Preferences")); + item.activate.connect(() => { + this.prefs.show(); + }); + + item.show(); + menu.append(item); + + // about item + item = new Gtk.ImageMenuItem.with_mnemonic(_("_About")); + item.show(); + item.activate.connect(() => { + var about = new AboutWindow(); + about.run(); + about.destroy(); + }); + menu.append(item); + + // separator + var sepa = new Gtk.SeparatorMenuItem(); + sepa.show(); + menu.append(sepa); + + // quit item + item = new Gtk.ImageMenuItem.with_mnemonic(_("_Quit")); + item.activate.connect(()=>{ + GLib.Application.get_default().release(); + }); + item.show(); + menu.append(item); + + #if HAVE_APPINDICATOR + this.indicator.set_menu(menu); + #else + this.indicator.popup_menu.connect((btn, time) => { + this.menu.popup(null, null, null, btn, time); + }); + #endif + + this.active = Config.global.show_indicator; + Config.global.notify["show-indicator"].connect((s, p) => { + this.active = Config.global.show_indicator; + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Shows the preferences menu. + ///////////////////////////////////////////////////////////////////// + + public void show_preferences() { + this.prefs.show(); + } +} + +} diff --git a/src/gui/newSliceWindow.vala b/src/gui/newSliceWindow.vala new file mode 100644 index 0000000..89294b5 --- /dev/null +++ b/src/gui/newSliceWindow.vala @@ -0,0 +1,433 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A window which allows selection of a new Slice which is about to be +/// added to a Pie. It can be also used to edit an existing Slice +///////////////////////////////////////////////////////////////////////// + +public class NewSliceWindow : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// This signal gets emitted when the user confirms his selection. + ///////////////////////////////////////////////////////////////////// + + public signal void on_select(ActionGroup action, bool as_new_slice, int at_position); + + ///////////////////////////////////////////////////////////////////// + /// The contained list of slice types. It contains both: Groups and + /// single actions. + ///////////////////////////////////////////////////////////////////// + + private SliceTypeList slice_type_list = null; + + ///////////////////////////////////////////////////////////////////// + /// The IconSelectWindow used for icon selection for a Slice. + ///////////////////////////////////////////////////////////////////// + + private IconSelectWindow? icon_window = null; + + ///////////////////////////////////////////////////////////////////// + /// Some widgets of this window. Loaded by a ui-builder and stored + /// for later access. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Dialog window = null; + private Gtk.Box name_box = null; + private Gtk.Box command_box = null; + private Gtk.Button icon_button = null; + private Gtk.Box no_options_box = null; + private Gtk.Box pie_box = null; + private Gtk.Box hotkey_box = null; + private Gtk.Box uri_box = null; + private Gtk.Box quickaction_box = null; + private Gtk.Box clipboard_box = null; + private Gtk.Box workspace_only_box = null; + private Gtk.Image icon = null; + private Gtk.Entry name_entry = null; + private Gtk.Entry command_entry = null; + private Gtk.Entry uri_entry = null; + private Gtk.Switch quickaction_checkbutton = null; + private Gtk.Switch workspace_only_checkbutton = null; + private Gtk.Scale clipboard_slider = null; + + ///////////////////////////////////////////////////////////////////// + /// Two custom widgets. For Pie and hotkey selection respectively. + ///////////////////////////////////////////////////////////////////// + + private PieComboList pie_select = null; + private TriggerSelectButton key_select = null; + + ///////////////////////////////////////////////////////////////////// + /// These members store information on the currently selected Slice. + ///////////////////////////////////////////////////////////////////// + + private string current_type = ""; + private string current_icon = ""; + private string current_id = ""; + private string current_custom_icon = ""; + private string current_hotkey = ""; + private string current_pie_to_open = ""; + + ///////////////////////////////////////////////////////////////////// + /// The position of the edited Slice in its parent Pie. + ///////////////////////////////////////////////////////////////////// + + private int slice_position = 0; + + ///////////////////////////////////////////////////////////////////// + /// True, if the Slice i going to be added as a new Slice. Else it + /// will edit the Slice at slice_position in its parent Pie. + ///////////////////////////////////////////////////////////////////// + + private bool add_as_new_slice = true; + + ///////////////////////////////////////////////////////////////////// + /// C'tor creates a new window. + ///////////////////////////////////////////////////////////////////// + + public NewSliceWindow() { + try { + + Gtk.Builder builder = new Gtk.Builder(); + + builder.add_from_file (Paths.ui_files + "/slice_select.ui"); + + this.slice_type_list = new SliceTypeList(); + this.slice_type_list.on_select.connect((type, icon) => { + + this.name_box.hide(); + this.command_box.hide(); + this.icon_button.sensitive = false; + this.no_options_box.hide(); + this.pie_box.hide(); + this.hotkey_box.hide(); + this.uri_box.hide(); + this.quickaction_box.hide(); + this.workspace_only_box.hide(); + this.clipboard_box.hide(); + + this.current_type = type; + + switch (type) { + case "bookmarks": case "devices": + case "menu": case "session": + this.no_options_box.show(); + this.set_icon(icon); + break; + case "window_list": + this.workspace_only_box.show(); + this.set_icon(icon); + break; + case "clipboard": + this.clipboard_box.show(); + this.set_icon(icon); + break; + case "app": + this.name_box.show(); + this.command_box.show(); + this.quickaction_box.show(); + this.icon_button.sensitive = true; + if (this.current_custom_icon == "") this.set_icon(icon); + else this.set_icon(this.current_custom_icon); + break; + case "key": + this.name_box.show(); + this.hotkey_box.show(); + this.quickaction_box.show(); + this.icon_button.sensitive = true; + if (this.current_custom_icon == "") this.set_icon(icon); + else this.set_icon(this.current_custom_icon); + break; + case "pie": + this.pie_box.show(); + this.quickaction_box.show(); + this.set_icon(PieManager.all_pies[this.pie_select.current_id].icon); + break; + case "uri": + this.name_box.show(); + this.uri_box.show(); + this.quickaction_box.show(); + this.icon_button.sensitive = true; + if (this.current_custom_icon == "") this.set_icon(icon); + else this.set_icon(this.current_custom_icon); + break; + } + }); + + this.name_box = builder.get_object("name-box") as Gtk.Box; + this.command_box = builder.get_object("command-box") as Gtk.Box; + this.icon_button = builder.get_object("icon-button") as Gtk.Button; + this.no_options_box = builder.get_object("no-options-box") as Gtk.Box; + this.pie_box = builder.get_object("pie-box") as Gtk.Box; + this.pie_select = new PieComboList(); + this.pie_select.on_select.connect((id) => { + this.current_pie_to_open = id; + this.set_icon(PieManager.all_pies[id].icon); + }); + + this.pie_box.pack_start(this.pie_select, true, true); + + this.hotkey_box = builder.get_object("hotkey-box") as Gtk.Box; + this.key_select = new TriggerSelectButton(false); + this.hotkey_box.pack_start(this.key_select, false, true); + this.key_select.on_select.connect((trigger) => { + this.current_hotkey = trigger.name; + }); + + this.uri_box = builder.get_object("uri-box") as Gtk.Box; + + this.name_entry = builder.get_object("name-entry") as Gtk.Entry; + this.uri_entry = builder.get_object("uri-entry") as Gtk.Entry; + this.command_entry = builder.get_object("command-entry") as Gtk.Entry; + this.quickaction_checkbutton = builder.get_object("quick-action-checkbutton") as Gtk.Switch; + this.quickaction_box = builder.get_object("quickaction-box") as Gtk.Box; + this.icon = builder.get_object("icon") as Gtk.Image; + + this.workspace_only_checkbutton = builder.get_object("workspace-only-checkbutton") as Gtk.Switch; + this.workspace_only_box = builder.get_object("workspace-only-box") as Gtk.Box; + + this.clipboard_box = builder.get_object("clipboard-box") as Gtk.Box; + this.clipboard_slider = (builder.get_object("clipboard-scale") as Gtk.Scale); + clipboard_slider.set_range(2, 24); + clipboard_slider.set_value(8); + + this.icon_button.clicked.connect(on_icon_button_clicked); + + var scroll_area = builder.get_object("slice-scrolledwindow") as Gtk.ScrolledWindow; + scroll_area.add(this.slice_type_list); + + this.window = builder.get_object("window") as Gtk.Dialog; + + (builder.get_object("ok-button") as Gtk.Button).clicked.connect(on_ok_button_clicked); + (builder.get_object("cancel-button") as Gtk.Button).clicked.connect(on_cancel_button_clicked); + + this.window.delete_event.connect(this.window.hide_on_delete); + + } catch (GLib.Error e) { + error("Could not load UI: %s\n", e.message); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the parent window, in order to make this window stay in + /// front. + ///////////////////////////////////////////////////////////////////// + + public void set_parent(Gtk.Window parent) { + this.window.set_transient_for(parent); + } + + ///////////////////////////////////////////////////////////////////// + /// Sows the window on the screen. + ///////////////////////////////////////////////////////////////////// + + public void show() { + this.slice_type_list.select_first(); + this.pie_select.select_first(); + this.key_select.set_trigger(new Trigger()); + this.window.show_all(); + } + + ///////////////////////////////////////////////////////////////////// + /// Reloads the window. + ///////////////////////////////////////////////////////////////////// + + public void reload() { + this.pie_select.reload(); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes all widgets display stuff according to the given action. + ///////////////////////////////////////////////////////////////////// + + public void set_action(ActionGroup group, int position) { + this.set_default(group.parent_id, position); + + this.add_as_new_slice = false; + string type = ""; + + if (group.get_type().depth() == 2) { + var action = group.actions[0]; + type = ActionRegistry.descriptions[action.get_type().name()].id; + this.select_type(type); + + this.set_icon(action.icon); + this.quickaction_checkbutton.active = action.is_quickaction; + this.name_entry.text = action.name; + + switch (type) { + case "app": + this.current_custom_icon = action.icon; + this.command_entry.text = action.real_command; + break; + case "key": + this.current_custom_icon = action.icon; + this.current_hotkey = action.real_command; + this.key_select.set_trigger(new Trigger.from_string(action.real_command)); + break; + case "pie": + this.pie_select.select(action.real_command); + break; + case "uri": + this.current_custom_icon = action.icon; + this.uri_entry.text = action.real_command; + break; + } + + } else { + type = GroupRegistry.descriptions[group.get_type().name()].id; + switch (type) { + case "clipboard": + this.clipboard_slider.set_value((group as ClipboardGroup).max_items); + break; + case "window_list": + this.workspace_only_checkbutton.active = (group as WindowListGroup).current_workspace_only; + break; + + } + this.select_type(type); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Selects a default action. + ///////////////////////////////////////////////////////////////////// + + public void set_default(string pie_id, int position) { + this.slice_position = position; + this.add_as_new_slice = true; + this.current_custom_icon = ""; + this.select_type("app"); + this.current_id = pie_id; + this.key_select.set_trigger(new Trigger()); + this.pie_select.select_first(); + this.name_entry.text = _("Rename me!"); + this.command_entry.text = ""; + this.uri_entry.text = ""; + } + + ///////////////////////////////////////////////////////////////////// + /// Selects a specific action type. + ///////////////////////////////////////////////////////////////////// + + private void select_type(string type) { + this.current_type = type; + this.slice_type_list.select(type); + } + + ///////////////////////////////////////////////////////////////////// + /// Called, when the user presses the ok button. + ///////////////////////////////////////////////////////////////////// + + private void on_ok_button_clicked() { + this.window.hide(); + + ActionGroup group = null; + + switch (this.current_type) { + case "bookmarks": group = new BookmarkGroup(this.current_id); break; + case "devices": group = new DevicesGroup(this.current_id); break; + case "menu": group = new MenuGroup(this.current_id); break; + case "session": group = new SessionGroup(this.current_id); break; + case "clipboard": + var g = new ClipboardGroup(this.current_id); + g.max_items = (int)this.clipboard_slider.get_value(); + group = g; + break; + case "window_list": + var g = new WindowListGroup(this.current_id); + g.current_workspace_only = this.workspace_only_checkbutton.active; + group = g; + break; + case "app": + group = new ActionGroup(this.current_id); + group.add_action(new AppAction(this.name_entry.text, this.current_icon, + this.command_entry.text, + this.quickaction_checkbutton.active)); + break; + case "key": + group = new ActionGroup(this.current_id); + group.add_action(new KeyAction(this.name_entry.text, this.current_icon, + this.current_hotkey, + this.quickaction_checkbutton.active)); + break; + case "pie": + group = new ActionGroup(this.current_id); + group.add_action(new PieAction(this.current_pie_to_open, + this.quickaction_checkbutton.active)); + break; + case "uri": + group = new ActionGroup(this.current_id); + group.add_action(new UriAction(this.name_entry.text, this.current_icon, + this.uri_entry.text, + this.quickaction_checkbutton.active)); + break; + } + + this.on_select(group, this.add_as_new_slice, this.slice_position); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user presses the cancel button. + ///////////////////////////////////////////////////////////////////// + + private void on_cancel_button_clicked() { + this.window.hide(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user presses the icon select button. + ///////////////////////////////////////////////////////////////////// + + private void on_icon_button_clicked(Gtk.Button button) { + if (this.icon_window == null) { + this.icon_window = new IconSelectWindow(this.window); + this.icon_window.on_ok.connect((icon) => { + this.current_custom_icon = icon; + this.set_icon(icon); + }); + } + + this.icon_window.show(); + this.icon_window.set_icon(this.current_icon); + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method which sets the icon of the icon select button. + /// It assures that both can be displayed: A customly chosen image + /// from or an icon from the current theme. + ///////////////////////////////////////////////////////////////////// + + private void set_icon(string icon) { + if (icon.contains("/")) + try { + this.icon.pixbuf = new Gdk.Pixbuf.from_file_at_scale(icon, this.icon.get_pixel_size(), + this.icon.get_pixel_size(), true); + } catch (GLib.Error error) { + warning(error.message); + } + else + this.icon.icon_name = icon; + + this.current_icon = icon; + } +} + +} diff --git a/src/gui/newsWindow.vala b/src/gui/newsWindow.vala new file mode 100644 index 0000000..cc1a77d --- /dev/null +++ b/src/gui/newsWindow.vala @@ -0,0 +1,73 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// +///////////////////////////////////////////////////////////////////////// + +public class NewsWindow: Gtk.Dialog { + + public static const int news_count = 2; + + ///////////////////////////////////////////////////////////////////// + /// + ///////////////////////////////////////////////////////////////////// + + public NewsWindow () { + this.title = "Gnome-Pie"; + + this.set_border_width(5); + + var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 12); + + var image = new Gtk.Image.from_icon_name("gnome-pie", Gtk.IconSize.DIALOG); + box.pack_start(image); + + var news = new Gtk.Label(""); + news.wrap = true; + news.set_width_chars(75); + news.set_markup("<b>Thank you!</b>\n\n"); + + box.pack_start(news, false, false); + + var check = new Gtk.CheckButton.with_label("Don't show this window again."); + check.toggled.connect((check_box) => { + var checky = check_box as Gtk.CheckButton; + + if (checky.active) Config.global.showed_news = news_count; + else Config.global.showed_news = news_count-1; + + Config.global.save(); + }); + + box.pack_end(check); + + (this.get_content_area() as Gtk.VBox).pack_start(box); + this.get_content_area().show_all(); + + this.add_button(_("_Close"), 0); + + this.response.connect((id) => { + if (id == 0) + this.hide(); + }); + } +} + +} diff --git a/src/gui/pieComboList.vala b/src/gui/pieComboList.vala new file mode 100644 index 0000000..f0fd22f --- /dev/null +++ b/src/gui/pieComboList.vala @@ -0,0 +1,155 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A drop-down list, containing one entry for each existing Pie. +///////////////////////////////////////////////////////////////////////// + +class PieComboList : Gtk.ComboBox { + + ///////////////////////////////////////////////////////////////////// + /// This signal gets emitted when the user selects a new Pie. + ///////////////////////////////////////////////////////////////////// + + public signal void on_select(string id); + + ///////////////////////////////////////////////////////////////////// + /// The currently selected row. + ///////////////////////////////////////////////////////////////////// + + public string current_id { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// Stores the data internally. + ///////////////////////////////////////////////////////////////////// + + private Gtk.ListStore data; + private enum DataPos {ICON, NAME, ID} + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs the Widget. + ///////////////////////////////////////////////////////////////////// + + public PieComboList() { + GLib.Object(); + + this.data = new Gtk.ListStore(3, typeof(Gdk.Pixbuf), + typeof(string), + typeof(string)); + + this.data.set_sort_column_id(1, Gtk.SortType.ASCENDING); + + base.set_model(this.data); + + var icon_render = new Gtk.CellRendererPixbuf(); + icon_render.xpad = 4; + this.pack_start(icon_render, false); + + var name_render = new Gtk.CellRendererText(); + this.pack_start(name_render, true); + + this.add_attribute(icon_render, "pixbuf", DataPos.ICON); + this.add_attribute(name_render, "text", DataPos.NAME); + + this.changed.connect(() => { + Gtk.TreeIter active; + if (this.get_active_iter(out active)) { + string id = ""; + this.data.get(active, DataPos.ID, out id); + this.on_select(id); + this.current_id = id; + } + }); + + reload(); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all existing Pies to the list. + ///////////////////////////////////////////////////////////////////// + + public void reload() { + Gtk.TreeIter active; + string id = ""; + if (this.get_active_iter(out active)) + this.data.get(active, DataPos.ID, out id); + + data.clear(); + foreach (var pie in PieManager.all_pies.entries) { + this.load_pie(pie.value); + } + + select_first(); + select(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Selects the first Pie. + ///////////////////////////////////////////////////////////////////// + + public void select_first() { + Gtk.TreeIter active; + + if(this.data.get_iter_first(out active) ) { + this.set_active_iter(active); + string id = ""; + this.data.get(active, DataPos.ID, out id); + this.on_select(id); + this.current_id = id; + } else { + this.on_select(""); + this.current_id = ""; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Selects the Pie with the given ID. + ///////////////////////////////////////////////////////////////////// + + public void select(string id) { + this.data.foreach((model, path, iter) => { + string pie_id; + this.data.get(iter, DataPos.ID, out pie_id); + + if (id == pie_id) { + this.set_active_iter(iter); + return true; + } + + return false; + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads one given pie to the list. + ///////////////////////////////////////////////////////////////////// + + private void load_pie(Pie pie) { + if (pie.id.length == 3) { + Gtk.TreeIter last; + this.data.append(out last); + var icon = new Icon(pie.icon, 24); + this.data.set(last, DataPos.ICON, icon.to_pixbuf(), + DataPos.NAME, pie.name, + DataPos.ID, pie.id); + } + } +} + +} diff --git a/src/gui/pieList.vala b/src/gui/pieList.vala new file mode 100644 index 0000000..77f833b --- /dev/null +++ b/src/gui/pieList.vala @@ -0,0 +1,275 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A list, containing one entry for each existing Pie. +///////////////////////////////////////////////////////////////////////// + +class PieList : Gtk.TreeView { + + ///////////////////////////////////////////////////////////////////// + /// This signal gets emitted when the user selects a new Pie. + ///////////////////////////////////////////////////////////////////// + + public signal void on_select(string id); + public signal void on_activate(); + + ///////////////////////////////////////////////////////////////////// + /// Stores the data internally. + ///////////////////////////////////////////////////////////////////// + + private Gtk.ListStore data; + private enum DataPos {ICON, ICON_NAME, NAME, ID} + + ///////////////////////////////////////////////////////////////////// + /// Stores where a drag startet. + ///////////////////////////////////////////////////////////////////// + + private Gtk.TreeIter? drag_start = null; + + ///////////////////////////////////////////////////////////////////// + /// Rembers the time when a last drag move event was reported. Used + /// to avoid frequent changes of selected Pie when a Pie is dragged + /// over this widget. + ///////////////////////////////////////////////////////////////////// + + private uint last_hover = 0; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs the Widget. + ///////////////////////////////////////////////////////////////////// + + public PieList() { + GLib.Object(); + + this.data = new Gtk.ListStore(4, typeof(Gdk.Pixbuf), + typeof(string), + typeof(string), + typeof(string)); + + this.data.set_sort_column_id(DataPos.NAME, Gtk.SortType.ASCENDING); + + this.set_model(this.data); + this.set_headers_visible(true); + this.set_grid_lines(Gtk.TreeViewGridLines.NONE); + this.width_request = 170; + this.set_enable_search(false); + + this.set_events(Gdk.EventMask.POINTER_MOTION_MASK); + + var main_column = new Gtk.TreeViewColumn(); + main_column.title = _("Pies"); + var icon_render = new Gtk.CellRendererPixbuf(); + icon_render.xpad = 4; + icon_render.ypad = 4; + main_column.pack_start(icon_render, false); + + var name_render = new Gtk.CellRendererText(); + name_render.xpad = 6; + name_render.ellipsize = Pango.EllipsizeMode.END; + name_render.ellipsize_set = true; + main_column.pack_start(name_render, true); + + base.append_column(main_column); + + main_column.add_attribute(icon_render, "pixbuf", DataPos.ICON); + main_column.add_attribute(name_render, "markup", DataPos.NAME); + + // setup drag'n'drop + Gtk.TargetEntry uri_source = {"text/uri-list", 0, 0}; + Gtk.TargetEntry[] entries = { uri_source }; + this.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, entries, Gdk.DragAction.LINK); + this.enable_model_drag_dest(entries, Gdk.DragAction.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.LINK); + this.drag_data_get.connect(this.on_dnd_source); + this.drag_data_received.connect(this.on_dnd_received); + this.drag_begin.connect_after(this.on_start_drag); + this.drag_motion.connect(this.on_drag_move); + this.drag_leave.connect(() => { + this.last_hover = 0; + }); + + this.row_activated.connect(() => { + this.on_activate(); + }); + + this.get_selection().changed.connect(() => { + Gtk.TreeIter active; + if (this.get_selection().get_selected(null, out active)) { + string id = ""; + this.data.get(active, DataPos.ID, out id); + this.on_select(id); + } + }); + + reload_all(); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all existing Pies to the list. + ///////////////////////////////////////////////////////////////////// + + public void reload_all() { + Gtk.TreeIter active; + string id = ""; + if (this.get_selection().get_selected(null, out active)) + this.data.get(active, DataPos.ID, out id); + + data.clear(); + foreach (var pie in PieManager.all_pies.entries) { + this.load_pie(pie.value); + } + + select(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Selects the first Pie. + ///////////////////////////////////////////////////////////////////// + + public void select_first() { + Gtk.TreeIter active; + + if(this.data.get_iter_first(out active) ) { + this.get_selection().select_iter(active); + string id = ""; + this.data.get(active, DataPos.ID, out id); + this.on_select(id); + } else { + this.on_select(""); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Selects the Pie with the given ID. + ///////////////////////////////////////////////////////////////////// + + public void select(string id) { + this.data.foreach((model, path, iter) => { + string pie_id; + this.data.get(iter, DataPos.ID, out pie_id); + + if (id == pie_id) { + this.get_selection().select_iter(iter); + return true; + } + + return false; + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads one given pie to the list. + ///////////////////////////////////////////////////////////////////// + + private void load_pie(Pie pie) { + if (pie.id.length == 3) { + Gtk.TreeIter last; + this.data.append(out last); + var icon = new Icon(pie.icon, 24); + this.data.set(last, DataPos.ICON, icon.to_pixbuf(), + DataPos.ICON_NAME, pie.icon, + DataPos.NAME,GLib.Markup.escape_text(pie.name) + "\n" + + "<span font-size='x-small'>" + PieManager.get_accelerator_label_of(pie.id) + "</span>", + DataPos.ID, pie.id); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a drag which started on this Widget was successfull. + ///////////////////////////////////////////////////////////////////// + + private void on_dnd_source(Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time_) { + if (this.drag_start != null) { + string id = ""; + this.data.get(this.drag_start, DataPos.ID, out id); + selection_data.set_uris({"file://" + Paths.launchers + "/" + id + ".desktop"}); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a drag operation is started on this Widget. + ///////////////////////////////////////////////////////////////////// + + private void on_start_drag(Gdk.DragContext ctx) { + if (this.get_selection().get_selected(null, out this.drag_start)) { + string icon_name = ""; + this.data.get(this.drag_start, DataPos.ICON_NAME, out icon_name); + + var icon = new Icon(icon_name, 48); + var pixbuf = icon.to_pixbuf(); + Gtk.drag_set_icon_pixbuf(ctx, pixbuf, icon.size()/2, icon.size()/2); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when something is dragged over this Widget. + ///////////////////////////////////////////////////////////////////// + + private bool on_drag_move(Gdk.DragContext context, int x, int y, uint time) { + + Gtk.TreeViewDropPosition position; + Gtk.TreePath path; + + if (!this.get_dest_row_at_pos(x, y, out path, out position)) + return false; + + if (position == Gtk.TreeViewDropPosition.BEFORE) + this.set_drag_dest_row(path, Gtk.TreeViewDropPosition.INTO_OR_BEFORE); + else if (position == Gtk.TreeViewDropPosition.AFTER) + this.set_drag_dest_row(path, Gtk.TreeViewDropPosition.INTO_OR_AFTER); + + Gdk.drag_status(context, context.get_suggested_action(), time); + + // avoid too frequent selection... + this.last_hover = time; + + GLib.Timeout.add(150, () => { + if (this.last_hover == time) + this.get_selection().select_path(path); + return false; + }); + + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user finishes a drag operation on this widget. + /// Only used for external drags. + ///////////////////////////////////////////////////////////////////// + + private void on_dnd_received(Gdk.DragContext context, int x, int y, + Gtk.SelectionData selection_data, uint info, uint time_) { + + Gtk.TreeIter active; + if (this.get_selection().get_selected(null, out active)) { + string id = ""; + this.data.get(active, DataPos.ID, out id); + + var pie = PieManager.all_pies[id]; + + foreach (var uri in selection_data.get_uris()) { + pie.add_action(ActionRegistry.new_for_uri(uri), 0); + } + + this.on_select(id); + } + } +} + +} diff --git a/src/gui/pieOptionsWindow.vala b/src/gui/pieOptionsWindow.vala new file mode 100644 index 0000000..2f9cadf --- /dev/null +++ b/src/gui/pieOptionsWindow.vala @@ -0,0 +1,315 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This window allows the selection of a hotkey. It is returned in form +/// of a Trigger. Therefore it can be either a keyboard driven hotkey or +/// a mouse based hotkey. +///////////////////////////////////////////////////////////////////////// + +public class PieOptionsWindow : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// This signal is emitted when the user selects a new hot key. + ///////////////////////////////////////////////////////////////////// + + public signal void on_ok(Trigger trigger, string pie_name, string icon_name); + + ///////////////////////////////////////////////////////////////////// + /// Some private members which are needed by other methods. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Dialog window; + private Gtk.CheckButton turbo; + private Gtk.CheckButton delayed; + private Gtk.CheckButton centered; + private Gtk.CheckButton warp; + private Gtk.RadioButton rshape[10]; + private TriggerSelectButton trigger_button; + private Gtk.Entry name_entry = null; + private Gtk.Button? icon_button = null; + private Gtk.Image? icon = null; + private Gtk.Label? pie_id = null; + + private IconSelectWindow? icon_window = null; + + ///////////////////////////////////////////////////////////////////// + /// The currently configured trigger. + ///////////////////////////////////////////////////////////////////// + + private Trigger trigger = null; + + ///////////////////////////////////////////////////////////////////// + /// The trigger which was active when this window was opened. It is + /// stored in order to check whether anything has changed when the + /// user clicks on OK. + ///////////////////////////////////////////////////////////////////// + + private Trigger original_trigger = null; + + ///////////////////////////////////////////////////////////////////// + /// Stores the current icon name of the pie. + ///////////////////////////////////////////////////////////////////// + + private string icon_name = ""; + + ///////////////////////////////////////////////////////////////////// + /// Stores the id of the current pie. + ///////////////////////////////////////////////////////////////////// + + private string id = ""; + + ///////////////////////////////////////////////////////////////////// + /// Radioboxes call toggled() twice per selection change. + /// This flag is used to discard one of the two notifications. + ///////////////////////////////////////////////////////////////////// + + private static int notify_toggle = 0; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs a new PieOptionsWindow. + ///////////////////////////////////////////////////////////////////// + + public PieOptionsWindow() { + try { + + Gtk.Builder builder = new Gtk.Builder(); + + builder.add_from_file (Paths.ui_files + "/pie_options.ui"); + + this.window = builder.get_object("window") as Gtk.Dialog; + this.trigger_button = new TriggerSelectButton(true); + this.trigger_button.show(); + + this.trigger_button.on_select.connect((trigger) => { + this.trigger = new Trigger.from_values( + trigger.key_sym, + trigger.modifiers, + trigger.with_mouse, + this.turbo.active, + this.delayed.active, + this.centered.active, + this.warp.active, + this.get_radio_shape() + ); + }); + + (builder.get_object("trigger-box") as Gtk.Box).pack_start(this.trigger_button, true, true); + + (builder.get_object("ok-button") as Gtk.Button).clicked.connect(this.on_ok_button_clicked); + (builder.get_object("cancel-button") as Gtk.Button).clicked.connect(this.on_cancel_button_clicked); + + this.turbo = builder.get_object("turbo-check") as Gtk.CheckButton; + this.turbo.toggled.connect(this.on_check_toggled); + + this.delayed = builder.get_object("delay-check") as Gtk.CheckButton; + this.delayed.toggled.connect(this.on_check_toggled); + + this.centered = builder.get_object("center-check") as Gtk.CheckButton; + this.centered.toggled.connect(this.on_check_toggled); + + this.warp = builder.get_object("warp-check") as Gtk.CheckButton; + this.warp.toggled.connect(this.on_check_toggled); + + for (int i= 0; i < 10; i++) { + this.rshape[i] = builder.get_object("rshape%d".printf(i)) as Gtk.RadioButton; + this.rshape[i].toggled.connect(this.on_radio_toggled); + } + + this.name_entry = builder.get_object("name-entry") as Gtk.Entry; + this.name_entry.activate.connect(this.on_ok_button_clicked); + + this.pie_id = builder.get_object("pie-id") as Gtk.Label; + + this.icon = builder.get_object("icon") as Gtk.Image; + this.icon_button = builder.get_object("icon-button") as Gtk.Button; + this.icon_button.clicked.connect(on_icon_button_clicked); + + this.window.delete_event.connect(this.window.hide_on_delete); + + } catch (GLib.Error e) { + error("Could not load UI: %s\n", e.message); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the parent window, in order to make this window stay in + /// front. + ///////////////////////////////////////////////////////////////////// + + public void set_parent(Gtk.Window parent) { + this.window.set_transient_for(parent); + } + + ///////////////////////////////////////////////////////////////////// + /// Displays the window on the screen. + ///////////////////////////////////////////////////////////////////// + + public void show() { + this.window.show_all(); + } + + ///////////////////////////////////////////////////////////////////// + /// Initilizes all members to match the Trigger of the Pie with the + /// given ID. + ///////////////////////////////////////////////////////////////////// + + public void set_pie(string id) { + var trigger = new Trigger.from_string(PieManager.get_accelerator_of(id)); + var pie = PieManager.all_pies[id]; + + this.id = id; + + this.turbo.active = trigger.turbo; + this.delayed.active = trigger.delayed; + this.centered.active = trigger.centered; + this.warp.active = trigger.warp; + this.set_radio_shape( trigger.shape ); + this.original_trigger = trigger; + this.trigger = trigger; + this.name_entry.text = PieManager.get_name_of(id); + this.pie_id.label = "Pie-ID: " + id; + this.trigger_button.set_trigger(trigger); + this.set_icon(pie.icon); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when one of the checkboxes is toggled. + ///////////////////////////////////////////////////////////////////// + + private void on_check_toggled() { + if (this.trigger != null) { + this.trigger = new Trigger.from_values( + this.trigger.key_sym, this.trigger.modifiers, + this.trigger.with_mouse, this.turbo.active, + this.delayed.active, this.centered.active, + this.warp.active, + this.get_radio_shape() + ); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the current selected radio-button shape: 0= automatic + /// 5= full pie; 1,3,7,8= quarters; 2,4,6,8=halves + /// 1 | 4 | 7 + /// 2 | 5 | 8 + /// 3 | 6 | 9 + ///////////////////////////////////////////////////////////////////// + + private int get_radio_shape() { + int rs; + for (rs= 0; rs < 10; rs++) { + if (this.rshape[rs].active) { + break; + } + } + return rs; + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the current selected radio-button shape: 0= automatic + /// 5= full pie; 1,3,7,8= quarters; 2,4,6,8=halves + ///////////////////////////////////////////////////////////////////// + + private void set_radio_shape(int rs) { + if (rs < 0 || rs > 9) { + rs= 5; //replace invalid value with default= full pie + } + this.rshape[rs].active= true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called twice when one of the radioboxes is toggled. + ///////////////////////////////////////////////////////////////////// + + private void on_radio_toggled() { + notify_toggle= 1 - notify_toggle; + if (notify_toggle == 1) { + on_check_toggled(); //just call once + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the icon button is clicked. + ///////////////////////////////////////////////////////////////////// + + private void on_icon_button_clicked(Gtk.Button button) { + if (this.icon_window == null) { + this.icon_window = new IconSelectWindow(this.window); + this.icon_window.on_ok.connect((icon) => { + set_icon(icon); + }); + } + + this.icon_window.show(); + this.icon_window.set_icon(this.icon_name); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the OK-button is pressed. + ///////////////////////////////////////////////////////////////////// + + private void on_ok_button_clicked() { + var assigned_id = PieManager.get_assigned_id(this.trigger); + + if (assigned_id != "" && assigned_id != this.id) { + // it's already assigned + var error = _("This hotkey is already assigned to the pie \"%s\"! \n\nPlease select " + + "another one or cancel your selection.").printf(PieManager.get_name_of(assigned_id)); + var dialog = new Gtk.MessageDialog((Gtk.Window)this.window.get_toplevel(), Gtk.DialogFlags.MODAL, + Gtk.MessageType.ERROR, Gtk.ButtonsType.CANCEL, error); + dialog.run(); + dialog.destroy(); + } else { + // a unused hot key has been chosen, great! + this.on_ok(this.trigger, this.name_entry.text, this.icon_name); + this.window.hide(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the icon of the icon_button + ///////////////////////////////////////////////////////////////////// + + private void set_icon(string name) { + this.icon_name = name; + + if (name.contains("/")) { + try { + this.icon.pixbuf = new Gdk.Pixbuf.from_file_at_scale(name, + this.icon.get_pixel_size(), this.icon.get_pixel_size(), true); + } catch (GLib.Error error) { + warning(error.message); + } + } else { + this.icon.icon_name = name; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the cancel button is pressed. + ///////////////////////////////////////////////////////////////////// + + private void on_cancel_button_clicked() { + this.window.hide(); + } +} + +} diff --git a/src/gui/piePreview.vala b/src/gui/piePreview.vala new file mode 100644 index 0000000..ce1ba96 --- /dev/null +++ b/src/gui/piePreview.vala @@ -0,0 +1,387 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A custom widget displaying the preview of a Pie. It can be used to +/// configure the displayed Pie in various aspects. +///////////////////////////////////////////////////////////////////////// + +class PiePreview : Gtk.DrawingArea { + + ///////////////////////////////////////////////////////////////////// + /// These get called when the last Slice is removed and when the + /// first Slice is added respectively. + ///////////////////////////////////////////////////////////////////// + + public signal void on_last_slice_removed(); + public signal void on_first_slice_added(); + + ///////////////////////////////////////////////////////////////////// + /// The internally used renderer to draw the Pie. + ///////////////////////////////////////////////////////////////////// + + private PiePreviewRenderer renderer = null; + + ///////////////////////////////////////////////////////////////////// + /// The window which pops up, when a Slice is added or edited. + ///////////////////////////////////////////////////////////////////// + + private NewSliceWindow? new_slice_window = null; + + ///////////////////////////////////////////////////////////////////// + /// A timer used for calculating the frame time. + ///////////////////////////////////////////////////////////////////// + + private GLib.Timer timer; + + ///////////////////////////////////////////////////////////////////// + /// True, when it is possible to drag a slice from this widget. + /// False, when the user currently hovers over the add sign. + ///////////////////////////////////////////////////////////////////// + + private bool drag_enabled = false; + + ///////////////////////////////////////////////////////////////////// + /// The ID of the currently displayed Pie. + ///////////////////////////////////////////////////////////////////// + + private string current_id = ""; + + ///////////////////////////////////////////////////////////////////// + /// The position from where a Slice-drag started. + ///////////////////////////////////////////////////////////////////// + + private int drag_start_index = -1; + private string drag_start_id = ""; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates the widget. + ///////////////////////////////////////////////////////////////////// + + public PiePreview() { + this.renderer = new PiePreviewRenderer(this); + + this.draw.connect(this.on_draw); + this.timer = new GLib.Timer(); + this.set_events(Gdk.EventMask.POINTER_MOTION_MASK + | Gdk.EventMask.LEAVE_NOTIFY_MASK + | Gdk.EventMask.ENTER_NOTIFY_MASK); + + // setup drag and drop + this.enable_drag_source(); + + Gtk.TargetEntry uri_dest = {"text/uri-list", 0, 0}; + Gtk.TargetEntry slice_dest = {"text/plain", Gtk.TargetFlags.SAME_WIDGET, 0}; + Gtk.TargetEntry[] destinations = { uri_dest, slice_dest }; + Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, destinations, Gdk.DragAction.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.LINK); + + this.drag_begin.connect(this.on_start_drag); + this.drag_end.connect(this.on_end_drag); + this.drag_data_received.connect(this.on_dnd_received); + + // connect mouse events + this.drag_motion.connect(this.on_drag_move); + this.leave_notify_event.connect(this.on_mouse_leave); + this.enter_notify_event.connect(this.on_mouse_enter); + this.motion_notify_event.connect_after(this.on_mouse_move); + this.button_release_event.connect_after(this.on_button_release); + this.button_press_event.connect_after(this.on_button_press); + + this.new_slice_window = new NewSliceWindow(); + this.new_slice_window.on_select.connect((new_action, as_new_slice, at_position) => { + var pie = PieManager.all_pies[this.current_id]; + + if (new_action.has_quickaction()) + renderer.disable_quickactions(); + + if (as_new_slice) { + pie.add_group(new_action, at_position+1); + this.renderer.add_group(new_action, at_position+1); + + if (this.renderer.slice_count() == 1) + this.on_first_slice_added(); + } else { + pie.update_group(new_action, at_position); + this.renderer.update_group(new_action, at_position); + } + }); + + this.renderer.on_edit_slice.connect((pos) => { + this.new_slice_window.reload(); + + this.new_slice_window.set_parent(this.get_toplevel() as Gtk.Window); + this.new_slice_window.show(); + + var pie = PieManager.all_pies[this.current_id]; + this.new_slice_window.set_action(pie.action_groups[pos], pos); + }); + + this.renderer.on_add_slice.connect((pos) => { + this.new_slice_window.reload(); + + this.new_slice_window.set_parent(this.get_toplevel() as Gtk.Window); + this.new_slice_window.show(); + + this.new_slice_window.set_default(this.current_id, pos); + }); + + this.renderer.on_remove_slice.connect((pos) => { + + var dialog = new Gtk.MessageDialog(this.get_toplevel() as Gtk.Window, Gtk.DialogFlags.MODAL, + Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, + _("Do you really want to delete this Slice?")); + + dialog.response.connect((response) => { + if (response == Gtk.ResponseType.YES) { + var pie = PieManager.all_pies[this.current_id]; + + pie.remove_group(pos); + this.renderer.remove_group(pos); + + if (this.renderer.slice_count() == 0) + this.on_last_slice_removed(); + } + }); + + dialog.run(); + dialog.destroy(); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the currently displayed Pie to the Pie with the given ID. + ///////////////////////////////////////////////////////////////////// + + public void set_pie(string id) { + var style = this.get_style_context(); + + this.current_id = id; + this.override_background_color(Gtk.StateFlags.NORMAL, style.get_background_color(Gtk.StateFlags.NORMAL)); + this.renderer.load_pie(PieManager.all_pies[id]); + + if (id == this.drag_start_id) { + this.renderer.hide_group(this.drag_start_index); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Begins the draw loop. It automatically ends, when the containing + /// window becomes invisible. + ///////////////////////////////////////////////////////////////////// + + public void draw_loop() { + this.timer.start(); + this.queue_draw(); + + GLib.Timeout.add((uint)(1000.0/Config.global.refresh_rate), () => { + this.queue_draw(); + return this.get_toplevel().visible; + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Called every frame. + ///////////////////////////////////////////////////////////////////// + + private bool on_draw(Cairo.Context ctx) { + // store the frame time + double frame_time = this.timer.elapsed(); + this.timer.reset(); + + Gtk.Allocation allocation; + this.get_allocation(out allocation); + + ctx.translate((int)(allocation.width*0.5), (int)(allocation.height*0.5)); + + this.renderer.draw(frame_time, ctx); + + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse leaves the area of this widget. + ///////////////////////////////////////////////////////////////////// + + public bool on_mouse_leave(Gdk.EventCrossing event) { + this.renderer.on_mouse_leave(); + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse enters the area of this widget. + ///////////////////////////////////////////////////////////////////// + + public bool on_mouse_enter(Gdk.EventCrossing event) { + this.renderer.on_mouse_enter(); + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse moves in the area of this widget. + ///////////////////////////////////////////////////////////////////// + + private bool on_mouse_move(Gdk.EventMotion event) { + this.renderer.set_dnd_mode(false); + Gtk.Allocation allocation; + this.get_allocation(out allocation); + this.renderer.on_mouse_move(event.x-allocation.width*0.5, event.y-allocation.height*0.5); + + if (this.renderer.get_active_slice() < 0) this.disable_drag_source(); + else this.enable_drag_source(); + + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a mouse button is pressed. + ///////////////////////////////////////////////////////////////////// + + private bool on_button_press() { + this.renderer.on_button_press(); + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a mouse button is released. + ///////////////////////////////////////////////////////////////////// + + private bool on_button_release() { + if (!this.renderer.drag_n_drop_mode) + this.renderer.on_button_release(); + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse is moved over this widget. + ///////////////////////////////////////////////////////////////////// + + private bool on_drag_move(Gdk.DragContext ctx, int x, int y, uint time) { + this.renderer.set_dnd_mode(true); + Gtk.Allocation allocation; + this.get_allocation(out allocation); + this.renderer.on_mouse_move(x-allocation.width*0.5, y-allocation.height*0.5); + + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user tries to drag something from this widget. + ///////////////////////////////////////////////////////////////////// + + private void on_start_drag(Gdk.DragContext ctx) { + this.drag_start_index = this.renderer.get_active_slice(); + this.drag_start_id = this.current_id; + var icon = this.renderer.get_active_icon(); + var pixbuf = icon.to_pixbuf(); + + this.renderer.hide_group(this.drag_start_index); + Gtk.drag_set_icon_pixbuf(ctx, pixbuf, icon.size()/2, icon.size()/2); + + this.renderer.set_dnd_mode(true); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user finishes a drag operation on this widget. + /// Only used for Slice-movement. + ///////////////////////////////////////////////////////////////////// + + private void on_end_drag(Gdk.DragContext context) { + + if (context.list_targets() != null) { + + int target_index = this.renderer.get_active_slice(); + this.renderer.set_dnd_mode(false); + + context.list_targets().foreach((target) => { + Gdk.Atom target_type = (Gdk.Atom)target; + if (target_type.name() == "text/plain") { + if (this.current_id == this.drag_start_id) { + var pie = PieManager.all_pies[this.current_id]; + pie.move_group(this.drag_start_index, target_index); + this.renderer.show_hidden_group_at(target_index); + } else { + var src_pie = PieManager.all_pies[this.drag_start_id]; + var dst_pie = PieManager.all_pies[this.current_id]; + dst_pie.add_group(src_pie.action_groups[this.drag_start_index], target_index); + this.renderer.add_group(dst_pie.action_groups[target_index], target_index); + + if (this.renderer.slices.size == 1) + this.on_first_slice_added(); + + if ((context.get_actions() & Gdk.DragAction.COPY) == 0) + src_pie.remove_group(this.drag_start_index); + } + + + } + }); + + this.drag_start_index = -1; + this.drag_start_id = ""; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user finishes a drag operation on this widget. + /// Only used for external drags. + ///////////////////////////////////////////////////////////////////// + + private void on_dnd_received(Gdk.DragContext context, int x, int y, + Gtk.SelectionData selection_data, uint info, uint time_) { + + var pie = PieManager.all_pies[this.current_id]; + int position = this.renderer.get_active_slice(); + this.renderer.set_dnd_mode(false); + + foreach (var uri in selection_data.get_uris()) { + pie.add_action(ActionRegistry.new_for_uri(uri), position); + this.renderer.add_group(pie.action_groups[position], position); + + if (this.renderer.slices.size == 1) + this.on_first_slice_added(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Enables this widget to be a source for drag operations. + ///////////////////////////////////////////////////////////////////// + + private void enable_drag_source() { + if (!this.drag_enabled) { + this.drag_enabled = true; + Gtk.TargetEntry slice_source = {"text/plain", Gtk.TargetFlags.SAME_WIDGET | Gtk.TargetFlags.SAME_APP, 0}; + Gtk.TargetEntry[] sources = { slice_source }; + Gtk.drag_source_set(this, Gdk.ModifierType.BUTTON1_MASK, sources, Gdk.DragAction.MOVE | Gdk.DragAction.COPY); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Disables this widget to be a source for drag operations. + ///////////////////////////////////////////////////////////////////// + + private void disable_drag_source() { + if (this.drag_enabled) { + this.drag_enabled = false; + Gtk.drag_source_unset(this); + } + } + +} + +} diff --git a/src/gui/piePreviewAddSign.vala b/src/gui/piePreviewAddSign.vala new file mode 100644 index 0000000..b3f6f7b --- /dev/null +++ b/src/gui/piePreviewAddSign.vala @@ -0,0 +1,224 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A liitle plus-sign displayed on the preview widget to indicate where +/// the user may add a new Slice. +///////////////////////////////////////////////////////////////////////// + +public class PiePreviewAddSign : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Gets emitted, when the users clicks on this object. + ///////////////////////////////////////////////////////////////////// + + public signal void on_clicked(int position); + + ///////////////////////////////////////////////////////////////////// + /// The image used to display this oject. + ///////////////////////////////////////////////////////////////////// + + public Image icon { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// True, when the add sign is currently visible. + ///////////////////////////////////////////////////////////////////// + + public bool visible { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// The position of the sign in its parent Pie. May be 2.5 for + /// example. + ///////////////////////////////////////////////////////////////////// + + private double position = 0; + + ///////////////////////////////////////////////////////////////////// + /// The parent renderer. + ///////////////////////////////////////////////////////////////////// + + private unowned PiePreviewRenderer parent; + + ///////////////////////////////////////////////////////////////////// + /// Some values used for displaying this sign. + ///////////////////////////////////////////////////////////////////// + + private double time = 0; + private double max_size = 0; + private double angle = 0; + private AnimatedValue size; + private AnimatedValue alpha; + private AnimatedValue activity; + private AnimatedValue clicked; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, sets everything up. + ///////////////////////////////////////////////////////////////////// + + public PiePreviewAddSign(PiePreviewRenderer parent) { + this.parent = parent; + + this.size = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 2.0); + this.alpha = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 0.0); + this.activity = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, -3, -3, 0, 0.0); + this.clicked = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 1, 1, 0, 0.0); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads the desired icon for this sign. + ///////////////////////////////////////////////////////////////////// + + public void load() { + this.icon = new Icon("list-add", 36); + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the position where this object should be displayed. + ///////////////////////////////////////////////////////////////////// + + public void set_position(int position) { + double new_position = position; + + if (!this.parent.drag_n_drop_mode) + new_position += 0.5; + + this.position = new_position; + this.angle = 2.0 * PI * new_position/parent.slice_count(); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes this object visible. + ///////////////////////////////////////////////////////////////////// + + public void show() { + this.visible = true; + this.size.reset_target(this.max_size, 0.3); + this.alpha.reset_target(1.0, 0.3); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes this object invisible. + ///////////////////////////////////////////////////////////////////// + + public void hide() { + this.visible = false; + this.size.reset_target(0.0, 0.3); + this.alpha.reset_target(0.0, 0.3); + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the size of this object. All transitions will be smooth. + ///////////////////////////////////////////////////////////////////// + + public void set_size(double size) { + this.max_size = size; + this.size.reset_target(size, 0.5); + } + + ///////////////////////////////////////////////////////////////////// + /// Draws the sign to the given context. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx) { + + this.time += frame_time; + + this.size.update(frame_time); + this.alpha.update(frame_time); + this.activity.update(frame_time); + this.clicked.update(frame_time); + + if (this.parent.slice_count() == 0) { + ctx.save(); + + double scale = this.clicked.val + + GLib.Math.sin(this.time*10)*0.02*this.alpha.val + + this.alpha.val*0.08 - 0.1; + ctx.scale(scale, scale); + + // paint the image + icon.paint_on(ctx); + + ctx.restore(); + + } else if (this.alpha.val*this.activity.val > 0) { + ctx.save(); + + // distance from the center + double radius = 120; + + // transform the context + ctx.translate(cos(this.angle)*radius, sin(this.angle)*radius); + double scale = this.size.val*this.clicked.val + + this.activity.val*0.07 + + GLib.Math.sin(this.time*10)*0.03*this.activity.val + - 0.1; + ctx.scale(scale, scale); + + // paint the image + icon.paint_on(ctx, this.alpha.val*this.activity.val); + + ctx.restore(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse moves to another position. + ///////////////////////////////////////////////////////////////////// + + public void on_mouse_move(double angle) { + if (parent.slice_count() > 0) { + double direction = 2.0 * PI * position/parent.slice_count(); + double diff = fabs(angle-direction); + + if (diff > PI) + diff = 2 * PI - diff; + + if (diff < 0.5*PI/parent.slice_count()) this.activity.reset_target(1.0, 1.0); + else this.activity.reset_target(-3.0, 1.5); + } else { + this.activity.reset_target(1.0, 1.0); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a button of the mouse is pressed. + ///////////////////////////////////////////////////////////////////// + + public void on_button_press(double x, double y) { + if (this.activity.end == 1.0) { + this.clicked.reset_target(0.9, 0.1); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a button of the mouse is released. + ///////////////////////////////////////////////////////////////////// + + public void on_button_release(double x, double y) { + if (this.clicked.end == 0.9) { + this.on_clicked((int)this.position); + this.clicked.reset_target(1.0, 0.1); + } + } +} + +} diff --git a/src/gui/piePreviewCenter.vala b/src/gui/piePreviewCenter.vala new file mode 100644 index 0000000..2a163b6 --- /dev/null +++ b/src/gui/piePreviewCenter.vala @@ -0,0 +1,109 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// +///////////////////////////////////////////////////////////////////////// + +public class PiePreviewCenter : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// THe Images displayed. When the displayed text changes the + /// currently displayed text becomes the old_text. So it's possible + /// to create a smooth transitions. + ///////////////////////////////////////////////////////////////////// + + private RenderedText text = null; + private RenderedText old_text = null; + + ///////////////////////////////////////////////////////////////////// + /// Stores the currently displayed text in order to avoid frequent + /// and useless updates. + ///////////////////////////////////////////////////////////////////// + + private string current_text = null; + + ///////////////////////////////////////////////////////////////////// + /// An AnimatedValue for smooth transitions. + ///////////////////////////////////////////////////////////////////// + + private AnimatedValue blend; + + ///////////////////////////////////////////////////////////////////// + /// The parent renderer. + ///////////////////////////////////////////////////////////////////// + + private unowned PiePreviewRenderer parent; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, sets everything up. + ///////////////////////////////////////////////////////////////////// + + public PiePreviewCenter(PiePreviewRenderer parent) { + this.parent = parent; + this.blend = new AnimatedValue.linear(0, 0, 0); + + this.text = new RenderedText("", 1, 1, "", new Color(), 1.0); + this.old_text = text; + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the currently displayed text. It will be smoothly + /// blended and may contain pango markup. + ///////////////////////////////////////////////////////////////////// + + public void set_text(string text) { + if (text != this.current_text) { + + var style = parent.parent.get_style_context(); + + this.old_text = this.text; + this.text = new RenderedText.with_markup( + text, 180, 180, style.get_font(Gtk.StateFlags.NORMAL).get_family()+" 10", + new Color.from_gdk(style.get_color(Gtk.StateFlags.NORMAL)), 1.0); + this.current_text = text; + + this.blend.reset_target(0.0, 0.0); + this.blend.reset_target(1.0, 0.1); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Draws the center to the given context. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx) { + + this.blend.update(frame_time); + + ctx.save(); + + if (this.parent.slice_count() == 0) + ctx.translate(0, 40); + + this.old_text.paint_on(ctx, 1-this.blend.val); + this.text.paint_on(ctx, this.blend.val); + + ctx.restore(); + } +} + +} diff --git a/src/gui/piePreviewDeleteSign.vala b/src/gui/piePreviewDeleteSign.vala new file mode 100644 index 0000000..a830002 --- /dev/null +++ b/src/gui/piePreviewDeleteSign.vala @@ -0,0 +1,195 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// The delete sign, displayed in the upper right corner of each +/// Slice. +///////////////////////////////////////////////////////////////////////// + +public class PiePreviewDeleteSign : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Called when the user clicked on this sign. + ///////////////////////////////////////////////////////////////////// + + public signal void on_clicked(); + + ///////////////////////////////////////////////////////////////////// + /// The image used to display this oject. + ///////////////////////////////////////////////////////////////////// + + public Image icon { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Some constants determining the look and behaviour of this Slice. + ///////////////////////////////////////////////////////////////////// + + private static const int radius = 18; + private static const double globale_scale = 0.8; + private static const double click_cancel_treshold = 5; + + ///////////////////////////////////////////////////////////////////// + /// True, when the add sign is currently visible. + ///////////////////////////////////////////////////////////////////// + + private bool visible = false; + + ///////////////////////////////////////////////////////////////////// + /// Some AnimatedValues for smooth transitions. + ///////////////////////////////////////////////////////////////////// + + private AnimatedValue size; + private AnimatedValue alpha; + private AnimatedValue activity; + private AnimatedValue clicked; + + ///////////////////////////////////////////////////////////////////// + /// Storing the position where a mouse click was executed. Useful for + /// canceling the click when the mouse moves some pixels. + ///////////////////////////////////////////////////////////////////// + + private double clicked_x = 0.0; + private double clicked_y = 0.0; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, sets everything up. + ///////////////////////////////////////////////////////////////////// + + public PiePreviewDeleteSign() { + this.size = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 2.0); + this.alpha = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 0.0); + this.activity = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, -3, -3, 0, 0.0); + this.clicked = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 1, 1, 0, 0.0); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an Action. All members are initialized accordingly. + ///////////////////////////////////////////////////////////////////// + + public void load() { + this.icon = new Icon("edit-delete", PiePreviewDeleteSign.radius*2); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes this object visible. + ///////////////////////////////////////////////////////////////////// + + public void show() { + if (!this.visible) { + this.visible = true; + this.alpha.reset_target(1.0, 0.3); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Makes this object invisible. + ///////////////////////////////////////////////////////////////////// + + public void hide() { + if (this.visible) { + this.visible = false; + this.alpha.reset_target(0.0, 0.3); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the size of this object. All transitions will be smooth. + ///////////////////////////////////////////////////////////////////// + + public void set_size(double size) { + this.size.reset_target(size, 0.2); + } + + ///////////////////////////////////////////////////////////////////// + /// Draws the sign to the given context. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx) { + this.size.update(frame_time); + this.alpha.update(frame_time); + this.activity.update(frame_time); + this.clicked.update(frame_time); + + if (this.alpha.val > 0) { + ctx.save(); + + // transform the context + double scale = (this.size.val*this.clicked.val + + this.activity.val*0.2 - 0.2)*PiePreviewDeleteSign.globale_scale; + ctx.scale(scale, scale); + + // paint the image + icon.paint_on(ctx, this.alpha.val); + + ctx.restore(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse moves to another position. + ///////////////////////////////////////////////////////////////////// + + public bool on_mouse_move(double x, double y) { + if (this.clicked.end == 0.9) { + double dist = GLib.Math.pow(x-this.clicked_x, 2) + GLib.Math.pow(y-this.clicked_y, 2); + if (dist > PiePreviewDeleteSign.click_cancel_treshold*PiePreviewDeleteSign.click_cancel_treshold) + this.clicked.reset_target(1.0, 0.1); + } + + if (GLib.Math.fabs(x) <= PiePreviewDeleteSign.radius*PiePreviewDeleteSign.globale_scale && GLib.Math.fabs(y) <= PiePreviewDeleteSign.radius*PiePreviewDeleteSign.globale_scale) { + this.activity.reset_target(1.0, 0.2); + return true; + } + + this.activity.reset_target(0.0, 0.2); + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a button of the mouse is pressed. + ///////////////////////////////////////////////////////////////////// + + public bool on_button_press(double x, double y) { + if (this.activity.end == 1.0) { + this.clicked.reset_target(0.9, 0.1); + this.clicked_x = x; + this.clicked_y = y; + return true; + } + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a button of the mouse is released. + ///////////////////////////////////////////////////////////////////// + + public bool on_button_release(double x, double y) { + if (this.clicked.end == 0.9) { + this.clicked.reset_target(1.0, 0.1); + this.on_clicked(); + + return true; + } + return false; + } +} + +} diff --git a/src/gui/piePreviewRenderer.vala b/src/gui/piePreviewRenderer.vala new file mode 100644 index 0000000..53dd2fb --- /dev/null +++ b/src/gui/piePreviewRenderer.vala @@ -0,0 +1,443 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A complex class which is able to draw the preview of a Pie. It can +/// manipulate the displayed Pie as well. +///////////////////////////////////////////////////////////////////////// + +public class PiePreviewRenderer : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// These signals get emitted when a slice is added, removed or + /// manipulated. + ///////////////////////////////////////////////////////////////////// + + public signal void on_add_slice(int position); + public signal void on_remove_slice(int position); + public signal void on_edit_slice(int position); + + ///////////////////////////////////////////////////////////////////// + /// True, when there is currently a drag going on. + ///////////////////////////////////////////////////////////////////// + + public bool drag_n_drop_mode { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// A list containing all SliceRenderers of this Pie. + ///////////////////////////////////////////////////////////////////// + + public Gee.ArrayList<PiePreviewSliceRenderer?> slices; + + ///////////////////////////////////////////////////////////////////// + /// When a Slice is moved within a Pie it is temporarily removed. + /// If so, it is stored in this member. + ///////////////////////////////////////////////////////////////////// + + public PiePreviewSliceRenderer hidden_group { get; private set; default=null; } + + ///////////////////////////////////////////////////////////////////// + /// The add sign which indicates that a new Slice could be added. + ///////////////////////////////////////////////////////////////////// + + private PiePreviewAddSign add_sign = null; + + ///////////////////////////////////////////////////////////////////// + /// The object which renders the name of the currently selected Slice + /// in the middle. + ///////////////////////////////////////////////////////////////////// + + private PiePreviewCenter center_renderer = null; + private enum CenterDisplay { NONE, ACTIVE_SLICE, DROP, ADD, DELETE } + + ///////////////////////////////////////////////////////////////////// + /// Some members storing some inter-frame-information. + ///////////////////////////////////////////////////////////////////// + + private int active_slice = -1; + private double angle = 0.0; + private double mouse_x = 0.0; + private double mouse_y = 0.0; + + ///////////////////////////////////////////////////////////////////// + /// The parent DrawingArea. + ///////////////////////////////////////////////////////////////////// + + public unowned Gtk.DrawingArea parent; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes members. + ///////////////////////////////////////////////////////////////////// + + public PiePreviewRenderer(Gtk.DrawingArea parent) { + this.parent = parent; + this.slices = new Gee.ArrayList<PiePreviewSliceRenderer?>(); + this.center_renderer = new PiePreviewCenter(this); + this.add_sign = new PiePreviewAddSign(this); + this.add_sign.load(); + + this.add_sign.on_clicked.connect((pos) => { + this.on_add_slice(pos); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an Pie. All members are initialized accordingly. + ///////////////////////////////////////////////////////////////////// + + public void load_pie(Pie pie) { + this.slices.clear(); + + foreach (var group in pie.action_groups) { + var renderer = new PiePreviewSliceRenderer(this); + renderer.load(group); + + this.add_slice_renderer(renderer); + this.connect_siganls(renderer); + } + + this.active_slice = -1; + this.update_sizes(); + this.update_positions(false); + } + + ///////////////////////////////////////////////////////////////////// + /// Enables or disables the drag n dropn mode. + ///////////////////////////////////////////////////////////////////// + + public void set_dnd_mode(bool dnd) { + if (this.drag_n_drop_mode != dnd) { + this.drag_n_drop_mode = dnd; + this.update_positions(); + this.update_sizes(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the number of Slices. + ///////////////////////////////////////////////////////////////////// + + public int slice_count() { + if (this.drag_n_drop_mode && !(this.slices.size == 0)) + return slices.size+1; + + return slices.size; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the index of the currently hovered Slice. + ///////////////////////////////////////////////////////////////////// + + public int get_active_slice() { + if (this.slices.size == 0) + return 0; + + if (this.drag_n_drop_mode) + return (int)(this.angle/(2*PI)*this.slice_count() + 0.5) % this.slice_count(); + + return this.active_slice; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the Icon of the currently hovered Slice. + ///////////////////////////////////////////////////////////////////// + + public Icon get_active_icon() { + if (this.active_slice >= 0 && this.active_slice < this.slices.size) + return this.slices[this.active_slice].icon; + else + return new Icon("", 24); + } + + ///////////////////////////////////////////////////////////////////// + /// Draws the entire Pie to the given context. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx) { + this.add_sign.draw(frame_time, ctx); + this.center_renderer.draw(frame_time, ctx); + + foreach (var slice in this.slices) + slice.draw(frame_time, ctx); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse leaves the drawing area of this renderer. + ///////////////////////////////////////////////////////////////////// + + public void on_mouse_leave() { + this.add_sign.hide(); + this.update_positions(); + this.update_center(CenterDisplay.NONE); + + foreach (var slice in this.slices) + slice.on_mouse_leave(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse enters the drawing area of this renderer. + ///////////////////////////////////////////////////////////////////// + + public void on_mouse_enter() { + this.add_sign.show(); + this.update_positions(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse moves in the drawing area of this renderer. + ///////////////////////////////////////////////////////////////////// + + public void on_mouse_move(double x, double y) { + this.mouse_x = x; + this.mouse_y = y; + + this.angle = acos(x/sqrt(x*x + y*y)); + if (y < 0) this.angle = 2*PI - this.angle; + + if (!this.drag_n_drop_mode) + this.active_slice = -1; + + bool delete_hovered = false; + + for (int i=0; i<this.slices.size; ++i) + if (slices[i].on_mouse_move(this.angle, x, y) && !this.drag_n_drop_mode) { + this.active_slice = i; + delete_hovered = slices[i].delete_hovered; + } + + if (this.drag_n_drop_mode) this.update_center(CenterDisplay.DROP); + else if (this.active_slice < 0) this.update_center(CenterDisplay.ADD); + else if (delete_hovered) this.update_center(CenterDisplay.DELETE); + else this.update_center(CenterDisplay.ACTIVE_SLICE); + + this.add_sign.on_mouse_move(this.angle); + + this.update_positions(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a mouse button is pressed over this renderer. + ///////////////////////////////////////////////////////////////////// + + public void on_button_press() { + for (int i=0; i<this.slices.size; ++i) + this.slices[i].on_button_press(this.mouse_x, this.mouse_y); + this.add_sign.on_button_press(this.mouse_x, this.mouse_y); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a mouse button is released over this renderer. + ///////////////////////////////////////////////////////////////////// + + public void on_button_release() { + for (int i=0; i<this.slices.size; ++i) + this.slices[i].on_button_release(this.mouse_x, this.mouse_y); + this.add_sign.on_button_release(this.mouse_x, this.mouse_y); + } + + ///////////////////////////////////////////////////////////////////// + /// Adds a new Slice to the renderer. + ///////////////////////////////////////////////////////////////////// + + public void add_group(ActionGroup group, int at_position = -1) { + var renderer = new PiePreviewSliceRenderer(this); + renderer.load(group); + this.add_slice_renderer(renderer, at_position); + this.connect_siganls(renderer); + } + + ///////////////////////////////////////////////////////////////////// + /// Removes a Slice from the renderer. + ///////////////////////////////////////////////////////////////////// + + public void remove_group(int index) { + if (this.slices.size > index) { + this.slices.remove_at(index); + this.update_positions(); + this.update_sizes(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Hides the Slice at the given position temporarily. + ///////////////////////////////////////////////////////////////////// + + public void hide_group(int index) { + if (this.slices.size > index) { + this.hidden_group = this.slices[index]; + this.remove_group(index); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Re-shows a Slice which has been hidden before. + ///////////////////////////////////////////////////////////////////// + + public void show_hidden_group_at(int index) { + if (this.slices.size >= index && this.hidden_group != null) { + this.hidden_group.set_position(index, false); + this.add_slice_renderer(this.hidden_group, index); + this.hidden_group = null; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Updates a Slice at the given position. + ///////////////////////////////////////////////////////////////////// + + public void update_group(ActionGroup group, int index) { + if (this.slices.size > index) { + var renderer = new PiePreviewSliceRenderer(this); + this.slices.set(index, renderer); + renderer.load(group); + + this.connect_siganls(renderer); + + this.update_positions(false); + this.update_sizes(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Disables all quickactions of this pie preview. + ///////////////////////////////////////////////////////////////////// + + public void disable_quickactions() { + foreach (var slice in this.slices) + slice.disable_quickactions(); + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method which adds a new Slice to the given position. + ///////////////////////////////////////////////////////////////////// + + private void add_slice_renderer(PiePreviewSliceRenderer renderer, int at_position = -1) { + if (at_position < 0 || at_position >= this.slices.size) + this.slices.add(renderer); + else + this.slices.insert(at_position, renderer); + + this.update_positions(false); + this.update_sizes(); + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method which connects all neccessary signals of a newly + /// added Slice. + ///////////////////////////////////////////////////////////////////// + + private void connect_siganls(PiePreviewSliceRenderer renderer) { + renderer.on_clicked.connect((pos) => { + this.on_edit_slice(pos); + }); + + renderer.on_remove.connect((pos) => { + this.on_remove_slice(pos); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Moves all slices to their positions. This may happen smoothly if + /// desired. + ///////////////////////////////////////////////////////////////////// + + private void update_positions(bool smoothly = true) { + if (this.slices.size > 0) { + if (this.add_sign.visible) { + int add_position = 0; + add_position = (int)(this.angle/(2*PI)*this.slice_count()) % this.slice_count(); + this.add_sign.set_position(add_position); + + for (int i=0; i<this.slices.size; ++i) { + this.slices[i].set_position(i, smoothly); + } + + } else if (this.drag_n_drop_mode) { + int add_position = 0; + add_position = (int)(this.angle/(2*PI)*this.slice_count() + 0.5) % this.slice_count(); + + for (int i=0; i<this.slices.size; ++i) { + this.slices[i].set_position(i >= add_position ? i+1 : i, smoothly); + } + + this.update_center(CenterDisplay.DROP); + + } else { + for (int i=0; i<this.slices.size; ++i) { + this.slices[i].set_position(i, smoothly); + } + + if (this.active_slice < 0) this.update_center(CenterDisplay.NONE); + else this.update_center(CenterDisplay.ACTIVE_SLICE); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Resizes all slices to their new sizes. This may happen smoothly + /// if desired. + ///////////////////////////////////////////////////////////////////// + + private void update_sizes() { + double size = 1.0; + if (this.slice_count() > 20) size = 0.5; + else if (this.slice_count() > 8) size = 1.0 - (double)(this.slice_count() - 8)/24.0; + + this.add_sign.set_size(size); + + for (int i=0; i<this.slices.size; ++i) + this.slices[i].set_size(size); + } + + ///////////////////////////////////////////////////////////////////// + /// Displays a new text in the middle of the preview. + ///////////////////////////////////////////////////////////////////// + + private void update_center(CenterDisplay display) { + switch (display) { + case CenterDisplay.ACTIVE_SLICE: + if (this.active_slice >= 0 && this.active_slice < this.slices.size) + this.center_renderer.set_text("<b>" + GLib.Markup.escape_text(slices[this.active_slice].name) + "</b>\n<small>" + + _("Click to edit") + "\n" + _("Drag to move") + "</small>"); + break; + case CenterDisplay.ADD: + this.center_renderer.set_text("<small>" + _("Click to add a new Slice") + "</small>"); + break; + case CenterDisplay.DROP: + if (hidden_group == null) + this.center_renderer.set_text("<small>" + _("Drop to add as new Slice") + "</small>"); + else + this.center_renderer.set_text("<b>" + GLib.Markup.escape_text(this.hidden_group.name) + "</b>\n<small>" + + _("Drop to move Slice") + "</small>"); + break; + case CenterDisplay.DELETE: + if (this.active_slice >= 0 && this.active_slice < this.slices.size) + this.center_renderer.set_text("<b>" + GLib.Markup.escape_text(slices[this.active_slice].name) + "</b>\n<small>" + + _("Click to delete") + "\n" + _("Drag to move") + "</small>"); + break; + default: + this.center_renderer.set_text(""); + break; + } + } +} + +} diff --git a/src/gui/piePreviewSliceRenderer.vala b/src/gui/piePreviewSliceRenderer.vala new file mode 100644 index 0000000..5b4d939 --- /dev/null +++ b/src/gui/piePreviewSliceRenderer.vala @@ -0,0 +1,276 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// Displays the preview of a Slice. +///////////////////////////////////////////////////////////////////////// + +public class PiePreviewSliceRenderer : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Called when the user clicked on this Slice. + ///////////////////////////////////////////////////////////////////// + + public signal void on_clicked(int position); + + ///////////////////////////////////////////////////////////////////// + /// Called when the user clicked on the delete sign. + ///////////////////////////////////////////////////////////////////// + + public signal void on_remove(int position); + + ///////////////////////////////////////////////////////////////////// + /// The image used to display this oject. + ///////////////////////////////////////////////////////////////////// + + public Icon icon { get; private set; } + public ActionGroup action_group { get; private set; } + public string name { get; private set; default=""; } + public bool delete_hovered { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// The parent renderer. + ///////////////////////////////////////////////////////////////////// + + private unowned PiePreviewRenderer parent; + + ///////////////////////////////////////////////////////////////////// + /// The delete sign, displayed in the upper right corner of each + /// Slice. + ///////////////////////////////////////////////////////////////////// + + private PiePreviewDeleteSign delete_sign = null; + + ///////////////////////////////////////////////////////////////////// + /// Some AnimatedValues for smooth transitions. + ///////////////////////////////////////////////////////////////////// + + private AnimatedValue angle; + private AnimatedValue size; + private AnimatedValue activity; + private AnimatedValue clicked; + + ///////////////////////////////////////////////////////////////////// + /// Some constants determining the look and behaviour of this Slice. + ///////////////////////////////////////////////////////////////////// + + private static const double pie_radius = 126; + private static const double radius = 24; + private static const double delete_x = 13; + private static const double delete_y = -13; + private static const double click_cancel_treshold = 5; + + ///////////////////////////////////////////////////////////////////// + /// Storing the position where a mouse click was executed. Useful for + /// canceling the click when the mouse moves some pixels. + ///////////////////////////////////////////////////////////////////// + + private double clicked_x = 0.0; + private double clicked_y = 0.0; + + ///////////////////////////////////////////////////////////////////// + /// The index of this slice in a pie. Clockwise assigned, starting + /// from the right-most slice. + ///////////////////////////////////////////////////////////////////// + + private int position; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, sets everything up. + ///////////////////////////////////////////////////////////////////// + + public PiePreviewSliceRenderer(PiePreviewRenderer parent) { + this.delete_sign = new PiePreviewDeleteSign(); + this.delete_sign.load(); + this.delete_sign.on_clicked.connect(() => { + this.on_remove(this.position); + }); + + this.parent = parent; + this.angle = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 0.5); + this.size = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 1.0); + this.activity = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 0, 0, 0, 0.0); + this.clicked = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, 1, 1, 0, 1.0); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an Action. All members are initialized accordingly. + ///////////////////////////////////////////////////////////////////// + + public void load(ActionGroup group) { + this.action_group = group; + + // if it's a custom ActionGroup + if (group.get_type().depth() == 2 && group.actions.size > 0) { + this.icon = new Icon(group.actions[0].icon, (int)(PiePreviewSliceRenderer.radius*2)); + this.name = group.actions[0].name; + } else { + this.icon = new Icon(GroupRegistry.descriptions[group.get_type().name()].icon, (int)(PiePreviewSliceRenderer.radius*2)); + this.name = GroupRegistry.descriptions[group.get_type().name()].name; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the position where this object should be displayed. + ///////////////////////////////////////////////////////////////////// + + public void set_position(int position, bool smoothly = true) { + double direction = 2.0 * PI * position/parent.slice_count(); + + if (direction != this.angle.end) { + this.position = position; + this.angle.reset_target(direction, smoothly ? 0.5 : 0.0); + + if (!smoothly) + this.angle.update(1.0); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the size of this object. All transitions will be smooth. + ///////////////////////////////////////////////////////////////////// + + public void set_size(double size) { + this.size.reset_target(size, 0.5); + this.delete_sign.set_size(size); + } + + ///////////////////////////////////////////////////////////////////// + /// Notifies that all quick actions should be disabled. + ///////////////////////////////////////////////////////////////////// + + public void disable_quickactions() { + this.action_group.disable_quickactions(); + } + + ///////////////////////////////////////////////////////////////////// + /// Draws the slice to the given context. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx) { + this.size.update(frame_time); + this.angle.update(frame_time); + this.activity.update(frame_time); + this.clicked.update(frame_time); + + ctx.save(); + + // transform the context + ctx.translate(cos(this.angle.val)*PiePreviewSliceRenderer.pie_radius, sin(this.angle.val)*PiePreviewSliceRenderer.pie_radius); + + double scale = this.size.val*this.clicked.val + + this.activity.val*0.1 - 0.1; + ctx.save(); + + ctx.scale(scale, scale); + + // paint the image + icon.paint_on(ctx); + + ctx.restore(); + + ctx.translate(PiePreviewSliceRenderer.delete_x*this.size.val, PiePreviewSliceRenderer.delete_y*this.size.val); + this.delete_sign.draw(frame_time, ctx); + + ctx.restore(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse moves to another position. + ///////////////////////////////////////////////////////////////////// + + public bool on_mouse_move(double angle, double x, double y) { + double direction = 2.0 * PI * position/parent.slice_count(); + double diff = fabs(angle-direction); + + if (diff > PI) + diff = 2 * PI - diff; + + bool active = diff < 0.5*PI/parent.slice_count(); + + if (active) { + this.activity.reset_target(1.0, 0.3); + this.delete_sign.show(); + } else { + this.activity.reset_target(0.0, 0.3); + this.delete_sign.hide(); + } + + if (this.clicked.end == 0.9) { + double dist = GLib.Math.pow(x-this.clicked_x, 2) + GLib.Math.pow(y-this.clicked_y, 2); + if (dist > PiePreviewSliceRenderer.click_cancel_treshold*PiePreviewSliceRenderer.click_cancel_treshold) + this.clicked.reset_target(1.0, 0.1); + } + + double own_x = cos(this.angle.val)*PiePreviewSliceRenderer.pie_radius; + double own_y = sin(this.angle.val)*PiePreviewSliceRenderer.pie_radius; + this.delete_hovered = this.delete_sign.on_mouse_move(x - own_x - PiePreviewSliceRenderer.delete_x*this.size.val, + y - own_y - PiePreviewSliceRenderer.delete_y*this.size.val); + + return active; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the mouse leaves the area of this widget. + ///////////////////////////////////////////////////////////////////// + + public void on_mouse_leave() { + this.activity.reset_target(0.0, 0.3); + this.delete_sign.hide(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a button of the mouse is pressed. + ///////////////////////////////////////////////////////////////////// + + public void on_button_press(double x, double y) { + bool delete_pressed = false; + if (this.activity.end == 1.0) { + double own_x = cos(this.angle.val)*PiePreviewSliceRenderer.pie_radius; + double own_y = sin(this.angle.val)*PiePreviewSliceRenderer.pie_radius; + delete_pressed = this.delete_sign.on_button_press(x - own_x - PiePreviewSliceRenderer.delete_x*this.size.val, + y - own_y - PiePreviewSliceRenderer.delete_y*this.size.val); + } + + if (!delete_pressed && this.activity.end == 1.0) { + this.clicked.reset_target(0.9, 0.1); + this.clicked_x = x; + this.clicked_y = y; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a button of the mouse is released. + ///////////////////////////////////////////////////////////////////// + + public void on_button_release(double x, double y) { + bool deleted = false; + if (this.activity.end == 1.0) + deleted = this.delete_sign.on_button_release(x, y); + + if (!deleted && this.clicked.end == 0.9) { + this.clicked.reset_target(1.0, 0.1); + this.on_clicked(this.position); + } + } +} + +} diff --git a/src/gui/preferencesWindow.vala b/src/gui/preferencesWindow.vala new file mode 100644 index 0000000..fff8168 --- /dev/null +++ b/src/gui/preferencesWindow.vala @@ -0,0 +1,604 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// The settings menu of Gnome-Pie. +///////////////////////////////////////////////////////////////////////// + +public class PreferencesWindow : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The ID of the currently selected Pie. + ///////////////////////////////////////////////////////////////////// + + private string selected_id = ""; + + ///////////////////////////////////////////////////////////////////// + /// Some Gtk widgets used by this window. + ///////////////////////////////////////////////////////////////////// + + private Gtk.Stack? stack = null; + private Gtk.Notebook? notebook = null; + + private Gtk.Window? window = null; + private Gtk.Label? no_pie_label = null; + private Gtk.Label? no_slice_label = null; + private Gtk.Box? preview_box = null; + private Gtk.EventBox? preview_background = null; + private Gtk.Button? remove_pie_button = null; + private Gtk.Button? edit_pie_button = null; + private Gtk.Button? theme_delete_button = null; + + private ThemeList? theme_list = null; + private Gtk.ToggleButton? indicator = null; + private Gtk.ToggleButton? search_by_string = null; + private Gtk.ToggleButton? autostart = null; + private Gtk.ToggleButton? captions = null; + + ///////////////////////////////////////////////////////////////////// + /// Some custom widgets and dialogs used by this window. + ///////////////////////////////////////////////////////////////////// + + private PiePreview? preview = null; + private PieList? pie_list = null; + private PieOptionsWindow? pie_options_window = null; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates the window. + ///////////////////////////////////////////////////////////////////// + + public PreferencesWindow() { + var builder = new Gtk.Builder.from_file(Paths.ui_files + "/preferences.ui"); + + this.window = builder.get_object("window") as Gtk.Window; + this.window.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.KEY_RELEASE_MASK | + Gdk.EventMask.KEY_PRESS_MASK | + Gdk.EventMask.POINTER_MOTION_MASK); + + if (!Daemon.disable_header_bar) { + var headerbar = new Gtk.HeaderBar(); + headerbar.show_close_button = true; + headerbar.title = _("Gnome-Pie Settings"); + headerbar.subtitle = _("bake your pies!"); + window.set_titlebar(headerbar); + } + + this.notebook = builder.get_object("notebook") as Gtk.Notebook; + + if (!Daemon.disable_stack_switcher) { + var main_box = builder.get_object("main-box") as Gtk.Box; + var pie_settings = builder.get_object("pie-settings") as Gtk.Box; + var general_settings = builder.get_object("general-settings") as Gtk.Box; + + pie_settings.parent.remove(pie_settings); + general_settings.parent.remove(general_settings); + + main_box.remove(this.notebook); + + Gtk.StackSwitcher switcher = new Gtk.StackSwitcher(); + switcher.margin_top = 10; + switcher.set_halign(Gtk.Align.CENTER); + main_box.pack_start(switcher, false, true, 0); + + this.stack = new Gtk.Stack(); + this.stack.transition_duration = 500; + this.stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT; + this.stack.homogeneous = true; + this.stack.halign = Gtk.Align.FILL; + this.stack.expand = true; + main_box.add(stack); + switcher.set_stack(stack); + + this.stack.add_with_properties(general_settings, "name", "1", "title", _("General Settings"), null); + this.stack.add_with_properties(pie_settings, "name", "2", "title", _("Pie Settings"), null); + } + + this.pie_list = new PieList(); + this.pie_list.on_select.connect(this.on_pie_select); + this.pie_list.on_activate.connect(() => { + this.on_edit_pie_button_clicked(); + }); + + var scroll_area = builder.get_object("pies-scrolledwindow") as Gtk.ScrolledWindow; + scroll_area.add(this.pie_list); + + this.preview = new PiePreview(); + this.preview.on_first_slice_added.connect(() => { + this.no_slice_label.hide(); + }); + + this.preview.on_last_slice_removed.connect(() => { + this.no_slice_label.show(); + }); + + preview_box = builder.get_object("preview-box") as Gtk.Box; + this.preview_box.pack_start(preview, true, true); + this.no_pie_label = builder.get_object("no-pie-label") as Gtk.Label; + this.no_slice_label = builder.get_object("no-slice-label") as Gtk.Label; + this.preview_background = builder.get_object("preview-background") as Gtk.EventBox; + + this.remove_pie_button = builder.get_object("remove-pie-button") as Gtk.Button; + this.remove_pie_button.clicked.connect(on_remove_pie_button_clicked); + + this.edit_pie_button = builder.get_object("edit-pie-button") as Gtk.Button; + this.edit_pie_button.clicked.connect(on_edit_pie_button_clicked); + + (builder.get_object("add-pie-button") as Gtk.Button).clicked.connect(on_add_pie_button_clicked); + + this.theme_list = new ThemeList(); + this.theme_list.on_select_new.connect(() => { + this.captions.active = Config.global.show_captions; + if (Config.global.theme.has_slice_captions) { + this.captions.sensitive = true; + } else { + this.captions.sensitive = false; + } + if (Config.global.theme.is_local()) { + this.theme_delete_button.sensitive = true; + } else { + this.theme_delete_button.sensitive = false; + } + }); + + scroll_area = builder.get_object("theme-scrolledwindow") as Gtk.ScrolledWindow; + scroll_area.add(this.theme_list); + + (builder.get_object("theme-help-button") as Gtk.Button).clicked.connect(() => { + try{ + GLib.AppInfo.launch_default_for_uri("http://simmesimme.github.io/lessons/2015/04/26/themes-for-gnome-pie/", null); + } catch (Error e) { + warning(e.message); + } + }); + + (builder.get_object("theme-export-button") as Gtk.Button).clicked.connect(on_export_theme_button_clicked); + (builder.get_object("theme-import-button") as Gtk.Button).clicked.connect(on_import_theme_button_clicked); + (builder.get_object("theme-reload-button") as Gtk.Button).clicked.connect(on_reload_theme_button_clicked); + (builder.get_object("theme-open-button") as Gtk.Button).clicked.connect(on_open_theme_button_clicked); + this.theme_delete_button = (builder.get_object("theme-delete-button") as Gtk.Button); + this.theme_delete_button.clicked.connect(on_delete_theme_button_clicked); + + this.autostart = (builder.get_object("autostart-checkbox") as Gtk.ToggleButton); + this.autostart.toggled.connect(on_autostart_toggled); + + this.indicator = (builder.get_object("indicator-checkbox") as Gtk.ToggleButton); + this.indicator.toggled.connect(on_indicator_toggled); + + this.search_by_string = (builder.get_object("select-by-string-checkbox") as Gtk.ToggleButton); + this.search_by_string.toggled.connect(on_search_by_string_toggled); + + this.captions = (builder.get_object("captions-checkbox") as Gtk.ToggleButton); + this.captions.toggled.connect(on_captions_toggled); + + var scale_slider = (builder.get_object("scale-hscale") as Gtk.Scale); + scale_slider.set_range(0.5, 2.0); + scale_slider.set_increments(0.05, 0.25); + scale_slider.set_value(Config.global.global_scale); + + bool changing = false; + bool changed_again = false; + + scale_slider.value_changed.connect(() => { + if (!changing) { + changing = true; + Timeout.add(300, () => { + if (changed_again) { + changed_again = false; + return true; + } + + Config.global.global_scale = scale_slider.get_value(); + Config.global.load_themes(Config.global.theme.name); + changing = false; + return false; + }); + } else { + changed_again = true; + } + }); + + var range_slider = (builder.get_object("range-hscale") as Gtk.Scale); + range_slider.set_range(0, 2000); + range_slider.set_increments(10, 100); + range_slider.set_value(Config.global.activation_range); + range_slider.value_changed.connect(() => { + Config.global.activation_range = (int)range_slider.get_value(); + }); + + var range_slices = (builder.get_object("range-slices") as Gtk.Scale); + range_slices.set_range(12, 96); + range_slices.set_increments(4, 12); + range_slices.set_value(Config.global.max_visible_slices); + range_slices.value_changed.connect(() => { + Config.global.max_visible_slices = (int)range_slices.get_value(); + }); + + var info_box = (builder.get_object("info-box") as Gtk.Box); + + // info label + var info_label = new TipViewer({ + _("Pies can be opened with the terminal command \"gnome-pie --open=ID\"."), + _("Feel free to visit Gnome-Pie's homepage at %s!").printf("<a href='http://simmesimme.github.io/gnome-pie.html'>gnome-pie.simonschneegans.de</a>"), + _("If you want to give some feedback, please write an e-mail to %s!").printf("<a href='mailto:code@simonschneegans.de'>code@simonschneegans.de</a>"), + _("You can support the development of Gnome-Pie by donating via %s.").printf("<a href='https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=X65SUVC4ZTQSC'>Paypal</a>"), + _("Translating Gnome-Pie to your language is easy. Translations are managed at %s.").printf("<a href='https://translate.zanata.org/zanata/iteration/view/gnome-pie/develop'>Zanata</a>"), + _("It's easy to create new themes for Gnome-Pie. Read the <a href='%s'>Tutorial</a> online.").printf("http://simmesimme.github.io/lessons/2015/04/26/themes-for-gnome-pie/"), + _("It's usually a good practice to have at most twelve slices per pie."), + _("You can export themes you created and share them with the community!"), + _("The source code of Gnome-Pie is available on %s.").printf("<a href='https://github.com/Simmesimme/Gnome-Pie'>Github</a>"), + _("Bugs can be reported at %s!").printf("<a href='https://github.com/Simmesimme/Gnome-Pie/issues'>Github</a>"), + _("Suggestions can be posted on %s!").printf("<a href='https://github.com/Simmesimme/Gnome-Pie/issues'>Github</a>"), + _("An awesome companion of Gnome-Pie is %s. It will make using your computer feel like magic!").printf("<a href='https://github.com/thjaeger/easystroke/wiki'>Easystroke</a>"), + _("You can drag'n'drop applications from your main menu to the pie above."), + _("You may drag'n'drop URLs and bookmarks from your internet browser to the pie above."), + _("You can drag'n'drop files and folders from your file browser to the pie above."), + _("You can drag'n'drop pies from the list on the left into other pies in order to create sub-pies."), + _("You can drag'n'drop pies from the list on the left to your desktop or dock to create a launcher for this pie.") + }); + this.window.show.connect(info_label.start_slide_show); + this.window.hide.connect(info_label.stop_slide_show); + + info_box.pack_end(info_label); + + this.window.hide.connect(() => { + // save settings on close + Config.global.save(); + Pies.save(); + + Timeout.add(100, () => { + IconSelectWindow.clear_icons(); + return false; + }); + }); + + this.window.delete_event.connect(this.window.hide_on_delete); + } + + ///////////////////////////////////////////////////////////////////// + /// Shows the window. + ///////////////////////////////////////////////////////////////////// + + public void show() { + this.preview.draw_loop(); + this.window.show_all(); + this.pie_list.select_first(); + + var style = this.preview_background.get_style_context(); + this.preview_background.override_background_color(Gtk.StateFlags.NORMAL, style.get_background_color(Gtk.StateFlags.NORMAL)); + + this.indicator.active = Config.global.show_indicator; + this.autostart.active = Config.global.auto_start; + this.captions.active = Config.global.show_captions; + this.search_by_string.active = Config.global.search_by_string; + + if (Config.global.theme.has_slice_captions) { + this.captions.sensitive = true; + } else { + this.captions.sensitive = false; + } + + if (Config.global.theme.is_local()) { + this.theme_delete_button.sensitive = true; + } else { + this.theme_delete_button.sensitive = false; + } + + if (!Daemon.disable_stack_switcher) { + this.stack.set_visible_child_full("2", Gtk.StackTransitionType.NONE); + } else { + this.notebook.set_current_page(1); + } + this.pie_list.has_focus = true; + } + + ///////////////////////////////////////////////////////////////////// + /// Creates or deletes the autostart file. This code is inspired + /// by project synapse as well. + ///////////////////////////////////////////////////////////////////// + + private void on_autostart_toggled(Gtk.ToggleButton check_box) { + + bool active = check_box.active; + if (!active && FileUtils.test(Paths.autostart, FileTest.EXISTS)) { + Config.global.auto_start = false; + // delete the autostart file + FileUtils.remove (Paths.autostart); + } + else if (active && !FileUtils.test(Paths.autostart, FileTest.EXISTS)) { + Config.global.auto_start = true; + + string autostart_entry = + "#!/usr/bin/env xdg-open\n" + + "[Desktop Entry]\n" + + "Name=Gnome-Pie\n" + + "Exec=" + Paths.executable + "\n" + + "Encoding=UTF-8\n" + + "Type=Application\n" + + "X-GNOME-Autostart-enabled=true\n" + + "Icon=gnome-pie\n"; + + // create the autostart file + string autostart_dir = GLib.Path.get_dirname(Paths.autostart); + if (!FileUtils.test(autostart_dir, FileTest.EXISTS | FileTest.IS_DIR)) { + DirUtils.create_with_parents(autostart_dir, 0755); + } + + try { + FileUtils.set_contents(Paths.autostart, autostart_entry); + FileUtils.chmod(Paths.autostart, 0755); + } catch (Error e) { + var d = new Gtk.MessageDialog(this.window, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, + "%s", e.message); + d.run(); + d.destroy(); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Saves the current theme to an archive. + ///////////////////////////////////////////////////////////////////// + + private void on_export_theme_button_clicked(Gtk.Button button) { + var dialog = new Gtk.FileChooserDialog("Pick a file", this.window, + Gtk.FileChooserAction.SAVE, + "_Cancel", + Gtk.ResponseType.CANCEL, + "_Save", + Gtk.ResponseType.ACCEPT); + + dialog.set_do_overwrite_confirmation(true); + dialog.set_modal(true); + dialog.filter = new Gtk.FileFilter(); + dialog.filter.add_pattern ("*.tar.gz"); + dialog.set_current_name(Config.global.theme.name + ".tar.gz"); + + dialog.response.connect((d, result) => { + if (result == Gtk.ResponseType.ACCEPT) { + var file = dialog.get_filename(); + if (!file.has_suffix(".tar.gz")) { + file = file + ".tar.gz"; + } + Config.global.theme.export(file); + } + dialog.destroy(); + }); + dialog.show(); + } + + ///////////////////////////////////////////////////////////////////// + /// Imports a new theme from an archive. + ///////////////////////////////////////////////////////////////////// + + private void on_import_theme_button_clicked(Gtk.Button button) { + var dialog = new Gtk.FileChooserDialog("Pick a file", this.window, + Gtk.FileChooserAction.OPEN, + "_Cancel", + Gtk.ResponseType.CANCEL, + "_Open", + Gtk.ResponseType.ACCEPT); + + dialog.set_modal(true); + dialog.filter = new Gtk.FileFilter(); + dialog.filter.add_pattern ("*.tar.gz"); + + var result = Gtk.MessageType.INFO; + var message = _("Sucessfully imported new theme!"); + + dialog.response.connect((d, r) => { + if (r == Gtk.ResponseType.ACCEPT) { + var file = dialog.get_filename(); + + var a = new ThemeImporter(); + if (a.open(file)) { + if (a.is_valid_theme) { + if (!Config.global.has_theme(a.theme_name)) { + if (a.extract_to(Paths.local_themes + "/" + a.theme_name)) { + Config.global.load_themes(a.theme_name); + this.theme_list.reload(); + } else { + message = _("An error occured while importing the theme: Failed to extract theme!"); + result = Gtk.MessageType.ERROR; + } + } else { + message = _("An error occured while importing the theme: A theme with this name does already exist!"); + result = Gtk.MessageType.ERROR; + } + } else { + message = _("An error occured while importing the theme: Theme archive does not contain a valid theme!"); + result = Gtk.MessageType.ERROR; + } + } else { + message = _("An error occured while importing the theme: Failed to open theme archive!"); + result = Gtk.MessageType.ERROR; + } + a.close(); + + var result_dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL, + result, Gtk.ButtonsType.CLOSE, message); + result_dialog.run(); + result_dialog.destroy(); + } + dialog.destroy(); + + }); + dialog.show(); + } + + ///////////////////////////////////////////////////////////////////// + /// Deleted the slected theme. + ///////////////////////////////////////////////////////////////////// + + private void on_delete_theme_button_clicked(Gtk.Button button) { + + var dialog = new Gtk.MessageDialog((Gtk.Window)this.window.get_toplevel(), Gtk.DialogFlags.MODAL, + Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, + _("Do you really want to delete the selected theme from %s?").printf(Config.global.theme.directory)); + + dialog.response.connect((response) => { + if (response == Gtk.ResponseType.YES) { + Paths.delete_directory(Config.global.theme.directory); + Config.global.load_themes(""); + this.theme_list.reload(); + } + }); + + dialog.run(); + dialog.destroy(); + } + + ///////////////////////////////////////////////////////////////////// + /// Reloads all themes. + ///////////////////////////////////////////////////////////////////// + + private void on_reload_theme_button_clicked(Gtk.Button button) { + Config.global.load_themes(Config.global.theme.name); + this.theme_list.reload(); + } + + ///////////////////////////////////////////////////////////////////// + /// Opens the loaction of the them in the file browser. + ///////////////////////////////////////////////////////////////////// + + private void on_open_theme_button_clicked(Gtk.Button button) { + try{ + GLib.AppInfo.launch_default_for_uri("file://" + Config.global.theme.directory, null); + } catch (Error e) { + warning(e.message); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Shows or hides the indicator. + ///////////////////////////////////////////////////////////////////// + + private void on_indicator_toggled(Gtk.ToggleButton check_box) { + var check = check_box as Gtk.CheckButton; + Config.global.show_indicator = check.active; + } + + ///////////////////////////////////////////////////////////////////// + /// Shows or hides the captions of Slices. + ///////////////////////////////////////////////////////////////////// + + private void on_captions_toggled(Gtk.ToggleButton check_box) { + var check = check_box as Gtk.CheckButton; + Config.global.show_captions = check.active; + } + + ///////////////////////////////////////////////////////////////////// + /// Enables or disables Slice selection by typing. + ///////////////////////////////////////////////////////////////////// + + private void on_search_by_string_toggled(Gtk.ToggleButton check_box) { + var check = check_box as Gtk.CheckButton; + Config.global.search_by_string = check.active; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when a new Pie is selected in the PieList. + ///////////////////////////////////////////////////////////////////// + + private void on_pie_select(string id) { + selected_id = id; + + this.no_slice_label.hide(); + this.no_pie_label.hide(); + this.preview_box.hide(); + + this.remove_pie_button.sensitive = false; + this.edit_pie_button.sensitive = false; + + if (id == "") { + this.no_pie_label.show(); + } else { + var pie = PieManager.all_pies[selected_id]; + + this.preview.set_pie(id); + this.preview_box.show(); + + if (pie.action_groups.size == 0) { + this.no_slice_label.show(); + } + + this.remove_pie_button.sensitive = true; + this.edit_pie_button.sensitive = true; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the add Pie button is clicked. + ///////////////////////////////////////////////////////////////////// + + private void on_add_pie_button_clicked(Gtk.Button button) { + var new_pie = PieManager.create_persistent_pie(_("New Pie"), "stock_unknown", null); + this.pie_list.reload_all(); + this.pie_list.select(new_pie.id); + + this.on_edit_pie_button_clicked(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the remove Pie button is clicked. + ///////////////////////////////////////////////////////////////////// + + private void on_remove_pie_button_clicked(Gtk.Button button) { + if (this.selected_id != "") { + var dialog = new Gtk.MessageDialog((Gtk.Window)this.window.get_toplevel(), Gtk.DialogFlags.MODAL, + Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, + _("Do you really want to delete the selected Pie with all contained Slices?")); + + dialog.response.connect((response) => { + if (response == Gtk.ResponseType.YES) { + PieManager.remove_pie(selected_id); + this.pie_list.reload_all(); + this.pie_list.select_first(); + } + }); + + dialog.run(); + dialog.destroy(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the edit pie button is clicked. + ///////////////////////////////////////////////////////////////////// + + private void on_edit_pie_button_clicked(Gtk.Button? button = null) { + if (this.pie_options_window == null) { + this.pie_options_window = new PieOptionsWindow(); + this.pie_options_window.set_parent(window); + this.pie_options_window.on_ok.connect((trigger, name, icon) => { + var pie = PieManager.all_pies[selected_id]; + pie.name = name; + pie.icon = icon; + PieManager.bind_trigger(trigger, selected_id); + PieManager.create_launcher(pie.id); + this.pie_list.reload_all(); + }); + } + + this.pie_options_window.set_pie(selected_id); + this.pie_options_window.show(); + } +} + +} diff --git a/src/gui/sliceTypeList.vala b/src/gui/sliceTypeList.vala new file mode 100644 index 0000000..1a9ecc4 --- /dev/null +++ b/src/gui/sliceTypeList.vala @@ -0,0 +1,173 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A list displaying all available Action types and ActionGroup types. +///////////////////////////////////////////////////////////////////////// + +class SliceTypeList : Gtk.TreeView { + + ///////////////////////////////////////////////////////////////////// + /// This signal gets emitted when the user selects a new Type. + ///////////////////////////////////////////////////////////////////// + + public signal void on_select(string id, string icon_name); + + ///////////////////////////////////////////////////////////////////// + /// The listore which staroes all types internally. + ///////////////////////////////////////////////////////////////////// + + private Gtk.ListStore data; + private enum DataPos {ICON, ICON_NAME, NAME, ID} + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs the Widget. + ///////////////////////////////////////////////////////////////////// + + public SliceTypeList() { + GLib.Object(); + + this.data = new Gtk.ListStore(4, typeof(Gdk.Pixbuf), + typeof(string), + typeof(string), + typeof(string)); + + this.data.set_sort_column_id(2, Gtk.SortType.ASCENDING); + + base.set_model(this.data); + base.set_headers_visible(true); + base.set_grid_lines(Gtk.TreeViewGridLines.NONE); + this.set_fixed_height_mode(true); + + var main_column = new Gtk.TreeViewColumn(); + main_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED); + main_column.title = _("Slice types"); + var icon_render = new Gtk.CellRendererPixbuf(); + main_column.pack_start(icon_render, false); + + var name_render = new Gtk.CellRendererText(); + name_render.xpad = 6; + main_column.pack_start(name_render, true); + + base.append_column(main_column); + + main_column.add_attribute(icon_render, "pixbuf", DataPos.ICON); + main_column.add_attribute(name_render, "markup", DataPos.NAME); + + this.get_selection().changed.connect(() => { + Gtk.TreeIter active; + if (this.get_selection().get_selected(null, out active)) { + string id = ""; + string icon = ""; + this.data.get(active, DataPos.ID, out id); + this.data.get(active, DataPos.ICON_NAME, out icon); + this.on_select(id, icon); + } + }); + + reload_all(); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads a registered actions and action groups. + ///////////////////////////////////////////////////////////////////// + + public void reload_all() { + Gtk.TreeIter active; + string current_id = ""; + if (this.get_selection().get_selected(null, out active)) + this.data.get(active, DataPos.ID, out current_id); + + data.clear(); + + foreach (var action_type in ActionRegistry.types) { + var description = ActionRegistry.descriptions[action_type]; + + Gtk.TreeIter current; + data.append(out current); + var icon = new Icon(description.icon, 36); + data.set(current, DataPos.ICON, icon.to_pixbuf()); + data.set(current, DataPos.ICON_NAME, description.icon); + data.set(current, DataPos.NAME, GLib.Markup.escape_text(description.name) + "\n" + + "<span font-size='x-small'>" + GLib.Markup.escape_text(description.description) + "</span>"); + data.set(current, DataPos.ID, description.id); + } + + foreach (var group_type in GroupRegistry.types) { + var description = GroupRegistry.descriptions[group_type]; + + Gtk.TreeIter current; + data.append(out current); + var icon = new Icon(description.icon, 36); + data.set(current, DataPos.ICON, icon.to_pixbuf()); + data.set(current, DataPos.ICON_NAME, description.icon); + data.set(current, DataPos.NAME, GLib.Markup.escape_text(description.name) + "\n" + + "<span font-size='x-small'>" + GLib.Markup.escape_text(description.description) + "</span>"); + data.set(current, DataPos.ID, description.id); + } + + select_first(); + select(current_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Selects the first type in the list. + ///////////////////////////////////////////////////////////////////// + + public void select_first() { + Gtk.TreeIter active; + + if(this.data.get_iter_first(out active) ) { + this.get_selection().select_iter(active); + string id = ""; + string icon = ""; + this.data.get(active, DataPos.ID, out id); + this.data.get(active, DataPos.ICON_NAME, out icon); + this.on_select(id, icon); + } else { + this.on_select("", "stock_unknown"); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Select the given slice type. + ///////////////////////////////////////////////////////////////////// + + public void select(string id) { + this.data.foreach((model, path, iter) => { + string pie_id; + this.data.get(iter, DataPos.ID, out pie_id); + + if (id == pie_id) { + this.get_selection().select_iter(iter); + string icon = ""; + this.data.get(iter, DataPos.ICON_NAME, out icon); + this.on_select(pie_id, icon); + this.scroll_to_cell(path, null, true, 0.5f, 0.5f); + this.has_focus = true; + + return true; + } + + return false; + }); + } +} + +} diff --git a/src/gui/themeList.vala b/src/gui/themeList.vala new file mode 100644 index 0000000..786c305 --- /dev/null +++ b/src/gui/themeList.vala @@ -0,0 +1,118 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A widget displaying all available themes of Gnome-Pie. +///////////////////////////////////////////////////////////////////////// + +class ThemeList : Gtk.TreeView { + + ///////////////////////////////////////////////////////////////////// + /// This signal gets emitted, when a new theme is selected by the + /// user. This new theme is applied automatically, with this signal + /// actions may be triggered which should be executed AFTER the + /// change to a new theme. + ///////////////////////////////////////////////////////////////////// + + public signal void on_select_new(); + + ///////////////////////////////////////////////////////////////////// + /// The currently selected row. + ///////////////////////////////////////////////////////////////////// + + private Gtk.TreeIter active { private get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// The positions in the data list store. + ///////////////////////////////////////////////////////////////////// + + private enum DataPos {ICON, NAME} + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs the Widget. + ///////////////////////////////////////////////////////////////////// + + public ThemeList() { + GLib.Object(); + + this.set_headers_visible(true); + this.set_grid_lines(Gtk.TreeViewGridLines.NONE); + this.set_fixed_height_mode(true); + + var main_column = new Gtk.TreeViewColumn(); + main_column.title = _("Themes"); + main_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED); + var icon_render = new Gtk.CellRendererPixbuf(); + icon_render.xpad = 4; + icon_render.ypad = 4; + main_column.pack_start(icon_render, false); + + var name_render = new Gtk.CellRendererText(); + name_render.xpad = 6; + main_column.pack_start(name_render, true); + + this.append_column(main_column); + + main_column.add_attribute(icon_render, "pixbuf", DataPos.ICON); + main_column.add_attribute(name_render, "markup", DataPos.NAME); + + this.get_selection().changed.connect(() => { + Gtk.TreeIter active; + if (this.get_selection().get_selected(null, out active)) { + Timeout.add(10, () => { + int index = int.parse(this.model.get_path(active).to_string()); + Config.global.theme = Config.global.themes[index]; + + this.on_select_new(); + + Config.global.theme.load(); + Config.global.theme.load_images(); + return false; + }); + } + }); + + reload(); + } + + public void reload() { + + var data = new Gtk.ListStore(2, typeof(Gdk.Pixbuf), + typeof(string)); + this.set_model(data); + + // load all themes into the list + var themes = Config.global.themes; + foreach(var theme in themes) { + Gtk.TreeIter current; + data.append(out current); + data.set(current, DataPos.ICON, theme.preview_icon.to_pixbuf()); + data.set(current, DataPos.NAME, GLib.Markup.escape_text(theme.name)+"\n" + + "<span font-size='x-small'>" + GLib.Markup.escape_text(theme.description) + + " - <i>"+GLib.Markup.escape_text(_("by")+" "+theme.author) + + "</i></span>"); + if(theme == Config.global.theme) { + get_selection().select_iter(current); + this.scroll_to_cell(get_selection().get_selected_rows(null).nth_data(0), null, true, 0.5f, 0.5f); + } + } + } +} + +} diff --git a/src/gui/tipViewer.vala b/src/gui/tipViewer.vala new file mode 100644 index 0000000..e2158bd --- /dev/null +++ b/src/gui/tipViewer.vala @@ -0,0 +1,163 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A widget showing tips. The tips are beautifully faded in and out. +///////////////////////////////////////////////////////////////////////// + +public class TipViewer : Gtk.Label { + + ///////////////////////////////////////////////////////////////////// + /// Some settings tweaking the behavior of the TipViewer. + ///////////////////////////////////////////////////////////////////// + + private const double fade_time = 0.5; + private const double frame_rate = 20.0; + private const double base_delay = 3.0; + + ///////////////////////////////////////////////////////////////////// + /// False, if the playback of tips is stopped. + ///////////////////////////////////////////////////////////////////// + + private bool playing = false; + + ///////////////////////////////////////////////////////////////////// + /// An array containing all tips. + ///////////////////////////////////////////////////////////////////// + + private string[] tips; + + ///////////////////////////////////////////////////////////////////// + /// The index of the currently displayed tip. + ///////////////////////////////////////////////////////////////////// + + private int index = -1; + + ///////////////////////////////////////////////////////////////////// + /// The fading value. + ///////////////////////////////////////////////////////////////////// + + private AnimatedValue alpha; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members and sets the basic layout. + ///////////////////////////////////////////////////////////////////// + + public TipViewer(string[] tips) { + this.tips = tips; + + this.alpha = new AnimatedValue.linear(0.0, 1.0, fade_time); + + this.set_alignment (0.0f, 0.5f); + this.opacity = 0; + this.wrap = true; + this.valign = Gtk.Align.END; + this.set_use_markup(true); + + this.override_font(Pango.FontDescription.from_string("8")); + } + + ///////////////////////////////////////////////////////////////////// + /// Starts the playback of tips. + ///////////////////////////////////////////////////////////////////// + + public void start_slide_show() { + if (!this.playing && tips.length > 1) { + this.playing = true; + show_tip(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Stops the playback of tips. + ///////////////////////////////////////////////////////////////////// + + public void stop_slide_show() { + this.playing = false; + } + + ///////////////////////////////////////////////////////////////////// + /// Starts the fading in. + ///////////////////////////////////////////////////////////////////// + + private void fade_in() { + this.alpha = new AnimatedValue.linear(this.alpha.val, 1.0, fade_time); + + GLib.Timeout.add((uint)(1000.0/frame_rate), () => { + this.alpha.update(1.0/frame_rate); + this.opacity = this.alpha.val; + + return (this.alpha.val != 1.0); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Starts the fading out. + ///////////////////////////////////////////////////////////////////// + + private void fade_out() { + this.alpha = new AnimatedValue.linear(this.alpha.val, 0.0, fade_time); + + GLib.Timeout.add((uint)(1000.0/frame_rate), () => { + this.alpha.update(1.0/frame_rate); + this.opacity = this.alpha.val; + + return (this.alpha.val != 0.0); + }); + } + + private void show_tip() { + + this.set_random_tip(); + + this.fade_in(); + + uint delay = (uint)(base_delay*1000.0) + tips[this.index].length*30; + + GLib.Timeout.add(delay, () => { + this.fade_out(); + + if (this.playing) { + GLib.Timeout.add((uint)(1000.0*fade_time), () => { + this.show_tip(); + return false; + }); + } + + return false; + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Chooses the next random tip. + ///////////////////////////////////////////////////////////////////// + + private void set_random_tip() { + if (tips.length > 1) { + int next_index = -1; + do { + next_index = GLib.Random.int_range(0, tips.length); + } while (next_index == this.index); + this.index = next_index; + this.label = tips[this.index]; + } + } +} + +} diff --git a/src/gui/triggerSelectButton.vala b/src/gui/triggerSelectButton.vala new file mode 100644 index 0000000..eb34066 --- /dev/null +++ b/src/gui/triggerSelectButton.vala @@ -0,0 +1,163 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This window allows the selection of a hotkey. It is returned in form +/// of a Trigger. Therefore it can be either a keyboard driven hotkey or +/// a mouse based hotkey. +///////////////////////////////////////////////////////////////////////// + +public class TriggerSelectButton : Gtk.ToggleButton { + + ///////////////////////////////////////////////////////////////////// + /// This signal is emitted when the user selects a new hot key. + ///////////////////////////////////////////////////////////////////// + + public signal void on_select(Trigger trigger); + + ///////////////////////////////////////////////////////////////////// + /// The currently contained Trigger. + ///////////////////////////////////////////////////////////////////// + + private Trigger trigger = null; + + ///////////////////////////////////////////////////////////////////// + /// True, if mouse buttons can be bound as well. + ///////////////////////////////////////////////////////////////////// + + private bool enable_mouse = false; + + ///////////////////////////////////////////////////////////////////// + /// These modifiers are ignored. + ///////////////////////////////////////////////////////////////////// + + private Gdk.ModifierType lock_modifiers = Gdk.ModifierType.MOD2_MASK + |Gdk.ModifierType.MOD4_MASK + |Gdk.ModifierType.MOD5_MASK + |Gdk.ModifierType.LOCK_MASK; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, constructs a new TriggerSelectWindow. + ///////////////////////////////////////////////////////////////////// + + public TriggerSelectButton(bool enable_mouse) { + this.enable_mouse = enable_mouse; + + this.toggled.connect(() => { + if (this.active) { + this.set_label(_("Press a hotkey ...")); + Gtk.grab_add(this); + FocusGrabber.grab(this.get_window(), true, true, true); + } + }); + + this.button_press_event.connect(this.on_button_press); + this.key_press_event.connect(this.on_key_press); + this.set_trigger(new Trigger()); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes the button display the given Trigger. + ///////////////////////////////////////////////////////////////////// + + public void set_trigger(Trigger trigger) { + this.trigger = trigger; + this.set_label(trigger.label); + } + + ///////////////////////////////////////////////////////////////////// + /// Can be called to cancel the selection process. + ///////////////////////////////////////////////////////////////////// + + private void cancel() { + this.set_label(trigger.label); + this.set_active(false); + Gtk.grab_remove(this); + FocusGrabber.ungrab(true, true); + } + + ///////////////////////////////////////////////////////////////////// + /// Makes the button display the given Trigger. + ///////////////////////////////////////////////////////////////////// + + private void update_trigger(Trigger trigger) { + if (this.trigger.name != trigger.name) { + this.set_trigger(trigger); + this.on_select(this.trigger); + } + + this.cancel(); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user presses a keyboard key. + ///////////////////////////////////////////////////////////////////// + + private bool on_key_press(Gdk.EventKey event) { + if (this.active) { + if (Gdk.keyval_name(event.keyval) == "Escape") { + this.cancel(); + } else if (Gdk.keyval_name(event.keyval) == "BackSpace") { + this.update_trigger(new Trigger()); + } else if (event.is_modifier == 0) { + Gdk.ModifierType state = event.state & ~ this.lock_modifiers; + this.update_trigger(new Trigger.from_values(event.keyval, state, false, false, false, + false, false, 5)); + } + + return true; + } + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user presses a button of the mouse. + ///////////////////////////////////////////////////////////////////// + + private bool on_button_press(Gdk.EventButton event) { + if (this.active) { + Gtk.Allocation rect; + this.get_allocation(out rect); + if (event.x < 0 || event.x > rect.width + || event.y < 0 || event.y > rect.height) { + + this.cancel(); + return true; + } + } + + if (this.active && this.enable_mouse) { + Gdk.ModifierType state = event.state & ~ this.lock_modifiers; + var new_trigger = new Trigger.from_values((int)event.button, state, true, + false, false, false, false, 5); + + if (new_trigger.key_code != 1) this.update_trigger(new_trigger); + else this.cancel(); + + return true; + } else if (this.active) { + this.cancel(); + return true; + } + + return false; + } +} + +} diff --git a/src/images/icon.vala b/src/images/icon.vala new file mode 100644 index 0000000..9cfccf8 --- /dev/null +++ b/src/images/icon.vala @@ -0,0 +1,135 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A class representing a square-shaped icon, loaded from the users +/// icon theme. +///////////////////////////////////////////////////////////////////////// + +public class Icon : Image { + + ///////////////////////////////////////////////////////////////////// + /// A cache which stores loaded icon. It is cleared when the icon + /// theme of the user changes. The key is in form <filename>@<size>. + ///////////////////////////////////////////////////////////////////// + + private static Gee.HashMap<string, Cairo.ImageSurface?> cache { private get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Initializes the cache. + ///////////////////////////////////////////////////////////////////// + + public static void init() { + clear_cache(); + + Gtk.IconTheme.get_default().changed.connect(() => { + clear_cache(); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Clears the cache. + ///////////////////////////////////////////////////////////////////// + + public static void clear_cache() { + cache = new Gee.HashMap<string, Cairo.ImageSurface?>(); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an icon from the current icon theme of the user. + ///////////////////////////////////////////////////////////////////// + + public Icon(string icon_name, int size) { + var cached = Icon.cache.get("%s@%u".printf(icon_name, size)); + + if (cached == null) { + this.load_file_at_size(Icon.get_icon_file(icon_name, size), size, size); + Icon.cache.set("%s@%u".printf(icon_name, size), this.surface); + } else { + this.surface = cached; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the size of the icon in pixels. Greetings to Liskov. + ///////////////////////////////////////////////////////////////////// + + public int size() { + return base.width(); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the icon name for a given GLib.Icon. + ///////////////////////////////////////////////////////////////////// + + public static string get_icon_name(GLib.Icon? icon) { + if (icon != null) { + var icon_names = icon.to_string().split(" "); + + foreach (var icon_name in icon_names) { + if (Gtk.IconTheme.get_default().has_icon(icon_name)) { + return icon_name; + } + } + } + + return ""; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the filename for a given system icon. + ///////////////////////////////////////////////////////////////////// + + public static string get_icon_file(string icon_name, int size) { + string result = ""; + + if (icon_name.contains("/")) { + var file = GLib.File.new_for_path(icon_name); + if(file.query_exists()) + return icon_name; + + warning("Icon \"" + icon_name + "\" not found! Using default icon..."); + } + + + var icon_theme = Gtk.IconTheme.get_default(); + var file = icon_theme.lookup_icon(icon_name, size, 0); + if (file != null) result = file.get_filename(); + + if (result == "") { + warning("Icon \"" + icon_name + "\" not found! Using default icon..."); + + string[] default_icons = {"application-default-icon", "stock_unknown"}; + foreach (var icon in default_icons) { + file = icon_theme.lookup_icon(icon, size, 0); + if (file != null) { + result = file.get_filename(); + break; + } + } + + if (result == "") + warning("No default icon found! Will be ugly..."); + } + + return result; + } +} + +} diff --git a/src/images/image.vala b/src/images/image.vala new file mode 100644 index 0000000..a903493 --- /dev/null +++ b/src/images/image.vala @@ -0,0 +1,215 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A class which loads image files. It can load image files in various +/// formats, including jpeg, png and svg. +///////////////////////////////////////////////////////////////////////// + +public class Image : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The internally used surface. + ///////////////////////////////////////////////////////////////////// + + public Cairo.ImageSurface surface { public get; protected set; default=null; } + + ///////////////////////////////////////////////////////////////////// + /// Creates an empty Image. + ///////////////////////////////////////////////////////////////////// + + public Image.empty(int width, int height, Color? color = null) { + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + + if (color != null) { + var ctx = this.context(); + ctx.set_source_rgb(color.r, color.g, color.b); + ctx.paint(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Creates an image from the the given filename. + ///////////////////////////////////////////////////////////////////// + + public Image.from_file(string filename) { + this.load_file(filename); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates an image from the the given filename at a given size. + ///////////////////////////////////////////////////////////////////// + + public Image.from_file_at_size(string filename, int width, int height) { + this.load_file_at_size(filename, width, height); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates an image from the the given Gdk.Pixbuf. + ///////////////////////////////////////////////////////////////////// + + public Image.from_pixbuf(Gdk.Pixbuf pixbuf) { + if (pixbuf != null) this.load_pixbuf(pixbuf); + else this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + } + + ///////////////////////////////////////////////////////////////////// + /// Captures a part of the screen. + ///////////////////////////////////////////////////////////////////// + + public Image.capture_screen(int posx, int posy, int width, int height, bool hide_pies = true) { + Gdk.Window root = Gdk.get_default_root_window(); + Gdk.Pixbuf pixbuf = Gdk.pixbuf_get_from_window(root, posx, posy, width, height); + + this.load_pixbuf(pixbuf); + + if (hide_pies) { + // check for opened pies + foreach (var window in PieManager.opened_windows) { + if (window.background != null) { + int x=0, y=0, dx=0, dy=0; + window.get_position(out x, out y); + window.get_size(out dx, out dy); + + var ctx = this.context(); + ctx.translate((int)(x-posx + (dx+3)/2), (int)(y-posy + (dy+3)/2)); + window.background.paint_on(ctx); + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an image from the the given filename. + ///////////////////////////////////////////////////////////////////// + + public void load_file(string filename) { + try { + var pixbuf = new Gdk.Pixbuf.from_file(filename); + + if (pixbuf != null) { + this.load_pixbuf(pixbuf); + } else { + warning("Failed to load " + filename + "!"); + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + } + } catch (GLib.Error e) { + message("Error loading image file: %s", e.message); + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an image from the the given filename at a given size. + ///////////////////////////////////////////////////////////////////// + + public void load_file_at_size(string filename, int width, int height) { + try { + var pixbuf = new Gdk.Pixbuf.from_file_at_size(filename, width, height); + + if (pixbuf != null) { + this.load_pixbuf(pixbuf); + } else { + warning("Failed to load " + filename + "!"); + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + } + + } catch (GLib.Error e) { + message("Error loading image file: %s", e.message); + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an image from the the given Gdk.Pixbuf. + ///////////////////////////////////////////////////////////////////// + + public void load_pixbuf(Gdk.Pixbuf pixbuf) { + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height); + + var ctx = this.context(); + Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 1.0, 1.0); + ctx.paint(); + } + + ///////////////////////////////////////////////////////////////////// + /// Paints the image onto the given Cairo.Context + ///////////////////////////////////////////////////////////////////// + + public void paint_on(Cairo.Context ctx, double alpha = 1.0) { + ctx.set_source_surface(this.surface, (int)(-0.5*this.width()-1), (int)(-0.5*this.height()-1)); + if (alpha >= 1.0) ctx.paint(); + else ctx.paint_with_alpha(alpha); + } + + ///////////////////////////////////////////////////////////////////// + /// Converts the image to a Gdk.Pixbuf. + ///////////////////////////////////////////////////////////////////// + + public Gdk.Pixbuf to_pixbuf() { + if (this.surface == null || this.surface.get_data() == null) + return new Gdk.Pixbuf(Gdk.Colorspace.RGB, true, 8, 1, 1); + + var pixbuf = new Gdk.Pixbuf.with_unowned_data(this.surface.get_data(), Gdk.Colorspace.RGB, true, 8, + width(), height(), this.surface.get_stride(), null); + + pixbuf = pixbuf.copy(); + + // funny stuff here --- need to swap Red end Blue because Cairo + // and Gdk are different... + uint8* p = pixbuf.pixels; + for (int i=0; i<width()*height()*4-4; i+=4) { + var tmp = *(p + i); + *(p + i) = *(p + i + 2); + *(p + i + 2) = tmp; + } + + return pixbuf; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns a Cairo.Context for the Image. + ///////////////////////////////////////////////////////////////////// + + public Cairo.Context context() { + return new Cairo.Context(this.surface); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the width of the image in pixels. + ///////////////////////////////////////////////////////////////////// + + public int width() { + if (this.surface != null) + return this.surface.get_width(); + return 0; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the height of the image in pixels. + ///////////////////////////////////////////////////////////////////// + + public int height() { + if (this.surface != null) + return this.surface.get_height(); + return 0; + } +} + +} diff --git a/src/images/renderedText.vala b/src/images/renderedText.vala new file mode 100644 index 0000000..2f4b82f --- /dev/null +++ b/src/images/renderedText.vala @@ -0,0 +1,152 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A class representing string, rendered on an Image. +///////////////////////////////////////////////////////////////////////// + +public class RenderedText : Image { + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new image representation of a string. + ///////////////////////////////////////////////////////////////////// + + public RenderedText(string text, int width, int height, string font, + Color color, double scale) { + + this.render_text(text, width, height, font, color, scale); + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new image representation of a string. This + /// string may contain markup information. + ///////////////////////////////////////////////////////////////////// + + public RenderedText.with_markup(string text, int width, int height, string font, + Color color, double scale) { + + this.render_markup(text, width, height, font, color, scale); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a new transparent image, with text written onto. + ///////////////////////////////////////////////////////////////////// + + public void render_text(string text, int width, int height, string font, + Color color, double scale) { + + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + + if (text != "") { + + var ctx = this.context(); + + // set the color + ctx.set_source_rgb(color.r, color.g, color.g); + + var layout = Pango.cairo_create_layout(ctx); + layout.set_width(Pango.units_from_double(width)); + + var font_description = Pango.FontDescription.from_string(font); + font_description.set_size((int)(font_description.get_size() * scale)); + + layout.set_font_description(font_description); + layout.set_text(text, -1); + + // add newlines at the end of each line, in order to allow ellipsizing + string broken_string = ""; + + for (int i=0; i<layout.get_line_count(); ++i) { + + string next_line = ""; + if (i == layout.get_line_count() -1) + next_line = text.substring(layout.get_line(i).start_index, -1); + else + next_line = text.substring(layout.get_line(i).start_index, layout.get_line(i).length); + + if (broken_string == "") { + broken_string = next_line; + } else if (next_line != "") { + // test whether the addition of a line would cause the height to become too large + string broken_string_tmp = broken_string + "\n" + next_line; + + var layout_tmp = Pango.cairo_create_layout(ctx); + layout_tmp.set_width(Pango.units_from_double(width)); + + layout_tmp.set_font_description(font_description); + + layout_tmp.set_text(broken_string_tmp, -1); + Pango.Rectangle extents; + layout_tmp.get_pixel_extents(null, out extents); + + if (extents.height > height) broken_string = broken_string + next_line; + else broken_string = broken_string_tmp; + } + } + + layout.set_text(broken_string, -1); + + layout.set_ellipsize(Pango.EllipsizeMode.END); + layout.set_alignment(Pango.Alignment.CENTER); + + Pango.Rectangle extents; + layout.get_pixel_extents(null, out extents); + ctx.move_to(0, (int)(0.5*(height - extents.height))); + + Pango.cairo_update_layout(ctx, layout); + Pango.cairo_show_layout(ctx, layout); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a new transparent image, with text written onto. + ///////////////////////////////////////////////////////////////////// + + public void render_markup(string text, int width, int height, string font, + Color color, double scale) { + + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + + var ctx = this.context(); + + // set the color + ctx.set_source_rgb(color.r, color.g, color.g); + + var layout = Pango.cairo_create_layout(ctx); + layout.set_width(Pango.units_from_double(width)); + + var font_description = Pango.FontDescription.from_string(font); + font_description.set_size((int)(font_description.get_size() * scale)); + + layout.set_font_description(font_description); + layout.set_markup(text, -1); + + layout.set_ellipsize(Pango.EllipsizeMode.END); + layout.set_alignment(Pango.Alignment.CENTER); + + Pango.Rectangle extents; + layout.get_pixel_extents(null, out extents); + ctx.move_to(0, (int)(0.5*(height - extents.height))); + + Pango.cairo_update_layout(ctx, layout); + Pango.cairo_show_layout(ctx, layout); + } +} + +} diff --git a/src/images/themedIcon.vala b/src/images/themedIcon.vala new file mode 100644 index 0000000..9f4ca8a --- /dev/null +++ b/src/images/themedIcon.vala @@ -0,0 +1,130 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A class representing a square-shaped icon, themed according to the +/// current theme of Gnome-Pie. +///////////////////////////////////////////////////////////////////////// + +public class ThemedIcon : Image { + + ///////////////////////////////////////////////////////////////////// + /// Paint a slice icon according to the current theme. + ///////////////////////////////////////////////////////////////////// + + public ThemedIcon(string caption, string icon_name, bool active) { + + // get layers for the desired slice type + var layers = active ? Config.global.theme.active_slice_layers : Config.global.theme.inactive_slice_layers; + + // get max size + int size = 1; + foreach (var layer in layers) { + if (layer.image != null && layer.image.width() > size) + size = layer.image.width(); + } + + this.surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, size, size); + + // get size of icon layer + int icon_size = size; + foreach (var layer in layers) { + if (layer.image != null && layer.layer_type == SliceLayer.Type.ICON) + icon_size = layer.image.width(); + } + + Image icon; + if (icon_name.contains("/")) + icon = new Image.from_file_at_size(icon_name, icon_size, icon_size); + else + icon = new Icon(icon_name, icon_size); + + var color = new Color.from_icon(icon); + var ctx = this.context(); + + ctx.translate(size/2, size/2); + ctx.set_operator(Cairo.Operator.OVER); + + // now render all layers on top of each other + foreach (var layer in layers) { + + if (layer.visibility == SliceLayer.Visibility.ANY || + (Config.global.show_captions == (layer.visibility == SliceLayer.Visibility.WITH_CAPTION))) { + + if (layer.colorize) { + ctx.push_group(); + } + + if (layer.layer_type == SliceLayer.Type.ICON) { + ctx.push_group(); + + ctx.translate(layer.x, layer.y); + layer.image.paint_on(ctx); + + ctx.set_operator(Cairo.Operator.IN); + + if (layer.image.width() != icon_size) { + if (icon_name.contains("/")) + icon = new Image.from_file_at_size(icon_name, layer.image.width(), layer.image.width()); + else + icon = new Icon(icon_name,layer.image.width()); + } + + icon.paint_on(ctx); + ctx.translate(-layer.x, -layer.y); + + ctx.pop_group_to_source(); + ctx.paint(); + ctx.set_operator(Cairo.Operator.OVER); + + } else if (layer.layer_type == SliceLayer.Type.CAPTION) { + Image text = new RenderedText(caption, layer.width, layer.height, layer.font, layer.color, Config.global.global_scale); + ctx.translate(layer.x, layer.y); + text.paint_on(ctx); + ctx.translate(-layer.x, -layer.y); + } else if (layer.layer_type == SliceLayer.Type.FILE) { + ctx.translate(layer.x, layer.y); + layer.image.paint_on(ctx); + ctx.translate(-layer.x, -layer.y); + } + + // colorize the whole layer if neccasary + if (layer.colorize) { + ctx.set_operator(Cairo.Operator.ATOP); + ctx.set_source_rgb(color.r, color.g, color.b); + ctx.paint(); + + ctx.set_operator(Cairo.Operator.OVER); + ctx.pop_group_to_source(); + ctx.paint(); + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the size of the icon in pixels. Greetings to Liskov. + ///////////////////////////////////////////////////////////////////// + + public int size() { + return base.width(); + } +} + +} diff --git a/src/pies/defaultConfig.vala b/src/pies/defaultConfig.vala new file mode 100644 index 0000000..8763a1d --- /dev/null +++ b/src/pies/defaultConfig.vala @@ -0,0 +1,74 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A helper class which creates a user-specific default configuration. +///////////////////////////////////////////////////////////////////////// + +namespace Pies { + + public void create_default_config() { + + // add a pie with playback controls + var multimedia = PieManager.create_persistent_pie(_("Multimedia"), "media-playback-start", new Trigger.from_string("<Control><Alt>m")); + multimedia.add_action(new KeyAction(_("Next Track"), "media-skip-forward", "XF86AudioNext", true)); + multimedia.add_action(new KeyAction(_("Stop"), "media-playback-stop", "XF86AudioStop")); + multimedia.add_action(new KeyAction(_("Previous Track"), "media-skip-backward", "XF86AudioPrev")); + multimedia.add_action(new KeyAction(_("Play/Pause"), "media-playback-start", "XF86AudioPlay")); + + // add a pie with the users default applications + var apps = PieManager.create_persistent_pie(_("Applications"), "applications-accessories", new Trigger.from_string("<Control><Alt>a")); + apps.add_action(ActionRegistry.default_for_mime_type("text/plain")); + apps.add_action(ActionRegistry.default_for_mime_type("audio/ogg")); + apps.add_action(ActionRegistry.default_for_mime_type("video/ogg")); + apps.add_action(ActionRegistry.default_for_mime_type("image/jpg")); + apps.add_action(ActionRegistry.default_for_uri("http")); + apps.add_action(ActionRegistry.default_for_uri("mailto")); + + // add a pie with the users bookmarks and devices + var bookmarks = PieManager.create_persistent_pie(_("Bookmarks"), "user-bookmarks", new Trigger.from_string("<Control><Alt>b")); + bookmarks.add_group(new BookmarkGroup(bookmarks.id)); + bookmarks.add_group(new DevicesGroup(bookmarks.id)); + + // add a pie with session controls + var session = PieManager.create_persistent_pie(_("Session"), "system-log-out", new Trigger.from_string("<Control><Alt>q")); + session.add_group(new SessionGroup(session.id)); + + // add a pie with a main menu + var menu = PieManager.create_persistent_pie(_("Main Menu"), "start-here", new Trigger.from_string("<Control><Alt>space")); + menu.add_group(new MenuGroup(menu.id)); + + // add a pie with window controls + var window = PieManager.create_persistent_pie(_("Window"), "preferences-system-windows", new Trigger.from_string("<Control><Alt>w")); + window.add_action(new KeyAction(_("Scale"), "go-top", "<Control><Alt>s")); + window.add_action(new KeyAction(_("Minimize"), "go-bottom", "<Alt>F9", true)); + window.add_action(new KeyAction(_("Close"), "window-close", "<Alt>F4")); + window.add_action(new KeyAction(_("Maximize"), "view-fullscreen", "<Alt>F10")); + window.add_action(new KeyAction(_("Restore"), "view-restore", "<Alt>F5")); + + // add a pie with window list group + var alt_tab = PieManager.create_persistent_pie("Alt Tab", "dock", new Trigger.from_string("<Control><Alt>T")); + alt_tab.add_group(new WindowListGroup(alt_tab.id)); + + // save the configuration to file + Pies.save(); + } +} + +} diff --git a/src/pies/load.vala b/src/pies/load.vala new file mode 100644 index 0000000..0dfb423 --- /dev/null +++ b/src/pies/load.vala @@ -0,0 +1,208 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A helper method which loads pies according to the configuration file. +///////////////////////////////////////////////////////////////////////// + +namespace Pies { + + ///////////////////////////////////////////////////////////////////// + /// Loads all Pies from the pies.conf file. + ///////////////////////////////////////////////////////////////////// + + public void load() { + // check whether the config file exists + if (!GLib.File.new_for_path(Paths.pie_config).query_exists()) { + message("Creating new configuration file in \"" + Paths.pie_config + "\"."); + Pies.create_default_config(); + return; + } + + message("Loading Pies from \"" + Paths.pie_config + "\"."); + + // load the settings file + Xml.Parser.init(); + Xml.Doc* piesXML = Xml.Parser.parse_file(Paths.pie_config); + + if (piesXML != null) { + // begin parsing at the root element + Xml.Node* root = piesXML->get_root_element(); + if (root != null) { + for (Xml.Node* node = root->children; node != null; node = node->next) { + if (node->type == Xml.ElementType.ELEMENT_NODE) { + string node_name = node->name.down(); + switch (node_name) { + case "pie": + parse_pie(node); + break; + default: + warning("Invalid child element <" + node_name + "> in <pies> element pies.conf!"); + break; + } + } + } + } else { + warning("Error loading pies: pies.conf is empty! The cake is a lie..."); + } + + delete piesXML; + + } else { + warning("Error loading pies: pies.conf not found! The cake is a lie..."); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a <pie> element from the pies.conf file. + ///////////////////////////////////////////////////////////////////// + + private static void parse_pie(Xml.Node* node) { + string hotkey = ""; + string name = ""; + string icon = ""; + string id = ""; + int quickaction = -1; + + // parse all attributes of this node + for (Xml.Attr* attribute = node->properties; attribute != null; attribute = attribute->next) { + string attr_name = attribute->name.down(); + string attr_content = attribute->children->content; + + switch (attr_name) { + case "hotkey": + hotkey = attr_content; + break; + case "quickaction": + quickaction = int.parse(attr_content); + break; + case "name": + name = attr_content; + break; + case "icon": + icon = attr_content; + break; + case "id": + id = attr_content; + break; + default: + warning("Invalid setting \"" + attr_name + "\" in pies.conf!"); + break; + } + } + + if (name == "") { + warning("Invalid <pie> element in pies.conf: No name specified!"); + return; + } + + // add a new Pie with the loaded properties + var pie = PieManager.create_persistent_pie(name, icon, new Trigger.from_string(hotkey), id); + + // and parse all child elements + for (Xml.Node* slice = node->children; slice != null; slice = slice->next) { + if (slice->type == Xml.ElementType.ELEMENT_NODE) { + string node_name = slice->name.down(); + switch (node_name) { + case "slice": + parse_slice(slice, pie); + break; + case "group": + parse_group(slice, pie); + break; + default: + warning("Invalid child element <" + node_name + "> in <pie> element in pies.conf!"); + break; + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a <slice> element from the pies.conf file. + ///////////////////////////////////////////////////////////////////// + + private static void parse_slice(Xml.Node* slice, Pie pie) { + string name=""; + string icon=""; + string command=""; + string type=""; + bool quickaction = false; + + // parse all attributes of this node + for (Xml.Attr* attribute = slice->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 "icon": + icon = attr_content; + break; + case "command": + command = attr_content; + break; + case "type": + type = attr_content; + break; + case "quickaction": + quickaction = bool.parse(attr_content); + break; + default: + warning("Invalid attribute \"" + attr_name + "\" in <slice> element in pies.conf!"); + break; + } + } + + // create a new Action according to the loaded type + Action action = ActionRegistry.create_action(type, name, icon, command, quickaction); + + if (action != null) pie.add_action(action); + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a <group> element from the pies.conf file. + ///////////////////////////////////////////////////////////////////// + + private static void parse_group(Xml.Node* slice, Pie pie) { + string type=""; + + // parse all attributes of this node + for (Xml.Attr* attribute = slice->properties; attribute != null; attribute = attribute->next) { + string attr_name = attribute->name.down(); + string attr_content = attribute->children->content; + + if (attr_name == "type") { + type = attr_content; + break; + } + } + + ActionGroup group = GroupRegistry.create_group(type, pie.id); + group.on_load(slice); + + if (group != null) pie.add_group(group); + } +} + +} diff --git a/src/pies/pie.vala b/src/pies/pie.vala new file mode 100644 index 0000000..1699ada --- /dev/null +++ b/src/pies/pie.vala @@ -0,0 +1,122 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class stores information on a pie. A pie consists of a name, an +/// icon name and an unique ID. Furthermore it has an arbitrary amount +/// of ActionGroups storing Actions. +///////////////////////////////////////////////////////////////////////// + +public class Pie : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The name of this Pie. It has not to be unique. + ///////////////////////////////////////////////////////////////////// + + public string name { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// The name of the icon to be used for this Pie. It should exist in + /// the users current icon theme, else a standard icon will be used. + ///////////////////////////////////////////////////////////////////// + + public string icon { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// The ID of this Pie. It has to be unique among all Pies. This ID + /// consists of three digits when the Pie was created by the user, + /// of four digits when it was created dynamically by another class, + /// for example by an ActionGroup. + ///////////////////////////////////////////////////////////////////// + + public string id { get; construct; } + + ///////////////////////////////////////////////////////////////////// + /// Stores all ActionGroups of this Pie. + ///////////////////////////////////////////////////////////////////// + + public Gee.ArrayList<ActionGroup?> action_groups { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all given members. + ///////////////////////////////////////////////////////////////////// + + public Pie(string id, string name, string icon) { + GLib.Object(id: id, name: name, icon:icon); + + this.action_groups = new Gee.ArrayList<ActionGroup?>(); + } + + ///////////////////////////////////////////////////////////////////// + /// Should be called when this Pie is deleted, in order to clean up + /// stuff created by contained ActionGroups. + ///////////////////////////////////////////////////////////////////// + + public virtual void on_remove() { + foreach (var action_group in action_groups) + action_group.on_remove(); + } + + ///////////////////////////////////////////////////////////////////// + /// Adds an Action to this Pie. + ///////////////////////////////////////////////////////////////////// + + public void add_action(Action action, int at_position = -1) { + var group = new ActionGroup(this.id); + group.add_action(action); + this.add_group(group, at_position); + } + + ///////////////////////////////////////////////////////////////////// + /// Adds an ActionGroup to this Pie. + ///////////////////////////////////////////////////////////////////// + + public void add_group(ActionGroup group, int at_position = -1) { + if (group.has_quickaction()) { + foreach (var action_group in action_groups) + action_group.disable_quickactions(); + } + + if (at_position < 0 || at_position >= this.action_groups.size) + this.action_groups.add(group); + else + this.action_groups.insert(at_position, group); + } + + public void remove_group(int index) { + if (this.action_groups.size > index) + this.action_groups.remove_at(index); + } + + public void move_group(int from, int to) { + if (this.action_groups.size > from && this.action_groups.size > to) { + var tmp = this.action_groups[from]; + this.remove_group(from); + this.add_group(tmp, to); + } + } + + public void update_group(ActionGroup group, int index) { + if (this.action_groups.size > index) + this.action_groups.set(index, group); + } +} + +} + diff --git a/src/pies/pieManager.vala b/src/pies/pieManager.vala new file mode 100644 index 0000000..d2cc837 --- /dev/null +++ b/src/pies/pieManager.vala @@ -0,0 +1,347 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A static class which stores all Pies. It can be used to add, delete +/// and open Pies. +///////////////////////////////////////////////////////////////////////// + +public class PieManager : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// A map of all Pies. It contains both, dynamic and persistent Pies. + /// They are associated to their ID's. + ///////////////////////////////////////////////////////////////////// + + public static Gee.HashMap<string, Pie?> all_pies { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Stores all PieWindows which are currently opened. Should be + /// rarely more than two... + ///////////////////////////////////////////////////////////////////// + + public static Gee.HashSet<PieWindow?> opened_windows { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Stores all global hotkeys. + ///////////////////////////////////////////////////////////////////// + + private static BindingManager bindings; + + ///////////////////////////////////////////////////////////////////// + /// True, if any pie has the current focus. If it is closing this + /// will be false already. + ///////////////////////////////////////////////////////////////////// + + private static bool a_pie_is_active = false; + + ///////////////////////////////////////////////////////////////////// + /// Storing the position of the last Pie. Used for subpies, which are + /// opened at their parents location. + ///////////////////////////////////////////////////////////////////// + + private static int last_x = 0; + private static int last_y = 0; + + ///////////////////////////////////////////////////////////////////// + /// Initializes all Pies. They are loaded from the pies.conf file. + ///////////////////////////////////////////////////////////////////// + + public static void init() { + all_pies = new Gee.HashMap<string, Pie?>(); + opened_windows = new Gee.HashSet<PieWindow?>(); + bindings = new BindingManager(); + + // load all Pies from th pies.conf file + Pies.load(); + + // open the according pie if it's hotkey is pressed + bindings.on_press.connect((id) => { + open_pie(id); + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Opens the Pie with the given ID, if it exists. + ///////////////////////////////////////////////////////////////////// + + public static void open_pie(string id) { + if (!a_pie_is_active) { + Pie? pie = all_pies[id]; + + if (pie != null) { + + a_pie_is_active = true; + + //change WM_CLASS so launchers can track windows properly + Gdk.set_program_class("gnome-pie-" + id); + + var window = new PieWindow(); + window.load_pie(pie); + + window.on_closed.connect(() => { + opened_windows.remove(window); + if (opened_windows.size == 0) { + Icon.clear_cache(); + } + }); + + window.on_closing.connect(() => { + window.get_center_pos(out last_x, out last_y); + a_pie_is_active = false; + }); + + opened_windows.add(window); + + window.open(); + + //restore default WM_CLASS after window open + Gdk.set_program_class("Gnome-pie"); + + } else { + warning("Failed to open pie with ID \"" + id + "\": ID does not exist!"); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Prints the names of all pies with their IDs. + ///////////////////////////////////////////////////////////////////// + + public static void print_ids() { + foreach(var pie in all_pies.entries) { + if (pie.value.id.length == 3) { + message(pie.value.id + " " + pie.value.name); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the hotkey which the Pie with the given ID is bound to. + ///////////////////////////////////////////////////////////////////// + + public static string get_accelerator_of(string id) { + return bindings.get_accelerator_of(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns a human-readable version of the hotkey which the Pie + /// with the given ID is bound to. + ///////////////////////////////////////////////////////////////////// + + public static string get_accelerator_label_of(string id) { + return bindings.get_accelerator_label_of(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Bind the Pie with the given ID to the given trigger. + ///////////////////////////////////////////////////////////////////// + + public static void bind_trigger(Trigger trigger, string id) { + bindings.unbind(id); + bindings.bind(trigger, id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true if the pie with the given id is in turbo mode. + ///////////////////////////////////////////////////////////////////// + + public static bool get_is_turbo(string id) { + return bindings.get_is_turbo(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true if the pie with the given id opens in the middle of + /// the screen. + ///////////////////////////////////////////////////////////////////// + + public static bool get_is_centered(string id) { + return bindings.get_is_centered(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true if the mouse pointer will be warped to the center of + /// the pie. + ///////////////////////////////////////////////////////////////////// + + public static bool get_is_warp(string id) { + return bindings.get_is_warp(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true if the pie with the given id is auto shaped + ///////////////////////////////////////////////////////////////////// + + public static bool get_is_auto_shape(string id) { + return bindings.get_is_auto_shape(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the prefered pie shape number + ///////////////////////////////////////////////////////////////////// + + public static int get_shape_number(string id) { + return bindings.get_shape_number(id); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the name of the Pie with the given ID. + ///////////////////////////////////////////////////////////////////// + + public static string get_name_of(string id) { + Pie? pie = all_pies[id]; + if (pie == null) return ""; + else return pie.name; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the name ID of the Pie bound to the given Trigger. + /// Returns "" if there is nothing bound to this trigger. + ///////////////////////////////////////////////////////////////////// + + public static string get_assigned_id(Trigger trigger) { + return bindings.get_assigned_id(trigger); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a new Pie which is displayed in the configuration dialog + /// and gets saved. + ///////////////////////////////////////////////////////////////////// + + public static Pie create_persistent_pie(string name, string icon_name, Trigger? hotkey, string? desired_id = null) { + Pie pie = create_pie(name, icon_name, 100, 999, desired_id); + + if (hotkey != null) bindings.bind(hotkey, pie.id); + + create_launcher(pie.id); + + return pie; + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a new Pie which is not displayed in the configuration + /// dialog and is not saved. + ///////////////////////////////////////////////////////////////////// + + public static Pie create_dynamic_pie(string name, string icon_name, string? desired_id = null) { + return create_pie(name, icon_name, 1000, 9999, desired_id); + } + + ///////////////////////////////////////////////////////////////////// + /// Adds a new Pie. Can't be accesd from outer scope. Use + /// create_persistent_pie or create_dynamic_pie instead. + ///////////////////////////////////////////////////////////////////// + + private static Pie create_pie(string name, string icon_name, int min_id, int max_id, string? desired_id = null) { + var random = new GLib.Rand(); + + string final_id; + + if (desired_id == null) + final_id = random.int_range(min_id, max_id).to_string(); + else { + final_id = desired_id; + final_id.canon("0123456789", '_'); + final_id = final_id.replace("_", ""); + + int id = int.parse(final_id); + + if (id < min_id || id > max_id) { + final_id = random.int_range(min_id, max_id).to_string(); + warning("The ID for pie \"" + name + "\" should be in range %u - %u! Using \"" + final_id + "\" instead of \"" + desired_id + "\"...", min_id, max_id); + } + } + + if (all_pies.has_key(final_id)) { + var tmp = final_id; + var id_number = int.parse(final_id) + 1; + if (id_number == max_id+1) id_number = min_id; + final_id = id_number.to_string(); + warning("Trying to add pie \"" + name + "\": ID \"" + tmp + "\" already exists! Using \"" + final_id + "\" instead..."); + return create_pie(name, icon_name, min_id, max_id, final_id); + } + + Pie pie = new Pie(final_id, name, icon_name); + all_pies.set(final_id, pie); + + return pie; + } + + ///////////////////////////////////////////////////////////////////// + /// Removes the Pie with the given ID if it exists. Additionally it + /// unbinds it's global hotkey. + ///////////////////////////////////////////////////////////////////// + + public static void remove_pie(string id) { + if (all_pies.has_key(id)) { + all_pies[id].on_remove(); + all_pies.unset(id); + bindings.unbind(id); + + if (id.length == 3) + remove_launcher(id); + } + else { + warning("Failed to remove pie with ID \"" + id + "\": ID does not exist!"); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a desktop file for which opens the Pie with given ID. + ///////////////////////////////////////////////////////////////////// + + public static void create_launcher(string id) { + if (all_pies.has_key(id)) { + Pie? pie = all_pies[id]; + + string launcher_entry = + "#!/usr/bin/env xdg-open\n" + + "[Desktop Entry]\n" + + "Name=%s\n".printf(pie.name) + + "Exec=%s -o %s\n".printf(Paths.executable, pie.id) + + "Encoding=UTF-8\n" + + "Type=Application\n" + + "Icon=%s\n".printf(pie.icon) + + "StartupWMClass=gnome-pie-%s\n".printf(pie.id); + + // create the launcher file + string launcher = Paths.launchers + "/%s.desktop".printf(pie.id); + + try { + FileUtils.set_contents(launcher, launcher_entry); + FileUtils.chmod(launcher, 0755); + } catch (Error e) { + warning(e.message); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Deletes the desktop file for the Pie with the given ID. + ///////////////////////////////////////////////////////////////////// + + private static void remove_launcher(string id) { + string launcher = Paths.launchers + "/%s.desktop".printf(id); + if (FileUtils.test(launcher, FileTest.EXISTS)) { + FileUtils.remove(launcher); + } + } +} + +} diff --git a/src/pies/save.vala b/src/pies/save.vala new file mode 100644 index 0000000..efb3fb6 --- /dev/null +++ b/src/pies/save.vala @@ -0,0 +1,89 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A helper method which saves pies in a configuration file. +///////////////////////////////////////////////////////////////////////// + +namespace Pies { + + ///////////////////////////////////////////////////////////////////// + /// Saves all Pies of the PieManager to the pies.conf file. + ///////////////////////////////////////////////////////////////////// + + public void save() { + message("Saving Pies to \"" + Paths.pie_config + "\"."); + + // initializes the XML-Writer + var writer = new Xml.TextWriter.filename(Paths.pie_config); + writer.set_indent(true); + writer.start_document("1.0"); + writer.start_element("pies"); + + // iterate through all Pies + foreach (var pie_entry in PieManager.all_pies.entries) { + var pie = pie_entry.value; + + // if it's no dynamically created Pie + if (pie.id.length == 3) { + int slice_count = 0; + + // write all attributes of the Pie + writer.start_element("pie"); + writer.write_attribute("name", pie.name); + writer.write_attribute("id", pie.id); + writer.write_attribute("icon", pie.icon); + writer.write_attribute("hotkey", PieManager.get_accelerator_of(pie.id)); + + // and all of it's Actions + foreach (var group in pie.action_groups) { + // if it's a custom ActionGroup + if (group.get_type().depth() == 2) { + foreach (var action in group.actions) { + writer.start_element("slice"); + writer.write_attribute("type", ActionRegistry.descriptions[action.get_type().name()].id); + if (ActionRegistry.descriptions[action.get_type().name()].icon_name_editable) { + writer.write_attribute("name", action.name); + writer.write_attribute("icon", action.icon); + } + writer.write_attribute("command", action.real_command); + writer.write_attribute("quickAction", action.is_quickaction ? "true" : "false"); + writer.end_element(); + + ++ slice_count; + } + } else { + writer.start_element("group"); + group.on_save(writer); + writer.end_element(); + + slice_count += group.actions.size; + } + } + writer.end_element(); + } + } + writer.end_element(); + writer.end_document(); + } +} + +} diff --git a/src/renderers/centerRenderer.vala b/src/renderers/centerRenderer.vala new file mode 100644 index 0000000..c146216 --- /dev/null +++ b/src/renderers/centerRenderer.vala @@ -0,0 +1,232 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// Renders the center of a Pie. +///////////////////////////////////////////////////////////////////////// + +public class CenterRenderer : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The PieRenderer which owns this CenterRenderer. + ///////////////////////////////////////////////////////////////////// + + private unowned PieRenderer parent; + + ///////////////////////////////////////////////////////////////////// + /// The caption drawn in the center. Changes when the active slice + /// changes. + ///////////////////////////////////////////////////////////////////// + + private unowned Image? caption; + + ///////////////////////////////////////////////////////////////////// + /// The color of the currently active slice. Used to colorize layers. + ///////////////////////////////////////////////////////////////////// + + private Color color; + + ///////////////////////////////////////////////////////////////////// + /// Two AnimatedValues: alpha is for global transparency (when + /// fading in/out), activity is 1.0 if there is an active slice and + /// 0.0 if there is no active slice. + ///////////////////////////////////////////////////////////////////// + + private AnimatedValue activity; + private AnimatedValue alpha; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public CenterRenderer(PieRenderer parent) { + this.parent = parent; + this.activity = new AnimatedValue.linear(0.0, 0.0, Config.global.theme.transition_time); + this.alpha = new AnimatedValue.linear(0.0, 1.0, Config.global.theme.fade_in_time); + this.color = new Color(); + this.caption = null; + } + + ///////////////////////////////////////////////////////////////////// + /// Initiates the fade-out animation by resetting the targets of the + /// AnimatedValues to 0.0. + ///////////////////////////////////////////////////////////////////// + + public void fade_out() { + this.activity.reset_target(0.0, Config.global.theme.fade_out_time); + this.alpha.reset_target(0.0, Config.global.theme.fade_out_time); + } + + ///////////////////////////////////////////////////////////////////// + /// Should be called if the active slice of the PieRenderer changes. + /// The members activity, caption and color are set accordingly. + ///////////////////////////////////////////////////////////////////// + + public void set_active_slice(SliceRenderer? active_slice) { + if (active_slice == null) { + this.activity.reset_target(0.0, Config.global.theme.transition_time); + } else { + this.activity.reset_target(1.0, Config.global.theme.transition_time); + this.caption = active_slice.caption; + this.color = active_slice.color; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Draws all center layers and the caption. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx, double angle, int slice_track) { + // get all center_layers + var layers = Config.global.theme.center_layers; + + // update the AnimatedValues + this.activity.update(frame_time); + this.alpha.update(frame_time); + + // draw each layer + foreach (var layer in layers) { + ctx.save(); + + // calculate all values needed for animation/drawing + double max_scale = layer.active_scale*this.activity.val + + layer.inactive_scale*(1.0-this.activity.val); + double max_alpha = layer.active_alpha*this.activity.val + + layer.inactive_alpha*(1.0-this.activity.val); + double colorize = ((layer.active_colorize == true) ? this.activity.val : 0.0) + + ((layer.inactive_colorize == true) ? 1.0 - this.activity.val : 0.0); + double max_rotation_speed = layer.active_rotation_speed*this.activity.val + + layer.inactive_rotation_speed*(1.0-this.activity.val); + CenterLayer.RotationMode rotation_mode = ((this.activity.val > 0.5) ? + layer.active_rotation_mode : layer.inactive_rotation_mode); + + double direction = 0; + + if (rotation_mode == CenterLayer.RotationMode.TO_MOUSE) { + direction = angle; + + } else if (rotation_mode == CenterLayer.RotationMode.TO_ACTIVE) { + double slice_angle = parent.total_slice_count > 0 ? 2*PI/parent.total_slice_count : 0; + direction = (int)((angle+0.5*slice_angle) / (slice_angle))*slice_angle; + + } else if (rotation_mode == CenterLayer.RotationMode.TO_SECOND) { + var now = new DateTime.now_local(); + direction = 2*PI*(now.get_second()+60-15)/60; + + } else if (rotation_mode == CenterLayer.RotationMode.TO_MINUTE) { + var now = new DateTime.now_local(); + direction = 2*PI*(now.get_minute()+60-15)/60; + + } else if (rotation_mode == CenterLayer.RotationMode.TO_HOUR_24) { + var now = new DateTime.now_local(); + direction = 2*PI*(now.get_hour()+24-6)/24 + 2*PI*(now.get_minute())/(60*24); + + } else if (rotation_mode == CenterLayer.RotationMode.TO_HOUR_12) { + var now = new DateTime.now_local(); + direction = 2*PI*(now.get_hour()+12-3)/12 + 2*PI*(now.get_minute())/(60*12); + } + + if (rotation_mode == CenterLayer.RotationMode.AUTO) { + layer.rotation += max_rotation_speed*frame_time; + } else { + direction = Math.fmod(direction, 2*PI); + double diff = direction-layer.rotation; + double smoothy = fabs(diff) < 0.9 ? fabs(diff) + 0.1 : 1.0; + double step = max_rotation_speed*frame_time*smoothy; + + if (fabs(diff) <= step || fabs(diff) >= 2.0*PI - step) + layer.rotation = direction; + else { + if ((diff > 0 && diff < PI) || diff < -PI) layer.rotation += step; + else layer.rotation -= step; + } + } + + layer.rotation = fmod(layer.rotation+2*PI, 2*PI); + + if (colorize > 0.0) ctx.push_group(); + + // transform the context + ctx.rotate(layer.rotation); + ctx.scale(max_scale, max_scale); + + // paint the layer + layer.image.paint_on(ctx, this.alpha.val*max_alpha); + + // colorize it, if necessary + if (colorize > 0.0) { + ctx.set_operator(Cairo.Operator.ATOP); + ctx.set_source_rgb(this.color.r, this.color.g, this.color.b); + ctx.paint_with_alpha(colorize); + + ctx.set_operator(Cairo.Operator.OVER); + ctx.pop_group_to_source(); + ctx.paint(); + } + + ctx.restore(); + } + + // draw caption + if (Config.global.theme.caption && caption != null && this.activity.val > 0) { + ctx.save(); + ctx.identity_matrix(); + ctx.translate(this.parent.center_x, (int)(Config.global.theme.caption_position) + this.parent.center_y); + caption.paint_on(ctx, this.activity.val*this.alpha.val); + ctx.restore(); + } + + //scroll pie + if (this.alpha.val > 0.1 + && this.parent.original_visible_slice_count < this.parent.slice_count() + && this.parent.original_visible_slice_count > 0) { + int np= (this.parent.slice_count()+this.parent.original_visible_slice_count -1)/this.parent.original_visible_slice_count; + int cp= this.parent.first_slice_idx / this.parent.original_visible_slice_count; + int r= 8; //circle radious + int dy= 20; //distance between circles + double a= 0.8 * this.alpha.val; + int dx= (int)Config.global.theme.center_radius + r + 10; + if (this.parent.center_x + dx > this.parent.size_w) + dx= -dx; //no right side, put scroll in the left size + ctx.save(); + ctx.identity_matrix(); + ctx.translate(this.parent.center_x + dx, this.parent.center_y - (np-1)*dy/2); + for (int i=0; i<np; i++) { + ctx.arc( 0, 0, r, 0, 2*PI ); + if (i == cp){ + ctx.set_source_rgba(0.3,0.3,0.3, a); //light gray stroke + ctx.stroke_preserve(); + ctx.set_source_rgba(1,1,1, a); //white fill + ctx.fill(); //current + } else { + ctx.set_source_rgba(1,1,1, a); //white stroke + ctx.stroke_preserve(); + ctx.set_source_rgba(0.3,0.3,0.3, a/4); //light gray fill + ctx.fill(); //current + } + ctx.translate(0, dy); + } + ctx.restore(); + } + } +} + +} diff --git a/src/renderers/pieRenderer.vala b/src/renderers/pieRenderer.vala new file mode 100644 index 0000000..1ff2b3e --- /dev/null +++ b/src/renderers/pieRenderer.vala @@ -0,0 +1,890 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class renders a Pie. In order to accomplish that, it owns a +/// CenterRenderer and some SliceRenderers. +///////////////////////////////////////////////////////////////////////// + +public class PieRenderer : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The index of the slice used for quick action. (The action which + /// gets executed when the user clicks on the middle of the pie) + ///////////////////////////////////////////////////////////////////// + + public int quickaction { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// The index of the currently active slice. + ///////////////////////////////////////////////////////////////////// + + public int active_slice { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// True, if the hot keys are currently displayed. + ///////////////////////////////////////////////////////////////////// + + public bool show_hotkeys { get; set; } + + ///////////////////////////////////////////////////////////////////// + /// The width and height of the Pie in pixels. + ///////////////////////////////////////////////////////////////////// + + public int size_w { get; private set; } + public int size_h { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Center position relative to window top-left corner + ///////////////////////////////////////////////////////////////////// + + public int center_x { get; private set; } + public int center_y { get; private set; } + + + //////////////////////////////////////////////////////////////////// + /// Possible show pie modes. + /// FULL_PIE: Show the pie as a complete circle. + /// HPIE_LEFT: Eat half pie so it can be shown at the left of the screen. + /// HPIE_RIGHT: Eat half pie so it can be shown at the right of the screen. + /// HPIE_TOP: Eat half pie so it can be shown at the top of the screen. + /// HPIE_BOTTOM: Eat half pie so it can be shown at the bottom of the screen. + /// CPIE_TOP_LEFT: Eat 3/4 pie so it can be shown at the top-left corner. + /// CPIE_TOP_RIGHT: Eat 3/4 pie so it can be shown at the top-right corner. + /// CPIE_BOT_LEFT: Eat 3/4 pie so it can be shown at the bottom-left corner. + /// CPIE_BOT_RIGHT: Eat 3/4 pie so it can be shown at the bottom-right corner. + ///////////////////////////////////////////////////////////////////// + + public enum ShowPieMode { + FULL_PIE, + HPIE_LEFT, HPIE_RIGHT, HPIE_TOP, HPIE_BOTTOM, + CPIE_TOP_LEFT, CPIE_TOP_RIGHT, CPIE_BOT_LEFT, CPIE_BOT_RIGHT} + + ///////////////////////////////////////////////////////////////////// + /// Show pie mode: full, half-circle, corner + ///////////////////////////////////////////////////////////////////// + + public ShowPieMode pie_show_mode { get; private set; default= ShowPieMode.FULL_PIE; } + + ///////////////////////////////////////////////////////////////////// + /// Number of visible slices + ///////////////////////////////////////////////////////////////////// + + public int visible_slice_count { get; private set; } + + public int original_visible_slice_count { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Number of slices in full pie (visible or not) + ///////////////////////////////////////////////////////////////////// + + public int total_slice_count { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Maximun number of visible slices in a full pie + ///////////////////////////////////////////////////////////////////// + + public int max_visible_slices { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// The index of the first visible slice + ///////////////////////////////////////////////////////////////////// + + public int first_slice_idx { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Angular position of the first visible slice + ///////////////////////////////////////////////////////////////////// + + public double first_slice_angle { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Index of the slice where to go when up/down/left/right key is pressed + /// or -1 if that side of the pie was eaten + ///////////////////////////////////////////////////////////////////// + + public int up_slice_idx { get; private set; } + public int down_slice_idx { get; private set; } + public int left_slice_idx { get; private set; } + public int right_slice_idx { get; private set; } + + + ///////////////////////////////////////////////////////////////////// + /// The ID of the currently loaded Pie. + ///////////////////////////////////////////////////////////////////// + + public string id { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// True if the pie is currently navigated with the keyboard. This is + /// set to false as soon as the mouse moves. + ///////////////////////////////////////////////////////////////////// + + public bool key_board_control { get; set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// All SliceRenderers used to draw this Pie. + ///////////////////////////////////////////////////////////////////// + + private Gee.ArrayList<SliceRenderer?> slices; + + ///////////////////////////////////////////////////////////////////// + /// The renderer for the center of this pie. + ///////////////////////////////////////////////////////////////////// + + private CenterRenderer center; + + ///////////////////////////////////////////////////////////////////// + /// Maximum distance from the center that activates the slices + ///////////////////////////////////////////////////////////////////// + private int activation_range; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes members. + ///////////////////////////////////////////////////////////////////// + + public PieRenderer() { + this.slices = new Gee.ArrayList<SliceRenderer?>(); + this.center = new CenterRenderer(this); + this.quickaction = -1; + this.active_slice = -2; + this.size_w = 0; + this.size_h = 0; + this.activation_range= 300; + + this.max_visible_slices= Config.global.max_visible_slices; + + set_show_mode(ShowPieMode.FULL_PIE); + } + + + private void get_mouse_and_screen(out int mousex, out int mousey, out int screenx, out int screeny) { + // get the mouse position and screen resolution + double x = 0.0; + double y = 0.0; + + var display = Gdk.Display.get_default(); + var manager = display.get_device_manager(); + GLib.List<weak Gdk.Device?> list = manager.list_devices(Gdk.DeviceType.MASTER); + + foreach(var device in list) { + if (device.input_source != Gdk.InputSource.KEYBOARD) { + Gdk.Screen screen; + device.get_position( out screen, out x, out y ); + } + } + mousex= (int) x; + mousey= (int) y; + screenx= Gdk.Screen.width(); + screeny= Gdk.Screen.height(); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads a Pie. All members are initialized accordingly. + ///////////////////////////////////////////////////////////////////// + + public void load_pie(Pie pie) { + this.slices.clear(); + + this.id = pie.id; + + int count = 0; + foreach (var group in pie.action_groups) { + foreach (var action in group.actions) { + var renderer = new SliceRenderer(this); + this.slices.add(renderer); + renderer.load(action, slices.size-1); + + if (action.is_quickaction) { + this.quickaction = count; + } + + ++count; + } + } + + this.select_by_index(this.quickaction); + + + ShowPieMode showpie= ShowPieMode.FULL_PIE; + //set full pie to determine the number of visible slices + set_show_mode(showpie); + + int sz0= (int)fmax(2*Config.global.theme.radius + 2*Config.global.theme.visible_slice_radius*Config.global.theme.max_zoom, + 2*Config.global.theme.center_radius); + + int sz= sz0; + // increase size if there are many slices + if (this.total_slice_count > 0) { + sz = (int)fmax(sz0, + (((Config.global.theme.slice_radius + Config.global.theme.slice_gap)/tan(PI/this.total_slice_count)) + + Config.global.theme.visible_slice_radius)*2*Config.global.theme.max_zoom); + } + + + + + // get mouse position and screen resolution + int mouse_x, mouse_y, screen_x, screen_y; + get_mouse_and_screen( out mouse_x, out mouse_y, out screen_x, out screen_y ); + + //reduce the window size if needed to get closer to the actual mouse position + int reduce_szx= 1; + int reduce_szy= 1; + + if (PieManager.get_is_auto_shape(pie.id) && !PieManager.get_is_centered(pie.id)) { + //set the best show mode that put the mouse near the center + if (mouse_x < sz/2) { + if (mouse_y < sz/2) + showpie= ShowPieMode.CPIE_TOP_LEFT; //show 1/4 pie + else if (screen_y > 0 && screen_y-mouse_y < sz/2) + showpie= ShowPieMode.CPIE_BOT_LEFT; //show 1/4 pie + else + showpie= ShowPieMode.HPIE_LEFT; //show 1/2 pie + + } else if (mouse_y < sz/2) { + if (screen_x > 0 && screen_x-mouse_x < sz/2) + showpie= ShowPieMode.CPIE_TOP_RIGHT; //show 1/4 pie + else + showpie= ShowPieMode.HPIE_TOP; //show 1/2 pie + + } else if (screen_x > 0 && screen_x-mouse_x < sz/2) { + if (screen_y > 0 && screen_y-mouse_y < sz/2) + showpie= ShowPieMode.CPIE_BOT_RIGHT; //show 1/4 pie + else + showpie= ShowPieMode.HPIE_RIGHT; //show 1/2 pie + + } else if (screen_y > 0 && screen_y-mouse_y < sz/2) + showpie= ShowPieMode.HPIE_BOTTOM; //show 1/2 pie + + + } else { + //if the pie is centered in the screen, don't reduce the size + if (PieManager.get_is_centered(pie.id)) { + reduce_szx= 0; + reduce_szy= 0; + } + + //select the configured shape + //convert from radio-buttum number to ShowPieMode enum + switch( PieManager.get_shape_number(pie.id) ) { + case 1: + showpie= ShowPieMode.CPIE_BOT_RIGHT; + if (screen_x-mouse_x > sz/2) + reduce_szx= 0; //keep full width + if (screen_y-mouse_y > sz/2) + reduce_szy= 0; //keep full height + break; + case 2: + showpie= ShowPieMode.HPIE_RIGHT; + if (screen_x-mouse_x > sz/2) + reduce_szx= 0; //keep full width + break; + case 3: + showpie= ShowPieMode.CPIE_TOP_RIGHT; + if (screen_x-mouse_x > sz/2) + reduce_szx= 0; //keep full width + if (mouse_y > sz/2) + reduce_szy= 0; //keep full height + break; + case 4: + showpie= ShowPieMode.HPIE_BOTTOM; + if (screen_y-mouse_y > sz/2) + reduce_szy= 0; //keep full height + break; + case 6: + showpie= ShowPieMode.HPIE_TOP; + if (mouse_y > sz/2) + reduce_szy= 0; //keep full height + break; + case 7: + showpie= ShowPieMode.CPIE_BOT_LEFT; + if (mouse_x > sz/2) + reduce_szx= 0; //keep full width + if (screen_y-mouse_y > sz/2) + reduce_szy= 0; //keep full height + break; + case 8: + showpie= ShowPieMode.HPIE_LEFT; + if (mouse_x > sz/2) + reduce_szx= 0; //keep full width + break; + case 9: + showpie= ShowPieMode.CPIE_TOP_LEFT; + if (mouse_x > sz/2) + reduce_szx= 0; //keep full width + if (mouse_y > sz/2) + reduce_szy= 0; //keep full height + break; + } + } + //set the new show pie mode + set_show_mode(showpie); + + //recalc size + sz = sz0; + if (this.total_slice_count > 0) { + sz = (int)fmax(sz0, + (((Config.global.theme.slice_radius + Config.global.theme.slice_gap)/tan(PI/this.total_slice_count)) + + Config.global.theme.visible_slice_radius)*2*Config.global.theme.max_zoom); + } + //activation_range = normal pie radius + "outer" activation_range + this.activation_range= (int)((double)Config.global.activation_range + sz/(2*Config.global.theme.max_zoom)); + + int szx = 1; //full width + int szy = 1; //full height + switch(this.pie_show_mode) { + //half pie + case ShowPieMode.HPIE_LEFT: + szx = 0; //half width, center to the left + break; + case ShowPieMode.HPIE_RIGHT: + szx = 2; //half width, center to the right + break; + case ShowPieMode.HPIE_TOP: + szy = 0; //half height, center to the top + break; + case ShowPieMode.HPIE_BOTTOM: + szy = 2; //half height, center to the bottom + break; + + //cuarter pie + case ShowPieMode.CPIE_TOP_LEFT: + szx = 0; //half width, center to the left + szy = 0; //half height, center to the top + break; + case ShowPieMode.CPIE_TOP_RIGHT: + szx = 2; //half width, center to the right + szy = 0; //half height, center to the top + break; + case ShowPieMode.CPIE_BOT_LEFT: + szx = 0; //half width, center to the left + szy = 2; //half height, center to the bottom + break; + case ShowPieMode.CPIE_BOT_RIGHT: + szx = 2; //half width, center to the right + szy = 2; //half height, center to the bottom + break; + } + if (reduce_szx == 0) + szx = 1; //don't reduce width + if (reduce_szy == 0) + szy = 1; //don't reduce height + + int rc = (int)Config.global.theme.center_radius; + if (szx == 1 ) { + //full width + this.size_w = sz; + this.center_x = sz/2; //center position + } else { + //half width + this.size_w = sz/2 + rc; + if (szx == 0) { + this.center_x = rc; //center to the left + } else { + this.center_x = this.size_w-rc; //center to the right + } + } + if (szy == 1) { + //full heigth + this.size_h = sz; + this.center_y = sz/2; //center position + } else { + //half heigth + this.size_h = sz/2 + rc; + if (szy == 0) { + this.center_y = rc; //center to the top + } else { + this.center_y = this.size_h-rc; //center to the bottom + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Activates the currently active slice. + ///////////////////////////////////////////////////////////////////// + + public void activate(uint32 time_stamp) { + if (this.active_slice >= this.first_slice_idx + && this.active_slice < this.first_slice_idx+this.visible_slice_count) { + slices[active_slice].activate(time_stamp); + } + + //foreach (var slice in this.slices) + // slice.fade_out(); + for (int i= 0; i < this.visible_slice_count; ++i) { + this.slices[ i+this.first_slice_idx ].fade_out(); + } + + center.fade_out(); + } + + ///////////////////////////////////////////////////////////////////// + /// Asks all renders to fade out. + ///////////////////////////////////////////////////////////////////// + + public void cancel() { + //foreach (var slice in this.slices) + // slice.fade_out(); + for (int i= 0; i < this.visible_slice_count; ++i) { + this.slices[ i+this.first_slice_idx ].fade_out(); + } + + center.fade_out(); + } + + + ///////////////////////////////////////////////////////////////////// + /// Called when the up-key is pressed. Selects the next slice towards + /// the top. + ///////////////////////////////////////////////////////////////////// + + public void select_up() { + move_active_slice(this.up_slice_idx, this.down_slice_idx); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the down-key is pressed. Selects the next slice + /// towards the bottom. + ///////////////////////////////////////////////////////////////////// + + public void select_down() { + move_active_slice(this.down_slice_idx, this.up_slice_idx); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the left-key is pressed. Selects the next slice + /// towards the left. + ///////////////////////////////////////////////////////////////////// + + public void select_left() { + move_active_slice(this.left_slice_idx, this.right_slice_idx); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the right-key is pressed. Selects the next slice + /// towards the right. + ///////////////////////////////////////////////////////////////////// + + public void select_right() { + move_active_slice(this.right_slice_idx, this.left_slice_idx); + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the page_up-key is pressed. Selects the next + /// group of slices. + ///////////////////////////////////////////////////////////////////// + + public void select_nextpage() { + if (this.first_slice_idx+this.visible_slice_count < slices.size) { + //advance one page + this.first_slice_idx += this.visible_slice_count; + if (this.first_slice_idx+this.visible_slice_count >= slices.size) { + this.visible_slice_count= slices.size - this.first_slice_idx; + } + this.reset_slice_anim(); + this.select_by_index(-1); + calc_key_navigation_pos(); + this.key_board_control = true; + + } else if (this.first_slice_idx > 0) { + //go to first page + this.first_slice_idx= 0; + this.reset_slice_anim(); + //recover the original value + this.visible_slice_count= this.original_visible_slice_count; + this.reset_slice_anim(); + this.select_by_index(-1); + calc_key_navigation_pos(); + this.key_board_control = true; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the page_down-key is pressed. Selects the previous + /// group of slices. + ///////////////////////////////////////////////////////////////////// + + public void select_prevpage() { + if (this.first_slice_idx > 0) { + //go back one page + //recover the original value + this.visible_slice_count= this.original_visible_slice_count; + this.first_slice_idx -= this.visible_slice_count; + if (this.first_slice_idx < 0) { + this.first_slice_idx= 0; + } + this.reset_slice_anim(); + this.select_by_index(-1); + calc_key_navigation_pos(); + this.key_board_control = true; + + } else if (this.visible_slice_count < slices.size) { + //go to last page + int n= slices.size % this.original_visible_slice_count; + if (n == 0) + //all pages have the same number of slices + this.visible_slice_count= this.original_visible_slice_count; + else + //last page has less slices than previous + this.visible_slice_count= n; + this.first_slice_idx= slices.size - this.visible_slice_count; + this.reset_slice_anim(); + this.select_by_index(-1); + calc_key_navigation_pos(); + this.key_board_control = true; + } + } + + private void reset_slice_anim() { + //reset animation values in all the new visible slices + for (int i= 0; i < this.visible_slice_count; ++i) + this.slices[ i+this.first_slice_idx ].reset_anim(); + } + + ///////////////////////////////////////////////////////////////////// + /// Selects a slice based on a search string. + ///////////////////////////////////////////////////////////////////// + + public void select_by_string(string search) { + float max_similarity = 0; + int index = -1; + + for (int i=0; i<this.visible_slice_count; ++i) { + float similarity = 0; + int cur_pos = 0; + var name = slices[this.first_slice_idx+i].action.name.down(); + + for (int j=0; j<search.length; ++j) { + int next_pos = name.index_of(search.substring(j, 1), cur_pos); + + if (next_pos != -1) { + cur_pos = next_pos; + similarity += (float)(name.length-next_pos)/name.length + 2; + } + } + + if (similarity > max_similarity) { + index = this.first_slice_idx+i; + max_similarity = similarity; + } + } + + if (index >= 0 && index < slice_count()) { + key_board_control = true; + select_by_index(index); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the amount of slices in this pie. + ///////////////////////////////////////////////////////////////////// + + public int slice_count() { + return slices.size; + } + + ///////////////////////////////////////////////////////////////////// + /// Draws the entire pie. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx, int mouse_x, int mouse_y) { + if (this.size_w > 0) { + double distance = sqrt(mouse_x*mouse_x + mouse_y*mouse_y); + double angle = 0.0; + int slice_track= 0; + + if (this.key_board_control) { + int n= this.active_slice - this.first_slice_idx; + angle = 2.0*PI*n/(double)this.total_slice_count + this.first_slice_angle; + slice_track= 1; + } else { + + if (distance > 0) { + angle = acos(mouse_x/distance); + if (mouse_y < 0) + angle = 2*PI - angle; + } + + int next_active_slice = this.active_slice; + + if (distance < Config.global.theme.active_radius + && this.quickaction >= this.first_slice_idx + && this.quickaction < this.first_slice_idx+this.visible_slice_count) { + + next_active_slice = this.quickaction; + int n= this.quickaction - this.first_slice_idx; + angle = 2.0*PI*n/(double)this.total_slice_count + this.first_slice_angle; + + } else if (distance > Config.global.theme.active_radius && this.total_slice_count > 0 + && distance < this.activation_range) { + double a= angle-this.first_slice_angle; + if (a < 0) + a= a + 2*PI; + next_active_slice = (int)(a*this.total_slice_count/(2*PI) + 0.5) % this.total_slice_count; + if (next_active_slice >= this.visible_slice_count) + next_active_slice = -1; + else { + next_active_slice = next_active_slice + this.first_slice_idx; + slice_track= 1; + } + } else { + next_active_slice = -1; + } + + this.select_by_index(next_active_slice); + } + + center.draw(frame_time, ctx, angle, slice_track); + + for (int i= 0; i < this.visible_slice_count; ++i) { + this.slices[ i+this.first_slice_idx ].draw(frame_time, ctx, angle, slice_track); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the user moves the mouse. + ///////////////////////////////////////////////////////////////////// + + public void on_mouse_move() { + this.key_board_control = false; + } + + ///////////////////////////////////////////////////////////////////// + /// Called when the currently active slice changes. + ///////////////////////////////////////////////////////////////////// + + public void select_by_index(int index) { + if (index != this.active_slice) { + if (index >= this.first_slice_idx && index < this.first_slice_idx+this.visible_slice_count) + this.active_slice = index; + else + this.active_slice = -1; + + SliceRenderer? active = (this.active_slice >= 0 && this.active_slice < slices.size) ? + this.slices[this.active_slice] : null; + + center.set_active_slice(active); + + for (int i= 0; i < this.visible_slice_count; ++i) { + this.slices[ i+this.first_slice_idx ].set_active_slice(active); + } + } + } + + private void set_show_mode(ShowPieMode show_mode) { + //The index of the first visible slice + this.first_slice_idx= 0; + //Angular position of the first visible slice + this.first_slice_angle= 0; + + int mult= 1; + switch(show_mode) { + //half pie + case ShowPieMode.HPIE_LEFT: + mult= 2; + this.first_slice_angle= -PI/2; + break; + case ShowPieMode.HPIE_RIGHT: + mult= 2; + this.first_slice_angle= PI/2; + break; + case ShowPieMode.HPIE_TOP: + mult= 2; + break; + case ShowPieMode.HPIE_BOTTOM: + this.first_slice_angle= PI; + mult= 2; + break; + + //cuarter pie + case ShowPieMode.CPIE_TOP_LEFT: + mult= 4; + break; + case ShowPieMode.CPIE_TOP_RIGHT: + this.first_slice_angle= PI/2; + mult= 4; + break; + case ShowPieMode.CPIE_BOT_LEFT: + this.first_slice_angle= -PI/2; + mult= 4; + break; + case ShowPieMode.CPIE_BOT_RIGHT: + this.first_slice_angle= PI; + mult= 4; + break; + + default: //ShowPieMode.FULL_PIE or invalid values + show_mode= ShowPieMode.FULL_PIE; + break; + } + this.pie_show_mode= show_mode; + //limit the number of visible slices + int maxview= this.max_visible_slices / mult; + //Number of visible slices + this.visible_slice_count= (int)fmin(slices.size, maxview); + //Number of slices in full pie (visible or not) + this.total_slice_count= this.visible_slice_count*mult; + if (mult > 1 && slices.size > 1) { + this.total_slice_count -= mult; + } + + //keep a copy of the original value since page up/down change it + original_visible_slice_count= visible_slice_count; + + calc_key_navigation_pos(); + } + + private void calc_key_navigation_pos() { + //calc slices index for keyboard navigation + + int a= this.first_slice_idx; + int b= this.first_slice_idx + this.visible_slice_count/4; + int c= this.first_slice_idx + this.visible_slice_count/2; + int d= this.first_slice_idx + (this.visible_slice_count*3)/4; + int e= this.first_slice_idx + this.visible_slice_count -1; + switch(this.pie_show_mode) { + //half pie + case ShowPieMode.HPIE_LEFT: + this.up_slice_idx= a; + this.right_slice_idx= c; + this.down_slice_idx= e; + this.left_slice_idx= -1; //no left slice, go up instead + break; + case ShowPieMode.HPIE_RIGHT: + this.down_slice_idx= a; + this.left_slice_idx= c; + this.up_slice_idx= e; + this.right_slice_idx= -1; //no right slice, go down instead + break; + case ShowPieMode.HPIE_TOP: + this.right_slice_idx= a; + this.down_slice_idx= c; + this.left_slice_idx= e; + this.up_slice_idx= -1; //no up slice, go left instead + break; + case ShowPieMode.HPIE_BOTTOM: + this.left_slice_idx= a; + this.up_slice_idx= c; + this.right_slice_idx= e; + this.down_slice_idx= -1; //no down slice, go right instead + break; + + //cuarter pie + case ShowPieMode.CPIE_TOP_LEFT: + this.right_slice_idx= a; + this.down_slice_idx= e; + this.up_slice_idx= -1; //no up slice, go right instead + this.left_slice_idx= -1; //no left slice, go down instead + break; + case ShowPieMode.CPIE_TOP_RIGHT: + this.down_slice_idx= a; + this.left_slice_idx= e; + this.up_slice_idx= -1; //no up slice, go left instead + this.right_slice_idx= -1; //no righ slice, go down instead + break; + case ShowPieMode.CPIE_BOT_LEFT: + this.up_slice_idx= a; + this.right_slice_idx= e; + this.down_slice_idx= -1; //no down slice, go right instead + this.left_slice_idx= -1; //no left slice, go up instead + break; + case ShowPieMode.CPIE_BOT_RIGHT: + this.left_slice_idx= a; + this.up_slice_idx= e; + this.down_slice_idx= -1; //no down slice, go left instead + this.right_slice_idx= -1; //no right slice, go up instead + break; + + default: //ShowPieMode.FULL_PIE or invalid values + this.right_slice_idx= a; + this.down_slice_idx= b; + this.left_slice_idx= c; + this.up_slice_idx= d; + break; + } + } + + + ///////////////////////////////////////////////////////////////////// + /// keyboard navigation helper + /// move current position one slice towards the given extreme + ///////////////////////////////////////////////////////////////////// + + private void move_active_slice(int extreme, int other_extreme ) { + int pos= this.active_slice; + + if (pos < 0 || pos == extreme) { + //no actual position or allready at the extreme + pos= extreme; //go to the extreme pos + + } else if (extreme == -1) { + //the extreme was eaten, just go away from the other_extreme + if (pos > other_extreme || other_extreme == 0) { + if (pos < this.visible_slice_count+this.first_slice_idx-1) + pos++; + } else if (pos > this.first_slice_idx) + pos--; + + } else if (other_extreme == -1) { + //the other_extreme was eaten, just get closer to the extreme + if (pos < extreme) + pos++; + else if (pos > extreme) + pos--; + + } else if (pos == other_extreme) { + //both extremes are present + //jump quickly form one extreme to the other + pos= extreme; //go to the extreme pos + + } else { + //both extremes are present + //add or substract 1 to position in a circular manner + if (extreme > other_extreme) { + if (pos > other_extreme && pos < extreme) + //other_extreme < pos < extreme + pos= pos+1; + else + pos= pos-1; + } else { + if (pos > extreme && pos < other_extreme) + //extreme < pos < other_extreme + pos= pos-1; + else + pos= pos+1; + } + + if (pos < this.first_slice_idx) + pos= this.visible_slice_count-1+this.first_slice_idx; + + if (pos >= this.visible_slice_count+this.first_slice_idx) + pos= this.first_slice_idx; + } + + this.select_by_index(pos); + + this.key_board_control = true; + } +} + +} diff --git a/src/renderers/pieWindow.vala b/src/renderers/pieWindow.vala new file mode 100755 index 0000000..5accb15 --- /dev/null +++ b/src/renderers/pieWindow.vala @@ -0,0 +1,500 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// An invisible window. Used to draw Pies onto. +///////////////////////////////////////////////////////////////////////// + +public class PieWindow : Gtk.Window { + + ///////////////////////////////////////////////////////////////////// + /// Signal which gets emitted when the PieWindow is about to close. + ///////////////////////////////////////////////////////////////////// + + public signal void on_closing(); + + ///////////////////////////////////////////////////////////////////// + /// Signal which gets emitted when the PieWindow is closed. + ///////////////////////////////////////////////////////////////////// + + public signal void on_closed(); + + ///////////////////////////////////////////////////////////////////// + /// The background image used for fake transparency if + /// has_compositing is false. + ///////////////////////////////////////////////////////////////////// + + public Image background { get; private set; default=null; } + + ///////////////////////////////////////////////////////////////////// + /// The background image position and size. + ///////////////////////////////////////////////////////////////////// + + private int back_x; + private int back_y; + private int back_sz_x; + private int back_sz_y; + + ///////////////////////////////////////////////////////////////////// + /// Some panels moves the window after it was realized. + /// This value set the maximum allowed panel height or width. + /// (how many pixels the window could be moved in every direction + /// from the screen borders towards the center) + ///////////////////////////////////////////////////////////////////// + + private int panel_sz = 64; + + ///////////////////////////////////////////////////////////////////// + /// This value set the maximum allowed mouse movement in pixels + /// from the capture to the show point in every direction. + ///////////////////////////////////////////////////////////////////// + + private int mouse_move = 30; + + ///////////////////////////////////////////////////////////////////// + /// The owned renderer. + ///////////////////////////////////////////////////////////////////// + + private PieRenderer renderer; + + ///////////////////////////////////////////////////////////////////// + /// True, if the Pie is currently fading out. + ///////////////////////////////////////////////////////////////////// + + private bool closing = false; + private bool closed = false; + + ///////////////////////////////////////////////////////////////////// + /// A timer used for calculating the frame time. + ///////////////////////////////////////////////////////////////////// + + private GLib.Timer timer; + + ///////////////////////////////////////////////////////////////////// + /// True, if the screen supports compositing. + ///////////////////////////////////////////////////////////////////// + + private bool has_compositing = false; + + ///////////////////////////////////////////////////////////////////// + /// When a Pie is opened, pressed buttons are accumulated and + /// matches are searched in all slices. + ///////////////////////////////////////////////////////////////////// + + private string search_string = ""; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, sets up the window. + ///////////////////////////////////////////////////////////////////// + + public PieWindow() { + this.renderer = new PieRenderer(); + + this.set_title("Gnome-Pie"); + this.set_skip_taskbar_hint(true); + this.set_skip_pager_hint(true); + this.set_keep_above(true); + this.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU); + this.set_decorated(false); + this.set_resizable(false); + this.icon_name = "gnome-pie"; + this.set_accept_focus(false); + this.app_paintable = true; + + // check for compositing + if (this.screen.is_composited()) { + this.set_visual(this.screen.get_rgba_visual()); + this.has_compositing = true; + } + + //add_events() call was removed because it causes that gnome-pie sometimes enter + //and infinte loop while processing some mouse-motion events. + //(this was seen in Ubuntu 14.04.2 64/32-bits -Glib 2.19- and in MATE 14.04.2) + // set up event filter + //this.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK | + // Gdk.EventMask.KEY_RELEASE_MASK | + // Gdk.EventMask.KEY_PRESS_MASK | + // Gdk.EventMask.POINTER_MOTION_MASK | + // Gdk.EventMask.SCROLL_MASK ); + + // activate on left click + this.button_release_event.connect ((e) => { + if (e.button == 1 || PieManager.get_is_turbo(this.renderer.id)) this.activate_slice(e.time); + return true; + }); + + // cancel on right click + this.button_press_event.connect ((e) => { + if (e.button == 3) this.cancel(); + return true; + }); + + // remember last pressed key in order to disable key repeat + uint last_key = 0; + uint32 last_time_stamp = 0; + this.key_press_event.connect((e) => { + if (e.keyval != last_key) { + this.handle_key_press(e.keyval, e.time, last_time_stamp, e.str); + last_key = e.keyval; + last_time_stamp = e.time; + } + return true; + }); + + // activate on key release if turbo_mode is enabled + this.key_release_event.connect((e) => { + last_key = 0; + if (PieManager.get_is_turbo(this.renderer.id)) + this.activate_slice(e.time); + else + this.handle_key_release(e.keyval); + return true; + }); + + // notify the renderer of mouse move events + this.motion_notify_event.connect((e) => { + this.renderer.on_mouse_move(); + return true; + }); + + this.show.connect_after(() => { + Gtk.grab_add(this); + FocusGrabber.grab(this.get_window(), true, true, false); + }); + + this.scroll_event.connect((e) => { + if (e.direction == Gdk.ScrollDirection.UP) + this.renderer.select_prevpage(); + + else if (e.direction == Gdk.ScrollDirection.DOWN) + this.renderer.select_nextpage(); + return true; + }); + + // draw the pie on expose + this.draw.connect(this.draw_window); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads a Pie to be rendered. + ///////////////////////////////////////////////////////////////////// + + public void load_pie(Pie pie) { + this.renderer.load_pie(pie); + this.set_window_position(pie); + this.set_size_request(renderer.size_w, renderer.size_h); + } + + ///////////////////////////////////////////////////////////////////// + /// Opens the window. load_pie should have been called before. + ///////////////////////////////////////////////////////////////////// + + public void open() { + this.realize(); + // capture the background image if there is no compositing + if (!this.has_compositing) { + this.get_position(out this.back_x, out this.back_y); + this.get_size(out this.back_sz_x, out this.back_sz_y); + this.back_sz_x++; + this.back_sz_y++; + + int screenx= Gdk.Screen.width(); + int screeny= Gdk.Screen.height(); + + //allow some window movement from the screen borders + //(some panels moves the window after it was realized) + int dx = this.panel_sz - this.back_x; + if (dx > 0) + this.back_sz_x += dx; + dx = this.panel_sz - (screenx - this.back_x - this.back_sz_x +1); + if (dx > 0) { + this.back_sz_x += dx; + this.back_x -= dx; + } + + int dy = this.panel_sz - this.back_y; + if (dy > 0) + this.back_sz_y += dy; + dy = this.panel_sz - (screeny - this.back_y - this.back_sz_y +1); + if (dy > 0) { + this.back_sz_y += dy; + this.back_y -= dy; + } + + //also tolerate some mouse movement + this.back_x -= this.mouse_move; + this.back_sz_x += this.mouse_move*2; + this.back_y -= this.mouse_move; + this.back_sz_y += this.mouse_move*2; + + //make sure we don't go outside the screen + if (this.back_x < 0) { + this.back_sz_x += this.back_x; + this.back_x = 0; + } + if (this.back_y < 0) { + this.back_sz_y += this.back_y; + this.back_y = 0; + } + if (this.back_x + this.back_sz_x > screenx) + this.back_sz_x = screenx - this.back_x; + if (this.back_y + this.back_sz_y > screeny) + this.back_sz_y = screeny - this.back_y; + this.background = new Image.capture_screen(this.back_x, this.back_y, this.back_sz_x, this.back_sz_y); + } + + // capture the input focus + this.show(); + + // start the timer + this.timer = new GLib.Timer(); + this.timer.start(); + this.queue_draw(); + + bool warp_pointer = PieManager.get_is_warp(this.renderer.id); + + // the main draw loop + GLib.Timeout.add((uint)(1000.0/Config.global.refresh_rate), () => { + if (this.closed) + return false; + + if (warp_pointer) { + warp_pointer = false; + int x, y; + this.get_center_pos(out x, out y); + this.set_mouse_position(x, y); + } + + this.queue_draw(); + return this.visible; + }); + } + + ///////////////////////////////////////////////////////////////////// + /// Gets the center position of the window. + ///////////////////////////////////////////////////////////////////// + + public void get_center_pos(out int out_x, out int out_y) { + int x=0, y=0; //width=0, height=0; + this.get_position(out x, out y); + out_x = x + renderer.center_x; + out_y = y + renderer.center_y; + } + + ///////////////////////////////////////////////////////////////////// + /// Gets the absolute position of the mouse pointer. + ///////////////////////////////////////////////////////////////////// + + private void get_mouse_position(out int mx, out int my) { + // get the mouse position + mx = 0; + my = 0; + Gdk.ModifierType mask; + + var display = Gdk.Display.get_default(); + var manager = display.get_device_manager(); + GLib.List<weak Gdk.Device?> list = manager.list_devices(Gdk.DeviceType.MASTER); + + foreach(var device in list) { + if (device.input_source != Gdk.InputSource.KEYBOARD) { + this.get_window().get_device_position(device, out mx, out my, out mask); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the absolute position of the mouse pointer. + ///////////////////////////////////////////////////////////////////// + + private void set_mouse_position(int mx, int my) { + var display = Gdk.Display.get_default(); + var manager = display.get_device_manager(); + GLib.List<weak Gdk.Device?> list = manager.list_devices(Gdk.DeviceType.MASTER); + foreach(var device in list) { + if (device.input_source != Gdk.InputSource.KEYBOARD) { + device.warp(Gdk.Screen.get_default(), mx, my); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Draw the Pie. + ///////////////////////////////////////////////////////////////////// + + private bool draw_window(Cairo.Context ctx) { + // paint the background image if there is no compositing + if (this.has_compositing) { + ctx.set_operator (Cairo.Operator.CLEAR); + ctx.paint(); + ctx.set_operator (Cairo.Operator.OVER); + } else { + //correct the background position if the window was moved + //since the background image was captured + int x, y; + this.get_position(out x, out y); + int dx = this.back_x - x; + int dy = this.back_y - y; + ctx.save(); + ctx.translate(dx, dy); + ctx.set_operator (Cairo.Operator.OVER); + ctx.set_source_surface(background.surface, -1, -1); + ctx.paint(); + ctx.restore(); + } + + // align the context to the center of the PieWindow + ctx.translate(this.renderer.center_x, this.renderer.center_y); + + // get the mouse position + int mouse_x, mouse_y; + get_mouse_position( out mouse_x, out mouse_y ); + + // store the frame time + double frame_time = this.timer.elapsed(); + this.timer.reset(); + + // render the Pie + this.renderer.draw(frame_time, ctx, mouse_x - (int)this.renderer.center_x, + mouse_y - (int)this.renderer.center_y); + + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Activates the currently activate slice. + ///////////////////////////////////////////////////////////////////// + + private void activate_slice(uint32 time_stamp) { + if (!this.closing) { + this.closing = true; + this.on_closing(); + Gtk.grab_remove(this); + FocusGrabber.ungrab(); + + GLib.Timeout.add(10, () => { + this.renderer.activate(time_stamp); + return false; + }); + + GLib.Timeout.add((uint)(Config.global.theme.fade_out_time*1000), () => { + this.closed = true; + this.on_closed(); + this.destroy(); + return false; + }); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Activates no slice and closes the PieWindow. + ///////////////////////////////////////////////////////////////////// + + private void cancel() { + if (!this.closing) { + this.closing = true; + this.on_closing(); + Gtk.grab_remove(this); + FocusGrabber.ungrab(); + this.renderer.cancel(); + + GLib.Timeout.add((uint)(Config.global.theme.fade_out_time*1000), () => { + this.closed = true; + this.on_closed(); + this.destroy(); + return false; + }); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Sets the position of the window to the center of the screen or to + /// the mouse. + ///////////////////////////////////////////////////////////////////// + + private void set_window_position(Pie pie) { + if(PieManager.get_is_centered(pie.id)) this.set_position(Gtk.WindowPosition.CENTER); + else this.set_position(Gtk.WindowPosition.MOUSE); + } + + ///////////////////////////////////////////////////////////////////// + /// Do some useful stuff when keys are pressed. + ///////////////////////////////////////////////////////////////////// + + private void handle_key_press(uint key, uint32 time_stamp, uint32 last_time_stamp, string text) { + if (last_time_stamp + 1000 < time_stamp) { + this.search_string = ""; + } + + if (Gdk.keyval_name(key) == "Escape") this.cancel(); + else if (Gdk.keyval_name(key) == "Return") this.activate_slice(time_stamp); + else if (Gdk.keyval_name(key) == "KP_Enter") this.activate_slice(time_stamp); + else if (!PieManager.get_is_turbo(this.renderer.id)) { + if (Gdk.keyval_name(key) == "Up") this.renderer.select_up(); + else if (Gdk.keyval_name(key) == "Down") this.renderer.select_down(); + else if (Gdk.keyval_name(key) == "Left") this.renderer.select_left(); + else if (Gdk.keyval_name(key) == "Right") this.renderer.select_right(); + else if (Gdk.keyval_name(key) == "Page_Down") this.renderer.select_nextpage(); + else if (Gdk.keyval_name(key) == "Page_Up") this.renderer.select_prevpage(); + else if (Gdk.keyval_name(key) == "Tab") this.renderer.select_nextpage(); + else if (Gdk.keyval_name(key) == "ISO_Left_Tab") this.renderer.select_prevpage(); + else if (Gdk.keyval_name(key) == "Alt_L" && !Config.global.search_by_string) this.renderer.show_hotkeys = true; + else { + + if (Config.global.search_by_string) { + this.search_string += text; + this.renderer.select_by_string(search_string.down()); + + } else { + + int index = -1; + + if (key >= 48 && key <= 57) index = ((int)key - 39)%10; + else if (key >= 97 && key <= 122) index = (int)key - 87; + else if (key >= 65 && key <= 90) index = (int)key - 55; + + if (index >= 0 && index < this.renderer.slice_count()) { + this.renderer.key_board_control = true; + this.renderer.select_by_index(index); + + if (this.renderer.active_slice == index) { + GLib.Timeout.add((uint)(Config.global.theme.transition_time*1000.0), ()=> { + this.activate_slice(time_stamp); + return false; + }); + } + } + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Do some useful stuff when keys are released. + ///////////////////////////////////////////////////////////////////// + + private void handle_key_release(uint key) { + if (!PieManager.get_is_turbo(this.renderer.id)) { + if (Gdk.keyval_name(key) == "Alt_L") this.renderer.show_hotkeys = false; + } + } +} + +} diff --git a/src/renderers/sliceRenderer.vala b/src/renderers/sliceRenderer.vala new file mode 100644 index 0000000..591fbdd --- /dev/null +++ b/src/renderers/sliceRenderer.vala @@ -0,0 +1,295 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// Renders a Slice of a Pie. According to the current theme. +///////////////////////////////////////////////////////////////////////// + +public class SliceRenderer : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Whether this slice is active (hovered) or not. + ///////////////////////////////////////////////////////////////////// + + public bool active {get; private set; default = false;} + + ///////////////////////////////////////////////////////////////////// + /// The Image which should be displayed as center caption when this + /// slice is active. + ///////////////////////////////////////////////////////////////////// + + public Image caption {get; private set;} + + ///////////////////////////////////////////////////////////////////// + /// The color which should be used for colorizing center layers when + /// this slice is active. + ///////////////////////////////////////////////////////////////////// + + public Color color {get; private set;} + + ///////////////////////////////////////////////////////////////////// + /// The Action which is rendered by this SliceRenderer. + ///////////////////////////////////////////////////////////////////// + + public Action action; + + ///////////////////////////////////////////////////////////////////// + /// The two Images used, when this slice is active or not. + ///////////////////////////////////////////////////////////////////// + + private Image active_icon; + private Image inactive_icon; + + ///////////////////////////////////////////////////////////////////// + /// The Image displaying the associated hot key of this slice. + ///////////////////////////////////////////////////////////////////// + + private Image hotkey; + + ///////////////////////////////////////////////////////////////////// + /// The PieRenderer which owns this SliceRenderer. + ///////////////////////////////////////////////////////////////////// + + private unowned PieRenderer parent; + + ///////////////////////////////////////////////////////////////////// + /// The index of this slice in a pie. Clockwise assigned, starting + /// from the right-most slice. + ///////////////////////////////////////////////////////////////////// + + private int position; + + ///////////////////////////////////////////////////////////////////// + /// AnimatedValues needed for a slice. + ///////////////////////////////////////////////////////////////////// + + private AnimatedValue fade; // for transitions from active to inactive + private AnimatedValue scale; // for zoom effect + private AnimatedValue alpha; // for fading in/out + private AnimatedValue fade_rotation; // for fading in/out + private AnimatedValue fade_scale; // for fading in/out + private AnimatedValue wobble; // for organic wobbling + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all AnimatedValues. + ///////////////////////////////////////////////////////////////////// + + public SliceRenderer(PieRenderer parent) { + this.parent = parent; + this.reset_anim(); + } + + ///////////////////////////////////////////////////////////////////// + /// Put all AnimatedValues in their initial values + ///////////////////////////////////////////////////////////////////// + + public void reset_anim() { + this.fade = new AnimatedValue.linear(0.0, 0.0, Config.global.theme.transition_time); + this.wobble = new AnimatedValue.linear(0.0, 0.0, Config.global.theme.transition_time); + this.alpha = new AnimatedValue.linear(0.0, 1.0, Config.global.theme.fade_in_time); + this.scale = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, + 1.0/Config.global.theme.max_zoom, + 1.0/Config.global.theme.max_zoom, + Config.global.theme.transition_time, + Config.global.theme.springiness); + this.fade_scale = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, + Config.global.theme.fade_in_zoom, 1.0, + Config.global.theme.fade_in_time, + Config.global.theme.springiness); + this.fade_rotation = new AnimatedValue.cubic(AnimatedValue.Direction.OUT, + Config.global.theme.fade_in_rotation, 0.0, + Config.global.theme.fade_in_time); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads an Action. All members are initialized accordingly. + ///////////////////////////////////////////////////////////////////// + + public void load(Action action, int position) { + this.position = position; + this.action = action; + + + if (Config.global.theme.caption) + this.caption = new RenderedText(action.name, + Config.global.theme.caption_width, + Config.global.theme.caption_height, + Config.global.theme.caption_font, + Config.global.theme.caption_color, + Config.global.global_scale); + + this.active_icon = new ThemedIcon(action.name, action.icon, true); + this.inactive_icon = new ThemedIcon(action.name, action.icon, false); + + this.color = new Color.from_icon(this.active_icon); + + string hotkey_label = ""; + if (position < 10) { + hotkey_label = "%u".printf((position+1)%10); + } else if (position < 36) { + hotkey_label = "%c".printf((char)(55 + position)); + } + + this.hotkey = new RenderedText(hotkey_label, (int)Config.global.theme.slice_radius*2, + (int)Config.global.theme.slice_radius*2, "sans 20", + new Color(), Config.global.global_scale); + } + + ///////////////////////////////////////////////////////////////////// + /// Activates the Action of this slice. + ///////////////////////////////////////////////////////////////////// + + public void activate(uint32 time_stamp) { + action.activate(time_stamp); + } + + ///////////////////////////////////////////////////////////////////// + /// Initiates the fade-out animation by resetting the targets of the + /// AnimatedValues to 0.0. + ///////////////////////////////////////////////////////////////////// + + public void fade_out() { + this.alpha.reset_target(0.0, Config.global.theme.fade_out_time); + this.fade_scale = new AnimatedValue.cubic(AnimatedValue.Direction.IN, + this.fade_scale.val, + Config.global.theme.fade_out_zoom, + Config.global.theme.fade_out_time, + Config.global.theme.springiness); + this.fade_rotation = new AnimatedValue.cubic(AnimatedValue.Direction.IN, + this.fade_rotation.val, + Config.global.theme.fade_out_rotation, + Config.global.theme.fade_out_time); + } + + ///////////////////////////////////////////////////////////////////// + /// Should be called if the active slice of the PieRenderer changes. + /// The members activity, caption and color are set accordingly. + ///////////////////////////////////////////////////////////////////// + + public void set_active_slice(SliceRenderer? active_slice) { + if (active_slice == this) { + this.fade.reset_target(1.0, Config.global.theme.transition_time); + } else { + this.fade.reset_target(0.0, Config.global.theme.transition_time); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Draws all layers of the slice. + ///////////////////////////////////////////////////////////////////// + + public void draw(double frame_time, Cairo.Context ctx, double angle, int slice_track) { + + // update the AnimatedValues + this.scale.update(frame_time); + this.alpha.update(frame_time); + this.fade.update(frame_time); + this.fade_scale.update(frame_time); + this.fade_rotation.update(frame_time); + this.wobble.update(frame_time); + + double direction = 2.0 * PI * (position-parent.first_slice_idx)/parent.total_slice_count + + parent.first_slice_angle + this.fade_rotation.val; + double max_scale = 1.0/Config.global.theme.max_zoom; + double diff = fabs(angle-direction); + + if (diff > 2 * PI) { + diff = diff - 2 * PI; + } + + if (diff > PI) { + diff = 2 * PI - diff; + } + + + active = ((parent.active_slice >= 0) && (diff < PI/parent.total_slice_count)); + + if (slice_track != 0) { + double wobble = Config.global.theme.wobble*diff/PI*(1-diff/PI); + if ((direction < angle && direction > angle - PI) || direction > PI+angle) { + this.wobble.reset_target(-wobble, Config.global.theme.transition_time*0.5); + } else { + this.wobble.reset_target(wobble, Config.global.theme.transition_time*0.5); + } + } else { + this.wobble.reset_target(0, Config.global.theme.transition_time*0.5); + } + + direction += this.wobble.val; + + if (diff < 2 * PI * Config.global.theme.zoom_range) + max_scale = (Config.global.theme.max_zoom/(diff * (Config.global.theme.max_zoom - 1) + /(2 * PI * Config.global.theme.zoom_range) + 1)) + /Config.global.theme.max_zoom; + + + + max_scale = (slice_track != 0 ? max_scale : 1.0/Config.global.theme.max_zoom); + + if (fabs(this.scale.end - max_scale) > Config.global.theme.max_zoom*0.005) + this.scale.reset_target(max_scale, Config.global.theme.transition_time); + + ctx.save(); + + // distance from the center + double radius = Config.global.theme.radius; + + // increase radius if there are many slices in a pie + if (atan((Config.global.theme.slice_radius+Config.global.theme.slice_gap) + /(radius/Config.global.theme.max_zoom)) > PI/parent.total_slice_count) { + radius = (Config.global.theme.slice_radius+Config.global.theme.slice_gap) + /tan(PI/parent.total_slice_count)*Config.global.theme.max_zoom; + } + + // transform the context + ctx.scale(scale.val*fade_scale.val, scale.val*fade_scale.val); + ctx.translate(cos(direction)*radius, sin(direction)*radius); + + ctx.push_group(); + + ctx.set_operator(Cairo.Operator.ADD); + + // paint the images + if (fade.val > 0.0) active_icon.paint_on(ctx, this.alpha.val*this.fade.val); + if (fade.val < 1.0) inactive_icon.paint_on(ctx, this.alpha.val*(1.0 - fade.val)); + + if (this.parent.show_hotkeys) { + ctx.set_operator(Cairo.Operator.ATOP); + ctx.set_source_rgba(0, 0, 0, 0.5); + ctx.paint(); + } + + ctx.set_operator(Cairo.Operator.OVER); + + + ctx.pop_group_to_source(); + ctx.paint(); + + // draw hotkeys if necassary + if (this.parent.show_hotkeys) { + this.hotkey.paint_on(ctx, 1.0); + } + + ctx.restore(); + } +} + +} diff --git a/src/themes/centerLayer.vala b/src/themes/centerLayer.vala new file mode 100644 index 0000000..34d473e --- /dev/null +++ b/src/themes/centerLayer.vala @@ -0,0 +1,116 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class representing a layer of the center of a pie. Each theme +/// may have plenty of them. +///////////////////////////////////////////////////////////////////////// + +public class CenterLayer : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Possible rotation modes. + /// AUTO: Turns the layer continously. + /// TO_MOUSE: Turns the layer always to the pointer. + /// TO_ACTIVE: Turns the layer to the active slice. + /// TO_HOUR_12: Turns the layer to the position of the current hour. + /// TO_HOUR_24: Turns the layer to the position of the current hour. + /// TO_MINUTE: Turns the layer to the position of the current minute. + /// TO_SECOND: Turns the layer to the position of the current second. + ///////////////////////////////////////////////////////////////////// + + public enum RotationMode {AUTO, TO_MOUSE, TO_ACTIVE, TO_HOUR_12, + TO_HOUR_24, TO_MINUTE, TO_SECOND} + + ///////////////////////////////////////////////////////////////////// + /// Information on the contained image. + ///////////////////////////////////////////////////////////////////// + + public Image image {get; private set;} + public string image_file; + + ///////////////////////////////////////////////////////////////////// + /// Properties for the active state of this layer. + ///////////////////////////////////////////////////////////////////// + + public double active_scale {get; private set;} + public double active_rotation_speed {get; private set;} + public double active_alpha {get; private set;} + public bool active_colorize {get; private set;} + public RotationMode active_rotation_mode {get; private set;} + + ///////////////////////////////////////////////////////////////////// + /// Properties for the inactive state of this layer. + ///////////////////////////////////////////////////////////////////// + + public double inactive_scale {get; private set;} + public double inactive_rotation_speed {get; private set;} + public double inactive_alpha {get; private set;} + public bool inactive_colorize {get; private set;} + public RotationMode inactive_rotation_mode {get; private set;} + + ///////////////////////////////////////////////////////////////////// + /// The current rotation of this layer. TODO: Remove this. + ///////////////////////////////////////////////////////////////////// + + public double rotation {get; set;} + + ///////////////////////////////////////////////////////////////////// + /// Helper variable for image loading. + ///////////////////////////////////////////////////////////////////// + + private int center_radius; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members of the layer. + ///////////////////////////////////////////////////////////////////// + + public CenterLayer(string image_file, int center_radius, double active_scale, double active_rotation_speed, + double active_alpha, bool active_colorize, RotationMode active_rotation_mode, + double inactive_scale, double inactive_rotation_speed, + double inactive_alpha, bool inactive_colorize, RotationMode inactive_rotation_mode) { + + this.image_file = image_file; + this.center_radius = center_radius; + + this.active_scale = active_scale; + this.active_rotation_speed = active_rotation_speed; + this.active_alpha = active_alpha; + this.active_colorize = active_colorize; + this.active_rotation_mode = active_rotation_mode; + + this.inactive_scale = inactive_scale; + this.inactive_rotation_speed = inactive_rotation_speed; + this.inactive_alpha = inactive_alpha; + this.inactive_colorize = inactive_colorize; + this.inactive_rotation_mode = inactive_rotation_mode; + + this.rotation = 0.0; + } + + ///////////////////////////////////////////////////////////////////// + /// Loads the contained image. + ///////////////////////////////////////////////////////////////////// + + public void load_image() { + this.image = new Image.from_file_at_size(image_file, 2*center_radius, 2*center_radius); + } +} + +} diff --git a/src/themes/sliceLayer.vala b/src/themes/sliceLayer.vala new file mode 100644 index 0000000..bd9e98b --- /dev/null +++ b/src/themes/sliceLayer.vala @@ -0,0 +1,105 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class representing a layer of a slice of a pie. Each theme may +/// have plenty of them. +///////////////////////////////////////////////////////////////////////// + +public class SliceLayer : GLib.Object { + + public enum Type { FILE, ICON, CAPTION } + public enum Visibility { ANY, WITH_CAPTION, WITHOUT_CAPTION } + + public Type layer_type { get; private set; } + public Visibility visibility { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Information on the contained image. + ///////////////////////////////////////////////////////////////////// + + public Image image {get; set;} + + + ///////////////////////////////////////////////////////////////////// + /// Properties of this layer. + ///////////////////////////////////////////////////////////////////// + + public string icon_file {get; private set; default="";} + public bool colorize {get; private set; default=false;} + public int icon_size {get; private set; default=1;} + + public string font {get; private set; default="";} + public int width {get; private set; default=0;} + public int height {get; private set; default=0;} + public int x {get; private set; default=0;} + public int y {get; private set; default=0;} + public Color color {get; private set; default=new Color();} + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members of the layer. + ///////////////////////////////////////////////////////////////////// + + public SliceLayer.file(string icon_file, int icon_size, int x, int y, bool colorize, Visibility visibility) { + this.layer_type = Type.FILE; + this.icon_file = icon_file; + this.colorize = colorize; + this.icon_size = icon_size; + this.x = x; + this.y = y; + this.visibility = visibility; + } + + public SliceLayer.icon(string icon_file, int icon_size, int x, int y, bool colorize, Visibility visibility) { + this.layer_type = Type.ICON; + this.icon_file = icon_file; + this.colorize = colorize; + this.icon_size = icon_size; + this.x = x; + this.y = y; + this.visibility = visibility; + } + + public SliceLayer.caption(string font, int width, int height, int x, int y, Color color, bool colorize, Visibility visibility) { + this.layer_type = Type.CAPTION; + this.font = font; + this.width = width; + this.height = height; + this.x = x; + this.y = y; + this.color = color; + this.visibility = visibility; + this.colorize = colorize; + } + + ///////////////////////////////////////////////////////////////////// + /// Loads the contained image. + ///////////////////////////////////////////////////////////////////// + + public void load_image() { + this.image = null; + + if (this.icon_file == "" && this.layer_type == Type.ICON) + this.image = new Image.empty(this.icon_size, this.icon_size, new Color.from_rgb(1, 1, 1)); + else if (this.icon_file != "") + this.image = new Image.from_file_at_size(this.icon_file, this.icon_size, this.icon_size); + } +} + +} diff --git a/src/themes/theme.vala b/src/themes/theme.vala new file mode 100644 index 0000000..ccf38c2 --- /dev/null +++ b/src/themes/theme.vala @@ -0,0 +1,650 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +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<CenterLayer?> center_layers {get; private set;} + public Gee.ArrayList<SliceLayer?> active_slice_layers {get; private set;} + public Gee.ArrayList<SliceLayer?> 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<CenterLayer?>(); + this.active_slice_layers = new Gee.ArrayList<SliceLayer?>(); + this.inactive_slice_layers = new Gee.ArrayList<SliceLayer?>(); + + 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 <theme> 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 <pie> 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 <pie> 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 <pie> element!"); + break; + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a <center> 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 <center> 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 <center> element!"); + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a <slices> 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 <slices> 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 <slices> element!"); + } + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a <center_layer> 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 <center_layer> 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 <center_layer> 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 <center_layer> 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 <slice_layer> 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 <slice_layer> 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 <slice_layer> element!"); + break; + default: + warning("Invalid attribute \"" + attr_name + "\" in <slice_layer> 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 <caption> 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 <caption> element!"); + break; + } + } + + } + +} + +} diff --git a/src/themes/themeImporter.vala b/src/themes/themeImporter.vala new file mode 100644 index 0000000..f110696 --- /dev/null +++ b/src/themes/themeImporter.vala @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class provides functions to check whether an archive contains a +/// valid Gnome-Pie theme. +///////////////////////////////////////////////////////////////////////// + +public class ThemeImporter : ArchiveReader { + + public bool is_valid_theme; + public string theme_name; + + ///////////////////////////////////////////////////////////////////// + /// Returns + ///////////////////////////////////////////////////////////////////// + + public new bool open(string path) { + + this.is_valid_theme = false; + this.theme_name = ""; + + var tmp_reader = new ArchiveReader(); + + if (tmp_reader.open(path)) { + try { + var tmp_dir = GLib.DirUtils.make_tmp("gnomepieXXXXXX"); + if (tmp_reader.extract_to(tmp_dir)) { + var tmp_theme = new Theme(tmp_dir); + if (tmp_theme.load()) { + is_valid_theme = true; + theme_name = tmp_theme.name; + } + } + } catch (Error e) { + warning(e.message); + } + } + + tmp_reader.close(); + + return base.open(path); + } +} + +} diff --git a/src/utilities/animatedValue.vala b/src/utilities/animatedValue.vala new file mode 100644 index 0000000..79be155 --- /dev/null +++ b/src/utilities/animatedValue.vala @@ -0,0 +1,197 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A class which interpolates smoothly between to given values. +/// Duration and interpolation mode can be specified. +///////////////////////////////////////////////////////////////////////// + +public class AnimatedValue : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The direction of the interpolation. + ///////////////////////////////////////////////////////////////////// + + public enum Direction { IN, OUT, IN_OUT, OUT_IN } + + ///////////////////////////////////////////////////////////////////// + /// Type of the interpolation, linear or cubic. + ///////////////////////////////////////////////////////////////////// + + private enum Type { LINEAR, CUBIC } + + ///////////////////////////////////////////////////////////////////// + /// Current value, interpolated. + ///////////////////////////////////////////////////////////////////// + + public double val { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Starting value of the interpolation. + ///////////////////////////////////////////////////////////////////// + + public double start { get; private set; default=0.0; } + + ///////////////////////////////////////////////////////////////////// + /// Final value of the interpolation. + ///////////////////////////////////////////////////////////////////// + + public double end { get; private set; default=0.0; } + + ///////////////////////////////////////////////////////////////////// + /// The current state. In range 0 .. 1 + ///////////////////////////////////////////////////////////////////// + + private double state = 0.0; + + ///////////////////////////////////////////////////////////////////// + /// Duration of the interpolation. Should be in the same unit as + /// taken for the update() method. + ///////////////////////////////////////////////////////////////////// + + private double duration = 0.0; + + ///////////////////////////////////////////////////////////////////// + /// The amount of over-shooting of the cubicly interpolated value. + ///////////////////////////////////////////////////////////////////// + + private double multiplier = 0.0; + + ///////////////////////////////////////////////////////////////////// + /// Type of the interpolation, linear or cubic. + ///////////////////////////////////////////////////////////////////// + + private Type type = Type.LINEAR; + + ///////////////////////////////////////////////////////////////////// + /// The direction of the interpolation. + ///////////////////////////////////////////////////////////////////// + + private Direction direction = Direction.IN; + + ///////////////////////////////////////////////////////////////////// + /// Creates a new linearly interpolated value. + ///////////////////////////////////////////////////////////////////// + + public AnimatedValue.linear(double start, double end, double duration) { + this.val = start; + this.start = start; + this.end = end; + this.duration = duration; + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a new cubicly interpolated value. + ///////////////////////////////////////////////////////////////////// + + public AnimatedValue.cubic(Direction direction, double start, double end, double duration, double multiplier = 0) { + this.val = start; + this.start = start; + this.end = end; + this.duration = duration; + this.direction = direction; + this.type = Type.CUBIC; + this.multiplier = multiplier; + } + + ///////////////////////////////////////////////////////////////////// + /// Resets the final value of the interpolation to a new value. The + /// current state is taken for the beginning from now. + ///////////////////////////////////////////////////////////////////// + + public void reset_target(double end, double duration) { + this.end = end; + this.duration = duration; + this.start = this.val; + + if (duration == 0.0) { + this.val = end; + this.state = 1.0; + } else { + this.state = 0.0; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Updates the interpolated value according to it's type. + ///////////////////////////////////////////////////////////////////// + + public void update(double time) { + this.state += time/this.duration; + + if (this.state < 1) { + + switch (this.type) { + case Type.LINEAR: + this.val = update_linear(); + break; + case Type.CUBIC: + switch (this.direction) { + case Direction.IN: + this.val = update_ease_in(); + return; + case Direction.OUT: + this.val = update_ease_out(); + return; + case Direction.IN_OUT: + this.val = update_ease_in_out(); + return; + case Direction.OUT_IN: + this.val = update_ease_out_in(); + return; + } + break; + } + + } else if (this.val != this.end) { + this.val = this.end; + } + } + + ///////////////////////////////////////////////////////////////////// + /// The following equations are based on Robert Penner's easing + /// equations. See (http://www.robertpenner.com/easing/) and their + /// adaption by Zeh Fernando, Nate Chatellier and Arthur Debert for + /// the Tweener class. See (http://code.google.com/p/tweener/). + ///////////////////////////////////////////////////////////////////// + + private double update_linear(double t = this.state, double s = this.start, double e = this.end) { + return (s + t*(e - s)); + } + + private double update_ease_in(double t = this.state, double s = this.start, double e = this.end) { + return (s + (t*t*((multiplier+1)*t-multiplier))*(e - s)); + } + + private double update_ease_out(double t = this.state, double s = this.start, double e = this.end) { + return (s + ((t-1) * (t-1) * ((multiplier+1)*(t-1)+multiplier) + 1) * (e - s)); + } + + private double update_ease_in_out(double t = this.state, double s = this.start, double e = this.end) { + if (this.state < 0.5) return update_ease_in(t*2, s, e - (e-s)*0.5); + else return update_ease_out(t*2-1, s + (e-s)*0.5, e); + } + + private double update_ease_out_in(double t = this.state, double s = this.start, double e = this.end) { + if (this.state < 0.5) return update_ease_out(t*2, s, e - (e-s)*0.5); + else return update_ease_in(t*2-1, s + (e-s)*0.5, e); + } +} + +} diff --git a/src/utilities/archiveReader.vala b/src/utilities/archiveReader.vala new file mode 100644 index 0000000..16e4541 --- /dev/null +++ b/src/utilities/archiveReader.vala @@ -0,0 +1,123 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class can be used to unpack an archive to a directory. +///////////////////////////////////////////////////////////////////////// + +public class ArchiveReader : GLib.Object { + + private Archive.Read archive; + private Archive.WriteDisk writer; + + ///////////////////////////////////////////////////////////////////// + /// Constructs a new ArchiveReader + ///////////////////////////////////////////////////////////////////// + + public ArchiveReader() { + this.archive = new Archive.Read(); + this.archive.support_format_all(); + this.archive.support_filter_all(); + + this.writer = new Archive.WriteDisk(); + this.writer.set_options( + Archive.ExtractFlags.TIME | + Archive.ExtractFlags.PERM | + Archive.ExtractFlags.ACL | + Archive.ExtractFlags.FFLAGS + ); + this.writer.set_standard_lookup(); + } + + ///////////////////////////////////////////////////////////////////// + /// Call this once after you created the ArchiveReader. Pass the + /// path to the target archive location. + ///////////////////////////////////////////////////////////////////// + + public bool open(string path) { + return this.archive.open_filename(path, 10240) == Archive.Result.OK; + } + + ///////////////////////////////////////////////////////////////////// + /// Extracts all files from the previously opened archive. + ///////////////////////////////////////////////////////////////////// + + public bool extract_to(string directory) { + while (true) { + unowned Archive.Entry entry; + var r = this.archive.next_header(out entry); + + if (r == Archive.Result.EOF) { + break; + } + + if (r != Archive.Result.OK) { + warning(this.archive.error_string()); + return false; + } + + entry.set_pathname(directory + "/" + entry.pathname()); + + r = this.writer.write_header(entry); + + if (r != Archive.Result.OK) { + warning(this.writer.error_string()); + return false; + } + + if (entry.size() > 0) { + while (true) { + size_t offset, size; + void *buff; + + r = this.archive.read_data_block(out buff, out size, out offset); + if (r == Archive.Result.EOF) { + break; + } + + if (r != Archive.Result.OK) { + warning(this.archive.error_string()); + return false; + } + + this.writer.write_data_block(buff, size, offset); + } + } + + r = this.writer.finish_entry(); + + if (r != Archive.Result.OK) { + warning(this.writer.error_string()); + return false; + } + } + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// When all files have been added, close the directory again. + ///////////////////////////////////////////////////////////////////// + + public void close() { + this.archive.close(); + this.writer.close(); + } +} + +} diff --git a/src/utilities/archiveWriter.vala b/src/utilities/archiveWriter.vala new file mode 100644 index 0000000..92bd31b --- /dev/null +++ b/src/utilities/archiveWriter.vala @@ -0,0 +1,139 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class can be used to pack a directory of files recursively into +/// a *.tar.gz archive. +///////////////////////////////////////////////////////////////////////// + +public class ArchiveWriter : GLib.Object { + + private Archive.Write archive; + + ///////////////////////////////////////////////////////////////////// + /// Constructs a new ArchiveWriter + ///////////////////////////////////////////////////////////////////// + + public ArchiveWriter() { + this.archive = new Archive.Write(); + this.archive.add_filter_gzip(); + this.archive.set_format_pax_restricted(); + + } + + ///////////////////////////////////////////////////////////////////// + /// Call this once after you created the ArchiveWriter. Pass the + /// path to the target archive location. + ///////////////////////////////////////////////////////////////////// + + public bool open(string path) { + return this.archive.open_filename(path) == Archive.Result.OK; + } + + ///////////////////////////////////////////////////////////////////// + /// Adds all files of a given directory to the previously opened + /// archive. + ///////////////////////////////////////////////////////////////////// + + public bool add(string directory) { + return add_directory(directory, directory); + } + + ///////////////////////////////////////////////////////////////////// + /// When all files have been added, close the directory again. + ///////////////////////////////////////////////////////////////////// + + public void close() { + this.archive.close(); + } + + ///////////////////////////////////////////////////////////////////// + /// Private helper function which traveres a directory recursively. + ///////////////////////////////////////////////////////////////////// + + private bool add_directory(string directory, string relative_to) { + try { + var d = Dir.open(directory); + string name; + while ((name = d.read_name()) != null) { + string path = Path.build_filename(directory, name); + if (FileUtils.test(path, FileTest.IS_DIR)) { + if (!add_directory(path, relative_to)) { + return false; + } + + } else if (FileUtils.test(path, FileTest.IS_REGULAR)) { + if (!add_file(path, relative_to)) { + return false; + } + + } else { + warning("Packaging theme: Ignoring irregular file " + name); + } + } + } catch (Error e) { + warning (e.message); + return false; + } + + return true; + + } + + ///////////////////////////////////////////////////////////////////// + /// Private halper which adds a file to the archive. + ///////////////////////////////////////////////////////////////////// + + public bool add_file(string path, string relative_to) { + var entry = new Archive.Entry(); + entry.set_pathname(path.replace(relative_to, "")); + + Posix.Stat st; + Posix.stat(path, out st); + entry.copy_stat(st); + entry.set_size(st.st_size); + + if (this.archive.write_header(entry) == Archive.Result.OK) { + try { + var reader = File.new_for_path(path).read(); + uint8 buffer[4096]; + + var len = reader.read(buffer); + + while(len > 0) { + this.archive.write_data(buffer, len); + len = reader.read(buffer); + } + + this.archive.finish_entry(); + } catch (Error e) { + warning (e.message); + return false; + } + + } else { + warning("Failed to include file " + path + " into archive"); + return false; + } + + return true; + } +} + +} diff --git a/src/utilities/bindingManager.vala b/src/utilities/bindingManager.vala new file mode 100644 index 0000000..ac5a8fb --- /dev/null +++ b/src/utilities/bindingManager.vala @@ -0,0 +1,428 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// Globally binds key stroke to given ID's. When one of the bound +/// strokes is invoked, a signal with the according ID is emitted. +///////////////////////////////////////////////////////////////////////// + +public class BindingManager : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Called when a stored binding is invoked. The according ID is + /// passed as argument. + ///////////////////////////////////////////////////////////////////// + + public signal void on_press(string id); + + ///////////////////////////////////////////////////////////////////// + /// A list storing bindings, which are invoked even if Gnome-Pie + /// doesn't have the current focus + ///////////////////////////////////////////////////////////////////// + + private Gee.List<Keybinding> bindings = new Gee.ArrayList<Keybinding>(); + + ///////////////////////////////////////////////////////////////////// + /// Ignored modifier masks, used to grab all keys even if these locks + /// are active. + ///////////////////////////////////////////////////////////////////// + + private static uint[] lock_modifiers = { + 0, + Gdk.ModifierType.MOD2_MASK, + Gdk.ModifierType.LOCK_MASK, + Gdk.ModifierType.MOD5_MASK, + + Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK, + Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.MOD5_MASK, + Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK, + + Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK + }; + + ///////////////////////////////////////////////////////////////////// + /// Some variables to remember which delayed binding was delayed. + /// When the delay passes without another event indicating that the + /// Trigger was released, the stored binding will be activated. + ///////////////////////////////////////////////////////////////////// + + private uint32 delayed_count = 0; + private X.Event? delayed_event = null; + private Keybinding? delayed_binding = null; + + ///////////////////////////////////////////////////////////////////// + /// Helper class to store keybinding + ///////////////////////////////////////////////////////////////////// + + private class Keybinding { + + public Keybinding(Trigger trigger, string id) { + this.trigger = trigger; + this.id = id; + } + + public Trigger trigger { get; set; } + public string id { get; set; } + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor adds the event filter to the root window. + ///////////////////////////////////////////////////////////////////// + + public BindingManager() { + // init filter to retrieve X.Events + Gdk.Window rootwin = Gdk.get_default_root_window(); + if(rootwin != null) { + rootwin.add_filter(event_filter); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Binds the ID to the given accelerator. + ///////////////////////////////////////////////////////////////////// + + public void bind(Trigger trigger, string id) { + if (trigger.key_code != 0) { + unowned X.Display display = Gdk.X11.get_default_xdisplay(); + X.ID xid = Gdk.X11.get_default_root_xwindow(); + + Gdk.error_trap_push(); + + // if bound to super key we need to grab MOD4 instead + // (for whatever reason...) + var modifiers = prepare_modifiers(trigger.modifiers); + + foreach(uint lock_modifier in lock_modifiers) { + if (trigger.with_mouse) { + display.grab_button(trigger.key_code, modifiers|lock_modifier, xid, false, + X.EventMask.ButtonPressMask | X.EventMask.ButtonReleaseMask, + X.GrabMode.Async, X.GrabMode.Async, xid, 0); + } else { + display.grab_key(trigger.key_code, modifiers|lock_modifier, + xid, false, X.GrabMode.Async, X.GrabMode.Async); + } + } + + Gdk.flush(); + Keybinding binding = new Keybinding(trigger, id); + bindings.add(binding); + display.flush(); + } else { + //no key_code: just add the bindind to the list to save optional trigger parameters + Keybinding binding = new Keybinding(trigger, id); + bindings.add(binding); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Unbinds the accelerator of the given ID. + ///////////////////////////////////////////////////////////////////// + + public void unbind(string id) { + foreach (var binding in bindings) { + if (id == binding.id) { + if (binding.trigger.key_code == 0) { + //no key_code: just remove the bindind from the list + bindings.remove(binding); + return; + } + break; + } + } + + unowned X.Display display = Gdk.X11.get_default_xdisplay(); + X.ID xid = Gdk.X11.get_default_root_xwindow(); + + Gee.List<Keybinding> remove_bindings = new Gee.ArrayList<Keybinding>(); + foreach(var binding in bindings) { + if(id == binding.id) { + + // if bound to super key we need to ungrab MOD4 instead + // (for whatever reason...) + var modifiers = prepare_modifiers(binding.trigger.modifiers); + + foreach(uint lock_modifier in lock_modifiers) { + if (binding.trigger.with_mouse) { + display.ungrab_button(binding.trigger.key_code, modifiers|lock_modifier, xid); + } else { + display.ungrab_key(binding.trigger.key_code, modifiers|lock_modifier, xid); + } + } + remove_bindings.add(binding); + } + } + + bindings.remove_all(remove_bindings); + display.flush(); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns a human readable accelerator for the given ID. + ///////////////////////////////////////////////////////////////////// + + public string get_accelerator_label_of(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + return binding.trigger.label_with_specials; + } + } + + return _("Not bound"); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the accelerator to which the given ID is bound. + ///////////////////////////////////////////////////////////////////// + + public string get_accelerator_of(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + return binding.trigger.name; + } + } + + return ""; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns whether the pie with the given ID is in turbo mode. + ///////////////////////////////////////////////////////////////////// + + public bool get_is_turbo(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + return binding.trigger.turbo; + } + } + + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns whether the pie with the given ID opens centered. + ///////////////////////////////////////////////////////////////////// + + public bool get_is_centered(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + return binding.trigger.centered; + } + } + + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns whether the pie with the given ID is in warp mode. + ///////////////////////////////////////////////////////////////////// + + public bool get_is_warp(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + return binding.trigger.warp; + } + } + + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns whether the pie with the given ID is auto shaped + ///////////////////////////////////////////////////////////////////// + + public bool get_is_auto_shape(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + return (binding.trigger.shape == 0); + } + } + + return false; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the prefered pie shape number + ///////////////////////////////////////////////////////////////////// + + public int get_shape_number(string id) { + foreach (var binding in bindings) { + if (binding.id == id) { + if (binding.trigger.shape == 0) + break; //return default if auto-shaped + return binding.trigger.shape; //use selected shape + } + } + + return 5; //default= full pie + } + + + ///////////////////////////////////////////////////////////////////// + /// Returns the name ID of the Pie bound to the given Trigger. + /// Returns "" if there is nothing bound to this trigger. + ///////////////////////////////////////////////////////////////////// + + public string get_assigned_id(Trigger trigger) { + var second = Trigger.remove_optional(trigger.name); + if (second != "") { + foreach (var binding in bindings) { + var first = Trigger.remove_optional(binding.trigger.name); + if (first == second) { + return binding.id; + } + } + } + return ""; + } + + ///////////////////////////////////////////////////////////////////// + /// If SUPER_MASK is set in the input, it will be replaced with + /// MOD4_MASK. For some reason this is required to listen for key + /// presses of the super button.... + ///////////////////////////////////////////////////////////////////// + + private Gdk.ModifierType prepare_modifiers(Gdk.ModifierType mods) { + if ((mods & Gdk.ModifierType.SUPER_MASK) > 0) { + mods |= Gdk.ModifierType.MOD4_MASK; + mods = mods & ~ Gdk.ModifierType.SUPER_MASK; + } + + return mods & ~lock_modifiers[7]; + } + + ///////////////////////////////////////////////////////////////////// + /// Event filter method needed to fetch X.Events. + ///////////////////////////////////////////////////////////////////// + + private Gdk.FilterReturn event_filter(Gdk.XEvent gdk_xevent, Gdk.Event gdk_event) { + + #if VALA_0_16 || VALA_0_17 + X.Event* xevent = (X.Event*) gdk_xevent; + #else + void* pointer = &gdk_xevent; + X.Event* xevent = (X.Event*) pointer; + #endif + + if(xevent->type == X.EventType.KeyPress) { + foreach(var binding in bindings) { + + // remove NumLock, CapsLock and ScrollLock from key state + var event_mods = prepare_modifiers((Gdk.ModifierType)xevent.xkey.state); + var bound_mods = prepare_modifiers(binding.trigger.modifiers); + + if(xevent->xkey.keycode == binding.trigger.key_code && + event_mods == bound_mods) { + + if (binding.trigger.delayed) { + this.activate_delayed(binding, *xevent); + } else { + on_press(binding.id); + } + } + } + } + else if(xevent->type == X.EventType.ButtonPress) { + foreach(var binding in bindings) { + + // remove NumLock, CapsLock and ScrollLock from key state + var event_mods = prepare_modifiers((Gdk.ModifierType)xevent.xbutton.state); + var bound_mods = prepare_modifiers(binding.trigger.modifiers); + + if(xevent->xbutton.button == binding.trigger.key_code && + event_mods == bound_mods) { + + if (binding.trigger.delayed) { + this.activate_delayed(binding, *xevent); + } else { + on_press(binding.id); + } + } + } + } + else if(xevent->type == X.EventType.ButtonRelease || xevent->type == X.EventType.KeyRelease) { + this.activate_delayed(null, *xevent); + } + + return Gdk.FilterReturn.CONTINUE; + } + + ///////////////////////////////////////////////////////////////////// + /// This method is always called when a trigger is activated which is + /// delayed. Therefore on_press() is only emitted, when this method + /// is not called again within 300 milliseconds. Else a fake event is + /// sent in order to simulate the actual key which has been pressed. + ///////////////////////////////////////////////////////////////////// + + private void activate_delayed(Keybinding? binding , X.Event event) { + // increase event count, so any waiting event will realize that + // something happened in the meantime + var current_count = ++this.delayed_count; + + if (binding == null && this.delayed_event != null) { + // if the trigger is released and an event is currently waiting + // simulate that the trigger has been pressed without any inter- + // ference of Gnome-Pie + unowned X.Display display = Gdk.X11.get_default_xdisplay(); + + // unbind the trigger, else we'll capture that event again ;) + unbind(delayed_binding.id); + + if (this.delayed_binding.trigger.with_mouse) { + // simulate mouse click + XTest.fake_button_event(display, this.delayed_event.xbutton.button, true, 0); + display.flush(); + + XTest.fake_button_event(display, this.delayed_event.xbutton.button, false, 0); + display.flush(); + + } else { + // simulate key press + XTest.fake_key_event(display, this.delayed_event.xkey.keycode, true, 0); + display.flush(); + + XTest.fake_key_event(display, this.delayed_event.xkey.keycode, false, 0); + display.flush(); + } + + // bind it again + bind(delayed_binding.trigger, delayed_binding.id); + + this.delayed_binding = null; + this.delayed_event = null; + + } else if (binding != null) { + // if the trigger has been pressed, store it and wait for any interuption + // within the next 300 milliseconds + this.delayed_event = event; + this.delayed_binding = binding; + + Timeout.add(300, () => { + // if nothing has been pressed in the meantime + if (current_count == this.delayed_count) { + this.delayed_binding = null; + this.delayed_event = null; + on_press(binding.id); + } + return false; + }); + } + } +} + +} diff --git a/src/utilities/color.vala b/src/utilities/color.vala new file mode 100644 index 0000000..a681e02 --- /dev/null +++ b/src/utilities/color.vala @@ -0,0 +1,327 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +using GLib.Math; + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A Color class with full rgb/hsv support +/// and some useful utility methods. +///////////////////////////////////////////////////////////////////////// + +public class Color: GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Private members, storing the actual color information. + /// In range 0 .. 1 + ///////////////////////////////////////////////////////////////////// + + private float _r; + private float _g; + private float _b; + private float _a; + + ///////////////////////////////////////////////////////////////////// + /// Creates a white Color. + ///////////////////////////////////////////////////////////////////// + + public Color() { + Color.from_rgb(1.0f, 1.0f, 1.0f); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a solid color with the given RGB values. + ///////////////////////////////////////////////////////////////////// + + public Color.from_rgb(float red, float green, float blue) { + Color.from_rgba(red, green, blue, 1.0f); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a translucient color with the given RGBA values. + ///////////////////////////////////////////////////////////////////// + + public Color.from_rgba(float red, float green, float blue, float alpha) { + r = red; + g = green; + b = blue; + a = alpha; + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a color from the given Gdk.Color + ///////////////////////////////////////////////////////////////////// + + public Color.from_gdk(Gdk.RGBA color) { + Color.from_rgba( + (float)color.red, + (float)color.green, + (float)color.blue, + (float)color.alpha + ); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a color from a given widget style + ///////////////////////////////////////////////////////////////////// + + public Color.from_widget_style(Gtk.Widget widget, string style_name) { + var ctx = widget.get_style_context(); + Gdk.RGBA color; + if (!ctx.lookup_color(style_name, out color)) { + warning("Failed to get style color for widget style \"" + style_name + "\"!"); + } + Color.from_gdk(color); + } + + ///////////////////////////////////////////////////////////////////// + /// Creates a color, parsed from a string, such as #22EE33 + ///////////////////////////////////////////////////////////////////// + + public Color.from_string(string hex_string) { + var color = Gdk.RGBA(); + color.parse(hex_string); + Color.from_gdk(color); + } + + ///////////////////////////////////////////////////////////////////// + /// Gets the main color from an Image. Code from Unity. + ///////////////////////////////////////////////////////////////////// + + public Color.from_icon(Image icon) { + unowned uchar[] data = icon.surface.get_data(); + + uint width = icon.surface.get_width(); + uint height = icon.surface.get_height(); + uint row_bytes = icon.surface.get_stride(); + + double total = 0.0; + double rtotal = 0.0; + double gtotal = 0.0; + double btotal = 0.0; + + for (uint i = 0; i < width; ++i) { + for (uint j = 0; j < height; ++j) { + uint pixel = j * row_bytes + i * 4; + double b = data[pixel + 0]/255.0; + double g = data[pixel + 1]/255.0; + double r = data[pixel + 2]/255.0; + double a = data[pixel + 3]/255.0; + + double saturation = (fmax (r, fmax (g, b)) - fmin (r, fmin (g, b))); + double relevance = 0.1 + 0.9 * a * saturation; + + rtotal += (r * relevance); + gtotal += (g * relevance); + btotal += (b * relevance); + + total += relevance; + } + } + + Color.from_rgb((float)(rtotal/total), (float)(gtotal/total), (float)(btotal/total)); + + if (s > 0.15f) s = 0.65f; + + v = 1.0f; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns this color as its hex representation. + ///////////////////////////////////////////////////////////////////// + + public string to_hex_string() { + return "#%02X%02X%02X".printf((int)(_r*255), (int)(_g*255), (int)(_b*255)); + } + + ///////////////////////////////////////////////////////////////////// + /// The reddish part of the color. + ///////////////////////////////////////////////////////////////////// + + public float r { + get { + return _r; + } + set { + if (value > 1.0f) _r = 1.0f; + else if (value < 0.0f) _r = 0.0f; + else _r = value; + } + } + + ///////////////////////////////////////////////////////////////////// + /// The greenish part of the color. + ///////////////////////////////////////////////////////////////////// + + public float g { + get { + return _g; + } + set { + if (value > 1.0f) _g = 1.0f; + else if (value < 0.0f) _g = 0.0f; + else _g = value; + } + } + + ///////////////////////////////////////////////////////////////////// + /// The blueish part of the color. + ///////////////////////////////////////////////////////////////////// + + public float b { + get { + return _b; + } + set { + if (value > 1.0f) _b = 1.0f; + else if (value < 0.0f) _b = 0.0f; + else _b = value; + } + } + + ///////////////////////////////////////////////////////////////////// + /// The transparency of the color. + ///////////////////////////////////////////////////////////////////// + + public float a { + get { + return _a; + } + set { + if (value > 1.0f) _a = 1.0f; + else if (value < 0.0f) _a = 0.0f; + else _a = value; + } + } + + ///////////////////////////////////////////////////////////////////// + /// The hue of the color. + ///////////////////////////////////////////////////////////////////// + + public float h { + get { + if (s > 0.0f) { + float maxi = fmaxf(fmaxf(r, g), b); + float mini = fminf(fminf(r, g), b); + + if (maxi == r) + return fmodf(60.0f*((g-b)/(maxi-mini))+360.0f, 360.0f); + else if (maxi == g) + return fmodf(60.0f*(2.0f + (b-r)/(maxi-mini))+360.0f, 360.0f); + else + return fmodf(60.0f*(4.0f + (r-g)/(maxi-mini))+360.0f, 360.0f); + } + else return 0.0f; + } + set { + setHSV(value, s, v); + } + } + + ///////////////////////////////////////////////////////////////////// + /// The saturation of the color. + ///////////////////////////////////////////////////////////////////// + + public float s { + get { + if (v == 0.0f) return 0.0f; + else return ((v-fminf(fminf(r, g), b)) / v); + } + set { + if (value > 1.0f) setHSV(h, 1.0f, v); + else if (value < 0.0f) setHSV(h, 0.0f, v); + else setHSV(h, value, v); + } + } + + ///////////////////////////////////////////////////////////////////// + /// The value of the color. + ///////////////////////////////////////////////////////////////////// + + public float v { + get { + return fmaxf(fmaxf(r, g), b); + } + set { + if (value > 1) setHSV(h, s, 1.0f); + else if (value < 0) setHSV(h, s, 0.0f); + else setHSV(h, s, value); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Inverts the color. + ///////////////////////////////////////////////////////////////////// + + public void invert() { + h += 180.0f; + v = 1.0f - v; + } + + ///////////////////////////////////////////////////////////////////// + /// Private member, used to apply color changes. + ///////////////////////////////////////////////////////////////////// + + private void setHSV(float hue, float saturation, float val) { + if(saturation == 0) { + r = val; + g = val; + b = val; + return; + } + hue = fmodf(hue, 360); + hue /= 60; + int i = (int) floorf(hue); + float f = hue - i; + + switch(i) { + case 0: + r = val; + g = val * (1.0f - saturation * (1.0f - f)); + b = val * (1.0f - saturation); + break; + case 1: + r = val * (1.0f - saturation * f); + g = val; + b = val * (1.0f - saturation); + break; + case 2: + r = val * (1.0f - saturation); + g = val; + b = val * (1.0f - saturation * (1.0f - f)); + break; + case 3: + r = val * (1.0f - saturation); + g = val * (1.0f - saturation * f); + b = val; + break; + case 4: + r = val * (1.0f - saturation * (1.0f - f)); + g = val * (1.0f - saturation); + b = val; + break; + default: + r = val; + g = val * (1.0f - saturation); + b = val * (1.0f - saturation * f); + break; + } + } +} + +} diff --git a/src/utilities/config.vala b/src/utilities/config.vala new file mode 100644 index 0000000..74bbcbb --- /dev/null +++ b/src/utilities/config.vala @@ -0,0 +1,239 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A singleton class for storing global settings. These settings can +/// be loaded from and saved to an XML file. +///////////////////////////////////////////////////////////////////////// + +public class Config : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The singleton instance of this class. + ///////////////////////////////////////////////////////////////////// + + private static Config _instance = null; + + ///////////////////////////////////////////////////////////////////// + /// Returns the singleton instance. + ///////////////////////////////////////////////////////////////////// + + public static Config global { + get { + if (_instance == null) { + _instance = new Config(); + _instance.load(); + } + return _instance; + } + private set { + _instance = value; + } + } + + ///////////////////////////////////////////////////////////////////// + /// All settings variables. + ///////////////////////////////////////////////////////////////////// + + public Theme theme { get; set; } + public double refresh_rate { get; set; default = 60.0; } + public double global_scale { get; set; default = 1.0; } + public int activation_range { get; set; default = 200; } + public int max_visible_slices { get; set; default = 24; } + public bool show_indicator { get; set; default = true; } + public bool show_captions { get; set; default = false; } + public bool search_by_string { get; set; default = true; } + public bool auto_start { get; set; default = false; } + public int showed_news { get; set; default = 0; } + public Gee.ArrayList<Theme?> themes { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Saves all above variables to a file. + ///////////////////////////////////////////////////////////////////// + + public void save() { + var writer = new Xml.TextWriter.filename(Paths.settings); + writer.start_document("1.0"); + writer.start_element("settings"); + writer.write_attribute("theme", theme.name); + writer.write_attribute("refresh_rate", refresh_rate.to_string()); + writer.write_attribute("global_scale", global_scale.to_string()); + writer.write_attribute("activation_range", activation_range.to_string()); + writer.write_attribute("max_visible_slices", max_visible_slices.to_string()); + writer.write_attribute("show_indicator", show_indicator ? "true" : "false"); + writer.write_attribute("show_captions", show_captions ? "true" : "false"); + writer.write_attribute("search_by_string", search_by_string ? "true" : "false"); + writer.write_attribute("showed_news", showed_news.to_string()); + writer.end_element(); + writer.end_document(); + } + + ///////////////////////////////////////////////////////////////////// + /// Loads all settings variables from a file. + ///////////////////////////////////////////////////////////////////// + + private void load() { + + // check for auto_start filename + this.auto_start = FileUtils.test(Paths.autostart, FileTest.EXISTS); + + // parse the settings file + Xml.Parser.init(); + Xml.Doc* settingsXML = Xml.Parser.parse_file(Paths.settings); + bool error_occrured = false; + string theme_name = ""; + + if (settingsXML != null) { + + Xml.Node* root = settingsXML->get_root_element(); + if (root != null) { + + 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 "theme": + theme_name = attr_content; + break; + case "refresh_rate": + refresh_rate = double.parse(attr_content); + break; + case "global_scale": + global_scale = double.parse(attr_content); + global_scale.clamp(0.5, 2.0); + break; + case "activation_range": + activation_range = int.parse(attr_content); + activation_range.clamp(0, 2000); + break; + case "max_visible_slices": + max_visible_slices = int.parse(attr_content); + max_visible_slices.clamp(10, 2000); + break; + case "show_indicator": + show_indicator = bool.parse(attr_content); + break; + case "show_captions": + show_captions = bool.parse(attr_content); + break; + case "search_by_string": + search_by_string = bool.parse(attr_content); + break; + case "showed_news": + showed_news = int.parse(attr_content); + break; + default: + warning("Invalid setting \"" + attr_name + "\" in gnome-pie.conf!"); + break; + } + } + + Xml.Parser.cleanup(); + + } else { + warning("Error loading settings: gnome-pie.conf is empty! Using defaults..."); + error_occrured = true; + } + + delete settingsXML; + + } else { + warning("Error loading settings: gnome-pie.conf not found! Using defaults..."); + error_occrured = true; + } + + load_themes(theme_name); + + if (error_occrured) { + save(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Registers all themes in the user's and in the global + /// theme directory. + ///////////////////////////////////////////////////////////////////// + + public void load_themes(string current) { + themes = new Gee.ArrayList<Theme?>(); + try { + string name; + + // load global themes + var d = Dir.open(Paths.global_themes); + while ((name = d.read_name()) != null) { + var new_theme = new Theme(Paths.global_themes + "/" + name); + + if (new_theme.load()) { + themes.add(new_theme); + } + } + + // load local themes + d = Dir.open(Paths.local_themes); + while ((name = d.read_name()) != null) { + var new_theme = new Theme(Paths.local_themes + "/" + name); + if (new_theme.load()) + themes.add(new_theme); + } + + } catch (Error e) { + warning (e.message); + } + + if (themes.size > 0) { + if (current == "") { + current = "Adwaita"; + warning("No theme specified! Using default..."); + } + foreach (var t in themes) { + if (t.name == current) { + theme = t; + break; + } + } + if (theme == null) { + theme = themes[0]; + warning("Theme \"" + current + "\" not found! Using fallback..."); + } + theme.load_images(); + } else { + error("No theme found!"); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true if a loaded theme has the given name or is in a + /// directory with the given name. + ///////////////////////////////////////////////////////////////////// + + public bool has_theme(string name) { + + foreach (var theme in themes) { + if (theme.name == name || theme.directory.has_suffix(name)) { + return true; + } + } + + return false; + } +} + +} diff --git a/src/utilities/focusGrabber.vala b/src/utilities/focusGrabber.vala new file mode 100644 index 0000000..baa5fed --- /dev/null +++ b/src/utilities/focusGrabber.vala @@ -0,0 +1,97 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// Some helper methods which focus the input on a given Gtk.Window. +///////////////////////////////////////////////////////////////////////// + +public class FocusGrabber : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Utilities for grabbing focus. + /// Code roughly from Gnome-Do/Synapse. + ///////////////////////////////////////////////////////////////////// + + public static void grab(Gdk.Window window, bool keyboard = true, bool pointer = true, bool owner_events = true) { + if (keyboard || pointer) { + window.raise(); + window.focus(Gdk.CURRENT_TIME); + + if (!try_grab_window(window, keyboard, pointer, owner_events)) { + int i = 0; + Timeout.add(100, () => { + if (++i >= 100) return false; + return !try_grab_window(window, keyboard, pointer, owner_events); + }); + } + } + } + + ///////////////////////////////////////////////////////////////////// + /// Code roughly from Gnome-Do/Synapse. + ///////////////////////////////////////////////////////////////////// + + public static void ungrab(bool keyboard = true, bool pointer = true) { + var display = Gdk.Display.get_default(); + var manager = display.get_device_manager(); + + GLib.List<weak Gdk.Device?> list = manager.list_devices(Gdk.DeviceType.MASTER); + + foreach(var device in list) { + if ((device.input_source == Gdk.InputSource.KEYBOARD && keyboard) + || (device.input_source != Gdk.InputSource.KEYBOARD && pointer)) + + device.ungrab(Gdk.CURRENT_TIME); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Code roughly from Gnome-Do/Synapse. + ///////////////////////////////////////////////////////////////////// + + private static bool try_grab_window(Gdk.Window window, bool keyboard, bool pointer, bool owner_events) { + var display = Gdk.Display.get_default(); + var manager = display.get_device_manager(); + + bool grabbed_all = true; + + GLib.List<weak Gdk.Device?> list = manager.list_devices(Gdk.DeviceType.MASTER); + + foreach(var device in list) { + if ((device.input_source == Gdk.InputSource.KEYBOARD && keyboard) + || (device.input_source != Gdk.InputSource.KEYBOARD && pointer)) { + + var status = device.grab(window, Gdk.GrabOwnership.APPLICATION, owner_events, + Gdk.EventMask.ALL_EVENTS_MASK, null, Gdk.CURRENT_TIME); + + if (status != Gdk.GrabStatus.SUCCESS) + grabbed_all = false; + } + } + + if (grabbed_all) + return true; + + ungrab(keyboard, pointer); + + return false; + } +} + +} diff --git a/src/utilities/key.vala b/src/utilities/key.vala new file mode 100644 index 0000000..486744d --- /dev/null +++ b/src/utilities/key.vala @@ -0,0 +1,161 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A class which represents a key stroke. It can be used to "press" +/// the associated keys. +///////////////////////////////////////////////////////////////////////// + +public class Key : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Some static members, which are often used by this class. + ///////////////////////////////////////////////////////////////////// + + private static X.Display display; + + private static int shift_code; + private static int ctrl_code; + private static int alt_code; + private static int super_code; + + ///////////////////////////////////////////////////////////////////// + /// A human readable form of the Key's accelerator. + ///////////////////////////////////////////////////////////////////// + + public string label { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// The accelerator of the Key. + ///////////////////////////////////////////////////////////////////// + + public string accelerator { get; private set; } + + ///////////////////////////////////////////////////////////////////// + /// Keycode and modifiers of this stroke. + ///////////////////////////////////////////////////////////////////// + + private int key_code; + private Gdk.ModifierType modifiers; + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members to defaults. + ///////////////////////////////////////////////////////////////////// + + public Key() { + this.accelerator = ""; + this.modifiers = 0; + this.key_code = 0; + this.label = _("Not bound"); + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public Key.from_string(string stroke) { + this.accelerator = stroke; + + uint keysym; + Gtk.accelerator_parse(stroke, out keysym, out this.modifiers); + this.key_code = display.keysym_to_keycode(keysym); + this.label = Gtk.accelerator_get_label(keysym, this.modifiers); + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, initializes all members. + ///////////////////////////////////////////////////////////////////// + + public Key.from_values(uint keysym, Gdk.ModifierType modifiers) { + this.accelerator = Gtk.accelerator_name(keysym, modifiers); + this.label = Gtk.accelerator_get_label(keysym, modifiers); + this.key_code = display.keysym_to_keycode(keysym); + this.modifiers = modifiers; + } + + ///////////////////////////////////////////////////////////////////// + /// Initializes static members. + ///////////////////////////////////////////////////////////////////// + + static construct { + display = new X.Display(); + + shift_code = display.keysym_to_keycode(Gdk.keyval_from_name("Shift_L")); + ctrl_code = display.keysym_to_keycode(Gdk.keyval_from_name("Control_L")); + alt_code = display.keysym_to_keycode(Gdk.keyval_from_name("Alt_L")); + super_code = display.keysym_to_keycode(Gdk.keyval_from_name("Super_L")); + } + + ///////////////////////////////////////////////////////////////////// + /// Simulates the pressing of the Key . + ///////////////////////////////////////////////////////////////////// + + public void press() { + // store currently pressed modifier keys + Gdk.ModifierType current_modifiers = get_modifiers(); + + // release them and press the desired ones + press_modifiers(current_modifiers, false); + press_modifiers(this.modifiers, true); + + // send events to X + display.flush(); + + // press and release the actual key + XTest.fake_key_event(display, this.key_code, true, 0); + XTest.fake_key_event(display, this.key_code, false, 0); + + // release the pressed modifiers and re-press the keys hold down by the user + press_modifiers(this.modifiers, false); + press_modifiers(current_modifiers, true); + + // send events to X + display.flush(); + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method returning currently hold down modifier keys. + ///////////////////////////////////////////////////////////////////// + + private Gdk.ModifierType get_modifiers() { + Gdk.ModifierType modifiers; + Gtk.get_current_event_state(out modifiers); + return modifiers; + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method which 'presses' the desired modifier keys. + ///////////////////////////////////////////////////////////////////// + + private void press_modifiers(Gdk.ModifierType modifiers, bool down) { + if ((modifiers & Gdk.ModifierType.CONTROL_MASK) > 0) + XTest.fake_key_event(display, ctrl_code, down, 0); + + if ((modifiers & Gdk.ModifierType.SHIFT_MASK) > 0) + XTest.fake_key_event(display, shift_code, down, 0); + + if ((modifiers & Gdk.ModifierType.MOD1_MASK) > 0) + XTest.fake_key_event(display, alt_code, down, 0); + + if ((modifiers & Gdk.ModifierType.SUPER_MASK) > 0) + XTest.fake_key_event(display, super_code, down, 0); + } +} + +} diff --git a/src/utilities/logger.vala b/src/utilities/logger.vala new file mode 100644 index 0000000..7c66615 --- /dev/null +++ b/src/utilities/logger.vala @@ -0,0 +1,270 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A static class which beautifies the messages of the default logger. +/// Some of this code is inspired by plank's written by Robert Dyer. +/// Thanks a lot for this project! +///////////////////////////////////////////////////////////////////////// + +public class Logger { + + ///////////////////////////////////////////////////////////////////// + /// If these are set to false, the according messages are not shown + ///////////////////////////////////////////////////////////////////// + + private static const bool display_debug = true; + private static const bool display_warning = true; + private static const bool display_error = true; + private static const bool display_message = true; + + ///////////////////////////////////////////////////////////////////// + /// If these are set to false, the according messages are not logged + ///////////////////////////////////////////////////////////////////// + + private static const bool log_debug = false; + private static const bool log_warning = true; + private static const bool log_error = true; + private static const bool log_message = true; + + ///////////////////////////////////////////////////////////////////// + /// If true, a time stamp is shown in each message. + ///////////////////////////////////////////////////////////////////// + + private static const bool display_time = false; + private static const bool log_time = true; + + ///////////////////////////////////////////////////////////////////// + /// If true, the origin of the message is shown. In form file:line + ///////////////////////////////////////////////////////////////////// + + private static const bool display_file = false; + private static const bool log_file = false; + + ///////////////////////////////////////////////////////////////////// + /// A regex, used to format the standard message. + ///////////////////////////////////////////////////////////////////// + + private static Regex regex = null; + + ///////////////////////////////////////////////////////////////////// + /// Limit log and statistics size to roughly 1 MB. + ///////////////////////////////////////////////////////////////////// + + private static const int max_log_length = 1000000; + + private static int log_length; + + ///////////////////////////////////////////////////////////////////// + /// Possible terminal colors. + ///////////////////////////////////////////////////////////////////// + + private enum Color { + BLACK, + RED, + GREEN, + YELLOW, + BLUE, + PURPLE, + TURQUOISE, + WHITE + } + + ///////////////////////////////////////////////////////////////////// + /// Creates the regex and binds the handler. + ///////////////////////////////////////////////////////////////////// + + public static void init() { + log_length = -1; + + try { + regex = new Regex("""(.*)\.vala(:\d+): (.*)"""); + } catch {} + + GLib.Log.set_handler(null, GLib.LogLevelFlags.LEVEL_MASK, log_func); + } + + ///////////////////////////////////////////////////////////////////// + /// Appends a line to the log file + ///////////////////////////////////////////////////////////////////// + + private static void write_log_line(string line) { + var log = GLib.FileStream.open(Paths.log, "a"); + + if (log != null) { + if (log_length == -1) + log_length = (int)log.tell(); + + log.puts(line); + log_length += line.length; + } + + if (log_length > max_log_length) { + string content = ""; + + try { + GLib.FileUtils.get_contents(Paths.log, out content); + int split_index = content.index_of_char('\n', log_length - (int)(max_log_length*0.9)); + GLib.FileUtils.set_contents(Paths.log, content.substring(split_index+1)); + + log_length -= (split_index+1); + } catch (GLib.FileError e) {} + } + } + + ///////////////////////////////////////////////////////////////////// + /// Displays a message. + ///////////////////////////////////////////////////////////////////// + + private static void message(string message, string message_log) { + if (display_message) { + stdout.printf(set_color(Color.GREEN, false) + "[" + (display_time ? get_time() + " " : "") + "MESSAGE]" + message); + } + + if (log_message) { + write_log_line("[" + (log_time ? get_time() + " " : "") + "MESSAGE]" + message_log); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Displays a Debug message. + ///////////////////////////////////////////////////////////////////// + + private static void debug(string message, string message_log) { + if (display_debug) { + stdout.printf(set_color(Color.BLUE, false) + "[" + (display_time ? get_time() + " " : "") + " DEBUG ]" + message); + } + + if (log_debug) { + write_log_line("[" + (log_time ? get_time() + " " : "") + " DEBUG ]" + message_log); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Displays a Warning message. + ///////////////////////////////////////////////////////////////////// + + private static void warning(string message, string message_log) { + if (display_warning) { + stdout.printf(set_color(Color.YELLOW, false) + "[" + (display_time ? get_time() + " " : "") + "WARNING]" + message); + } + + if (log_warning) { + write_log_line("[" + (log_time ? get_time() + " " : "") + "WARNING]" + message_log); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Displays a Error message. + ///////////////////////////////////////////////////////////////////// + + private static void error(string message, string message_log) { + if (display_error) { + stdout.printf(set_color(Color.RED, false) + "[" + (display_time ? get_time() + " " : "") + " ERROR ]" + message); + } + + if (log_error) { + write_log_line("[" + (log_time ? get_time() + " " : "") + " ERROR ]" + message_log); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method which resets the terminal color. + ///////////////////////////////////////////////////////////////////// + + private static string reset_color() { + return "\x001b[0m"; + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method which sets the terminal color. + ///////////////////////////////////////////////////////////////////// + + private static string set_color(Color color, bool bold) { + if (bold) return "\x001b[1;%dm".printf((int)color + 30); + else return "\x001b[0;%dm".printf((int)color + 30); + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the current time in hh:mm:ss:mmmmmm + ///////////////////////////////////////////////////////////////////// + + private static string get_time() { + var now = new DateTime.now_local(); + return "%.4d:%.2d:%.2d:%.2d:%.2d:%.2d:%.6d".printf(now.get_year(), now.get_month(), now.get_day_of_month(), now.get_hour(), now.get_minute(), now.get_second(), now.get_microsecond()); + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method to format the message. + ///////////////////////////////////////////////////////////////////// + + private static string create_message(string message) { + if (display_file && regex != null && regex.match(message)) { + var parts = regex.split(message); + return " [%s%s]%s %s\n".printf(parts[1], parts[2], reset_color(), parts[3]); + } else if (regex != null && regex.match(message)) { + var parts = regex.split(message); + return "%s %s\n".printf(reset_color(), parts[3]); + } else { + return reset_color() + " " + message + "\n"; + } + } + + ///////////////////////////////////////////////////////////////////// + /// Helper method to format the message for logging. + ///////////////////////////////////////////////////////////////////// + + private static string create_log_message(string message) { + if (log_file && regex != null && regex.match(message)) { + var parts = regex.split(message); + return " [%s%s] %s\n".printf(parts[1], parts[2], parts[3]); + } else if (regex != null && regex.match(message)) { + var parts = regex.split(message); + return " %s\n".printf(parts[3]); + } else { + return " " + message + "\n"; + } + } + + ///////////////////////////////////////////////////////////////////// + /// The handler function. + ///////////////////////////////////////////////////////////////////// + + private static void log_func(string? d, LogLevelFlags flags, string text) { + switch (flags) { + case LogLevelFlags.LEVEL_ERROR: + case LogLevelFlags.LEVEL_CRITICAL: + error(create_message(text), create_log_message(text)); + break; + case LogLevelFlags.LEVEL_INFO: + case LogLevelFlags.LEVEL_MESSAGE: + message(create_message(text), create_log_message(text)); + break; + case LogLevelFlags.LEVEL_DEBUG: + debug(create_message(text), create_log_message(text)); + break; + case LogLevelFlags.LEVEL_WARNING: + default: + warning(create_message(text), create_log_message(text)); + break; + } + } +} + +} diff --git a/src/utilities/paths.vala b/src/utilities/paths.vala new file mode 100644 index 0000000..7bdd642 --- /dev/null +++ b/src/utilities/paths.vala @@ -0,0 +1,286 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// A static class which stores all relevant paths used by Gnome-Pie. +/// These depend upon the location from which the program was launched. +///////////////////////////////////////////////////////////////////////// + +public class Paths : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// The config directory, + /// usually ~/.config/gnome-pie/. + ///////////////////////////////////////////////////////////////////// + + public static string config_directory { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The log file, + /// usually ~/.config/gnome-pie/gnome-pie.log. + ///////////////////////////////////////////////////////////////////// + + public static string log { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The statistics file, + /// usually ~/.config/gnome-pie/gnome-pie.stats. + ///////////////////////////////////////////////////////////////////// + + public static string stats { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The settings file, + /// usually ~/.config/gnome-pie/gnome-pie.conf. + ///////////////////////////////////////////////////////////////////// + + public static string settings { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The pie configuration file + /// usually ~/.config/gnome-pie/pies.conf. + ///////////////////////////////////////////////////////////////////// + + public static string pie_config { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The directory containing themes installed by the user + /// usually ~/.config/gnome-pie/themes. + ///////////////////////////////////////////////////////////////////// + + public static string local_themes { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The directory containing pre-installed themes + /// usually /usr/share/gnome-pie/themes. + ///////////////////////////////////////////////////////////////////// + + public static string global_themes { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The directory containing locale files + /// usually /usr/share/locale. + ///////////////////////////////////////////////////////////////////// + + public static string locales { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The directory containing UI declaration files + /// usually /usr/share/gnome-pie/ui/. + ///////////////////////////////////////////////////////////////////// + + public static string ui_files { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The autostart file of gnome-pie_config + /// usually ~/.config/autostart/gnome-pie.desktop. + ///////////////////////////////////////////////////////////////////// + + public static string autostart { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The path where all pie-launchers are stored + /// usually ~/.config/gnome-pie/launchers. + ///////////////////////////////////////////////////////////////////// + + public static string launchers { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The path to the executable. + ///////////////////////////////////////////////////////////////////// + + public static string executable { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// Deletes a directory recursively from disk. Use with care :) + ///////////////////////////////////////////////////////////////////// + + public static void delete_directory(string directory) { + try { + var d = Dir.open(directory); + string name; + while ((name = d.read_name()) != null) { + string path = Path.build_filename(directory, name); + if (FileUtils.test(path, FileTest.IS_DIR)) { + delete_directory(path); + } else { + FileUtils.remove(path); + } + } + DirUtils.remove(directory); + } catch (Error e) { + warning (e.message); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Initializes all values above. + ///////////////////////////////////////////////////////////////////// + + public static void init() { + + // get path of executable + try { + executable = GLib.File.new_for_path(GLib.FileUtils.read_link("/proc/self/exe")).get_path(); + } catch (GLib.FileError e) { + warning("Failed to get path of executable!"); + } + + // append resources to icon search path to icon theme, if neccasary + var icon_dir = GLib.File.new_for_path(GLib.Path.get_dirname(executable)).get_child("resources"); + + if (icon_dir.query_exists()) { + string path = icon_dir.get_path(); + Gtk.IconTheme.get_default().append_search_path(path); + } + + Gtk.IconTheme.get_default().append_search_path("/usr/share/pixmaps/"); + Gtk.IconTheme.get_default().append_search_path("/usr/share/icons/hicolor/scalable/apps"); + Gtk.IconTheme.get_default().append_search_path("/usr/local/share/icons/hicolor/scalable/apps"); + + // get global paths + var default_dir = GLib.File.new_for_path("/usr/share/gnome-pie/"); + if(!default_dir.query_exists()) { + default_dir = GLib.File.new_for_path("/usr/local/share/gnome-pie/"); + + if(!default_dir.query_exists()) { + default_dir = GLib.File.new_for_path(GLib.Path.get_dirname( + executable)).get_child("resources"); + } + } + + global_themes = default_dir.get_path() + "/themes"; + ui_files = default_dir.get_path() + "/ui"; + + // get locales path + var locale_dir = GLib.File.new_for_path("/usr/share/locale/de/LC_MESSAGES/gnomepie.mo"); + if(locale_dir.query_exists()) { + locale_dir = GLib.File.new_for_path("/usr/share/locale"); + } else { + locale_dir = GLib.File.new_for_path("/usr/local/share/locale/de/LC_MESSAGES/gnomepie.mo"); + if(locale_dir.query_exists()) { + locale_dir = GLib.File.new_for_path("/usr/local/share/locale"); + } else { + locale_dir = GLib.File.new_for_path(GLib.Path.get_dirname( + executable)).get_child("resources/locale/de/LC_MESSAGES/gnomepie.mo"); + + if(locale_dir.query_exists()) { + locale_dir = GLib.File.new_for_path(GLib.Path.get_dirname( + executable)).get_child("resources/locale"); + } + } + } + + locales = locale_dir.get_path(); + + // get local paths + var config_dir = GLib.File.new_for_path( + GLib.Environment.get_user_config_dir()).get_child("gnome-pie"); + + // create config_dir if neccasary + if(!config_dir.query_exists()) { + try { + config_dir.make_directory(); + } catch (GLib.Error e) { + error(e.message); + } + } + + config_directory = config_dir.get_path(); + + // create local themes directory if neccasary + var themes_dir = config_dir.get_child("themes"); + if(!themes_dir.query_exists()) { + try { + themes_dir.make_directory(); + } catch (GLib.Error e) { + error(e.message); + } + } + + local_themes = themes_dir.get_path(); + + // create launchers directory if neccasary + var launchers_dir = config_dir.get_child("launchers"); + if(!launchers_dir.query_exists()) { + try { + launchers_dir.make_directory(); + } catch (GLib.Error e) { + error(e.message); + } + } + + launchers = launchers_dir.get_path(); + + // check for config file + var config_file = config_dir.get_child("pies.conf"); + + pie_config = config_file.get_path(); + settings = config_dir.get_path() + "/gnome-pie.conf"; + log = config_dir.get_path() + "/gnome-pie.log"; + stats = config_dir.get_path() + "/gnome-pie.stats"; + + if (!GLib.File.new_for_path(log).query_exists()) { + try { + FileUtils.set_contents(log, ""); + } catch (GLib.FileError e) { + error(e.message); + } + } + + if (!GLib.File.new_for_path(stats).query_exists()) { + try { + FileUtils.set_contents(stats, ""); + } catch (GLib.FileError e) { + error(e.message); + } + } + + // autostart file name + autostart = GLib.Path.build_filename(GLib.Environment.get_user_config_dir(), + "autostart", "gnome-pie.desktop", null); + + // print results + if (!GLib.File.new_for_path(pie_config).query_exists()) + warning("Failed to find pie configuration file \"pies.conf\"! (This should only happen when Gnome-Pie is started for the first time...)"); + + if (!GLib.File.new_for_path(settings).query_exists()) + warning("Failed to find settings file \"gnome-pie.conf\"! (This should only happen when Gnome-Pie is started for the first time...)"); + + if (!GLib.File.new_for_path(log).query_exists()) + warning("Failed to find log file \"gnome-pie.log\"!"); + + if (!GLib.File.new_for_path(stats).query_exists()) + warning("Failed to find statistics file \"gnome-pie.stats\"!"); + + if (!GLib.File.new_for_path(local_themes).query_exists()) + warning("Failed to find local themes directory!"); + + if (!GLib.File.new_for_path(launchers).query_exists()) + warning("Failed to find launchers directory!"); + + if (!GLib.File.new_for_path(global_themes).query_exists()) + warning("Failed to find global themes directory!"); + + if (!GLib.File.new_for_path(ui_files).query_exists()) + warning("Failed to find UI files directory!"); + } +} + +} diff --git a/src/utilities/trigger.vala b/src/utilities/trigger.vala new file mode 100644 index 0000000..5373b41 --- /dev/null +++ b/src/utilities/trigger.vala @@ -0,0 +1,357 @@ +///////////////////////////////////////////////////////////////////////// +// Copyright (c) 2011-2015 by Simon Schneegans +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +///////////////////////////////////////////////////////////////////////// + +namespace GnomePie { + +///////////////////////////////////////////////////////////////////////// +/// This class represents a hotkey, used to open pies. It supports any +/// combination of modifier keys with keyboard and mouse buttons. +///////////////////////////////////////////////////////////////////////// + +public class Trigger : GLib.Object { + + ///////////////////////////////////////////////////////////////////// + /// Returns a human-readable version of this Trigger. + ///////////////////////////////////////////////////////////////////// + + public string label { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// Returns a human-readable version of this Trigger. Small + /// identifiers for turbo mode and delayed mode are added. + ///////////////////////////////////////////////////////////////////// + + public string label_with_specials { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The Trigger string. Like [delayed]<Control>button3 + ///////////////////////////////////////////////////////////////////// + + public string name { get; private set; default=""; } + + ///////////////////////////////////////////////////////////////////// + /// The key code of the hotkey or the button number of the mouse. + ///////////////////////////////////////////////////////////////////// + + public int key_code { get; private set; default=0; } + + ///////////////////////////////////////////////////////////////////// + /// The keysym of the hotkey or the button number of the mouse. + ///////////////////////////////////////////////////////////////////// + + public uint key_sym { get; private set; default=0; } + + ///////////////////////////////////////////////////////////////////// + /// Modifier keys pressed for this hotkey. + ///////////////////////////////////////////////////////////////////// + + public Gdk.ModifierType modifiers { get; private set; default=0; } + + ///////////////////////////////////////////////////////////////////// + /// True if this hotkey involves the mouse. + ///////////////////////////////////////////////////////////////////// + + public bool with_mouse { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// True if the pie closes when the trigger hotkey is released. + ///////////////////////////////////////////////////////////////////// + + public bool turbo { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// True if the trigger should wait a short delay before being + /// triggered. + ///////////////////////////////////////////////////////////////////// + + public bool delayed { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// True if the pie opens in the middle of the screen. + ///////////////////////////////////////////////////////////////////// + + public bool centered { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// True if the mouse pointer is warped to the pie's center. + ///////////////////////////////////////////////////////////////////// + + public bool warp { get; private set; default=false; } + + ///////////////////////////////////////////////////////////////////// + /// Returns the current selected "radio-button" shape: 0= automatic + /// 5= full pie; 1,3,7,8= quarters; 2,4,6,8=halves + /// 1 | 4 | 7 + /// 2 | 5 | 8 + /// 3 | 6 | 9 + ///////////////////////////////////////////////////////////////////// + + public int shape { get; private set; default=5; } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new, "unbound" Trigger. + ///////////////////////////////////////////////////////////////////// + + public Trigger() { + this.set_unbound(); + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new Trigger from a given Trigger string. This is + /// in this format: "[option(s)]<modifier(s)>button" where + /// "<modifier>" is something like "<Alt>" or "<Control>", "button" + /// something like "s", "F4" or "button0" and "[option]" is either + /// "[turbo]", "[centered]", "[warp]", "["delayed"]" or "["shape#"]" + ///////////////////////////////////////////////////////////////////// + + public Trigger.from_string(string trigger) { + this.parse_string(trigger); + } + + ///////////////////////////////////////////////////////////////////// + /// C'tor, creates a new Trigger from the key values. + ///////////////////////////////////////////////////////////////////// + + public Trigger.from_values(uint key_sym, Gdk.ModifierType modifiers, + bool with_mouse, bool turbo, bool delayed, + bool centered, bool warp, int shape ) { + + string trigger = (turbo ? "[turbo]" : "") + + (delayed ? "[delayed]" : "") + + (centered ? "[centered]" : "") + + (warp ? "[warp]" : "") + + (shape!=5 ? "[shape%d]".printf(shape) : ""); + + if (with_mouse) { + trigger += Gtk.accelerator_name(0, modifiers) + "button%u".printf(key_sym); + } else { + trigger += Gtk.accelerator_name(key_sym, modifiers); + } + + this.parse_string(trigger); + } + + ///////////////////////////////////////////////////////////////////// + /// Parses a Trigger string. This is + /// in this format: "[option(s)]<modifier(s)>button" where + /// "<modifier>" is something like "<Alt>" or "<Control>", "button" + /// something like "s", "F4" or "button0" and "[option]" is either + /// "[turbo]", "[centered]", "[warp]", "["delayed"]" or "["shape#"]" + ///////////////////////////////////////////////////////////////////// + + public void parse_string(string trigger) { + if (this.is_valid(trigger)) { + // copy string + string check_string = trigger; + + this.name = check_string; + + this.turbo = check_string.contains("[turbo]"); + this.delayed = check_string.contains("[delayed]"); + this.centered = check_string.contains("[centered]"); + this.warp = check_string.contains("[warp]"); + + this.shape= parse_shape( check_string ); + + // remove optional arguments + check_string = remove_optional(check_string); + + int button = this.get_mouse_button(check_string); + if (button > 0) { + this.with_mouse = true; + this.key_code = button; + this.key_sym = button; + + Gtk.accelerator_parse(check_string, null, out this._modifiers); + this.label = Gtk.accelerator_get_label(0, this.modifiers); + + string button_text = _("Button %i").printf(this.key_code); + + if (this.key_code == 1) + button_text = _("LeftButton"); + else if (this.key_code == 3) + button_text = _("RightButton"); + else if (this.key_code == 2) + button_text = _("MiddleButton"); + + this.label += button_text; + } else { + //empty triggers are ok now, they carry open options as well + if (check_string == "") { + this.label = _("Not bound"); + this.key_code = 0; + this.key_sym = 0; + this.modifiers = 0; + } else { + this.with_mouse = false; + + var display = new X.Display(); + + uint keysym = 0; + Gtk.accelerator_parse(check_string, out keysym, out this._modifiers); + this.key_code = display.keysym_to_keycode(keysym); + this.key_sym = keysym; + this.label = Gtk.accelerator_get_label(keysym, this.modifiers); + } + } + + this.label_with_specials = GLib.Markup.escape_text(this.label); + + string msg= ""; + if (this.turbo) { + msg= _("Turbo"); + } + if (this.delayed) { + if (msg == "") + msg= _("Delayed"); + else + msg += " | " + _("Delayed"); + } + if (this.centered) { + if (msg == "") + msg= _("Centered"); + else + msg += " | " + _("Centered"); + } + if (this.warp) { + if (msg == "") + msg= _("Warp"); + else + msg += " | " + _("Warp"); + } + if (this.shape == 0) { + if (msg == "") + msg= _("Auto-shaped"); + else + msg += " | " + _("Auto-shaped"); + } else if (this.shape == 1 || this.shape ==3 || this.shape == 7 || this.shape == 9) { + if (msg == "") + msg= _("Quarter pie"); + else + msg += " | " + _("Quarter pie"); + + } else if (this.shape == 2 || this.shape == 4 || this.shape == 6 || this.shape == 8) { + if (msg == "") + msg= _("Half pie"); + else + msg += " | " + _("Half pie"); + } + if (msg != "") + this.label_with_specials += (" [ " + msg + " ]"); + + } else { + this.set_unbound(); + } + } + + ///////////////////////////////////////////////////////////////////// + /// Extract shape number from trigger string + /// "[0]".."[9]" 0:auto 5:full pie (default) + /// 1,3,7,9=quarters 2,4,6,8= halves + ///////////////////////////////////////////////////////////////////// + + private int parse_shape(string trigger) { + int rs; + for( rs= 0; rs < 10; rs++ ) + if (trigger.contains("[shape%d]".printf(rs) )) + return rs; + return 5; //default= full pie + } + + ///////////////////////////////////////////////////////////////////// + /// Resets all member variables to their defaults. + ///////////////////////////////////////////////////////////////////// + + private void set_unbound() { + this.label = _("Not bound"); + this.label_with_specials = _("Not bound"); + this.name = ""; + this.key_code = 0; + this.key_sym = 0; + this.modifiers = 0; + this.turbo = false; + this.delayed = false; + this.centered = false; + this.warp = false; + this.shape = 5; //full pie + this.with_mouse = false; + } + + ///////////////////////////////////////////////////////////////////// + /// Remove optional arguments from the given string + /// "[turbo]", "[delayed]", "[warp]" "[centered]" and "[shape#]" + ///////////////////////////////////////////////////////////////////// + + public static string remove_optional(string trigger) { + string trg= trigger; + trg = trg.replace("[turbo]", ""); + trg = trg.replace("[delayed]", ""); + trg = trg.replace("[centered]", ""); + trg = trg.replace("[warp]", ""); + for (int rs= 0; rs < 10; rs++) + trg = trg.replace("[shape%d]".printf(rs), ""); + return trg; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns true, if the trigger string is in a valid format. + ///////////////////////////////////////////////////////////////////// + + private bool is_valid(string trigger) { + // remove optional arguments + string check_string = remove_optional(trigger); + + if (this.get_mouse_button(check_string) > 0) { + // it seems to be a valid mouse-trigger so replace button part, + // with something accepted by gtk, and check it with gtk + int button_index = check_string.index_of("button"); + check_string = check_string.slice(0, button_index) + "a"; + } + + //empty triggers are ok now, they carry open options as well + if (check_string == "") + return true; + + // now it shouls be a normal gtk accelerator + uint keysym = 0; + Gdk.ModifierType modifiers = 0; + Gtk.accelerator_parse(check_string, out keysym, out modifiers); + if (keysym == 0) + return false; + + return true; + } + + ///////////////////////////////////////////////////////////////////// + /// Returns the mouse button number of the given trigger string. + /// Returns -1 if it is not a mouse trigger. + ///////////////////////////////////////////////////////////////////// + + private int get_mouse_button(string trigger) { + if (trigger.contains("button")) { + // it seems to be a mouse-trigger so check the button part. + int button_index = trigger.index_of("button"); + int number = int.parse(trigger.slice(button_index + 6, trigger.length)); + if (number > 0) + return number; + } + + return -1; + } +} + +} |