FireTray/src/modules/linux/FiretrayGtkStatusIcon.jsm

342 lines
13 KiB
JavaScript

/* -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
var EXPORTED_SYMBOLS = [ "firetray" ];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://firetray/ctypes/linux/cairo.jsm");
Cu.import("resource://firetray/ctypes/linux/gdk.jsm");
Cu.import("resource://firetray/ctypes/linux/gio.jsm");
Cu.import("resource://firetray/ctypes/linux/gobject.jsm");
Cu.import("resource://firetray/ctypes/linux/gtk.jsm");
Cu.import("resource://firetray/ctypes/linux/pango.jsm");
Cu.import("resource://firetray/ctypes/linux/pangocairo.jsm");
Cu.import("resource://firetray/linux/FiretrayGtkIcons.jsm");
Cu.import("resource://firetray/commons.js");
firetray.Handler.subscribeLibsForClosing([cairo, gdk, gio, gobject, gtk, pango,
pangocairo]);
let log = firetray.Logging.getLogger("firetray.GtkStatusIcon");
if ("undefined" == typeof(firetray.Handler))
log.error("This module MUST be imported from/after FiretrayStatusIcon !");
firetray.GtkStatusIcon = {
MIN_FONT_SIZE: 4,
FILENAME_BLANK: null,
GTK_THEME_ICON_PATH: null,
initialized: false,
callbacks: {},
trayIcon: null,
themedIconApp: null,
themedIconNewMail: null,
init: function() {
this.FILENAME_BLANK = firetray.Utils.chromeToPath(
"chrome://firetray/skin/icons/blank-icon.png");
firetray.GtkIcons.init();
this.loadThemedIcons();
this.trayIcon = gtk.gtk_status_icon_new();
firetray.Handler.setIconImageDefault();
firetray.Handler.setIconTooltipDefault();
this.addCallbacks();
this.initialized = true;
return true;
},
shutdown: function() {
log.debug("Disabling GtkStatusIcon");
firetray.GtkIcons.shutdown();
// FIXME: XXX destroy icon here
this.initialized = false;
},
loadThemedIcons: function() {
if (firetray.Handler.inMailApp) {
let newMailIconNames = firetray.StatusIcon.getNewMailIconNames();
if (this.themedIconNewMail) gobject.g_object_unref(this.themedIconNewMail);
this.themedIconNewMail = this.initThemedIcon(newMailIconNames);
}
let appIconNames = firetray.StatusIcon.getAppIconNames();
if (this.themedIconApp) gobject.g_object_unref(this.themedIconApp);
this.themedIconApp = this.initThemedIcon(appIconNames);
},
initThemedIcon: function(names) {
if (!firetray.js.isArray(names)) throw new TypeError();
log.debug("themedIconNames="+names);
let namesLen = names.length;
log.debug("themedIconNamesLen="+namesLen);
let themedIconNames = ctypes.char.ptr.array(namesLen)();
for (let i=0; i<namesLen; ++i)
themedIconNames[i] = ctypes.char.array()(names[i]);
log.debug("themedIconNames="+themedIconNames);
let themedIcon = gio.g_themed_icon_new_from_names(themedIconNames, namesLen);
log.debug("themedIcon="+themedIcon);
return themedIcon;
},
addCallbacks: function() {
Cu.import("resource://firetray/linux/FiretrayPopupMenu.jsm");
/* NOTE: here we do use a function handler (instead of a function
definition) because we need the args passed to it ! As a consequence, we
need to abandon 'this' in PopupMenu.popup() */
this.callbacks.menuPopup = gtk.GCallbackMenuPopup_t(firetray.PopupMenu.popup); // void return, no sentinel
gobject.g_signal_connect(this.trayIcon, "popup-menu",
firetray.GtkStatusIcon.callbacks.menuPopup, firetray.PopupMenu.menu);
this.callbacks.onScroll = gtk.GCallbackOnScroll_t(
firetray.GtkStatusIcon.onScroll, null, FIRETRAY_CB_SENTINEL);
gobject.g_signal_connect(this.trayIcon, "scroll-event",
firetray.GtkStatusIcon.callbacks.onScroll, null);
log.debug("showHideAllWindows: "+firetray.Handler.hasOwnProperty("showHideAllWindows"));
this.callbacks.iconActivate = gtk.GCallbackStatusIconActivate_t(
firetray.GtkStatusIcon.onClick, null, FIRETRAY_CB_SENTINEL);
let handlerId = gobject.g_signal_connect(firetray.GtkStatusIcon.trayIcon,
"activate", firetray.GtkStatusIcon.callbacks.iconActivate, null);
log.debug("g_connect activate="+handlerId);
let pref = firetray.Utils.prefService.getIntPref("middle_click");
this.attachMiddleClickCallback(pref);
},
attachMiddleClickCallback: function(pref) {
log.debug("attachMiddleClickCallback pref="+pref);
if (pref === FIRETRAY_MIDDLE_CLICK_ACTIVATE_LAST) {
this.callbacks.iconMiddleClick = gtk.GCallbackStatusIconMiddleClick_t(
firetray.Handler.activateLastWindowCb, null, FIRETRAY_CB_SENTINEL);
} else if (pref === FIRETRAY_MIDDLE_CLICK_SHOW_HIDE) {
this.callbacks.iconMiddleClick = gtk.GCallbackStatusIconMiddleClick_t(
function(widget, event, data) {firetray.Handler.showHideAllWindows(); return true;},
null, FIRETRAY_CB_SENTINEL);
} else {
log.error("Unknown pref value for 'middle_click': "+pref);
return;
}
this.callbacks.iconMiddleClickId = gobject.g_signal_connect(
firetray.GtkStatusIcon.trayIcon,
"button-press-event", firetray.GtkStatusIcon.callbacks.iconMiddleClick,
null);
log.debug("g_connect middleClick="+this.callbacks.iconMiddleClickId);
},
detachMiddleClickCallback: function() {
log.debug("detachMiddleClickCallback");
gobject.g_signal_handler_disconnect(
firetray.GtkStatusIcon.trayIcon,
gobject.gulong(this.callbacks.iconMiddleClickId)
);
delete this.callbacks.iconMiddleClickId;
},
onScroll: function(icon, event, data) {
let gdkEventScroll = ctypes.cast(event, gdk.GdkEventScroll.ptr);
let direction = gdkEventScroll.contents.direction;
firetray.StatusIcon.onScroll(direction);
let stopPropagation = false;
return stopPropagation;
},
onClick: function(gtkStatusIcon, userData) {
firetray.Handler.showHideAllWindows();
let stopPropagation = true;
return stopPropagation;
},
setIconImageFromFile: function(filename) {
if (!firetray.GtkStatusIcon.trayIcon)
log.error("Icon missing");
log.debug(filename);
gtk.gtk_status_icon_set_from_file(firetray.GtkStatusIcon.trayIcon,
filename);
},
setIconImageFromGIcon: function(gicon) {
if (!firetray.GtkStatusIcon.trayIcon || !gicon)
log.error("Icon missing");
log.debug(gicon);
gtk.gtk_status_icon_set_from_gicon(firetray.GtkStatusIcon.trayIcon, gicon);
},
}; // GtkStatusIcon
firetray.StatusIcon.initImpl =
firetray.GtkStatusIcon.init.bind(firetray.GtkStatusIcon);
firetray.StatusIcon.shutdownImpl =
firetray.GtkStatusIcon.shutdown.bind(firetray.GtkStatusIcon);
firetray.StatusIcon.middleClickActionChanged = function() {
log.debug("middleClickActionChanged");
let pref = firetray.Utils.prefService.getIntPref("middle_click");
firetray.GtkStatusIcon.detachMiddleClickCallback();
firetray.GtkStatusIcon.attachMiddleClickCallback(pref);
};
firetray.Handler.loadIcons = firetray.GtkStatusIcon.loadThemedIcons;
firetray.Handler.setIconImageDefault = function() {
log.debug("setIconImageDefault");
if (!firetray.GtkStatusIcon.themedIconApp)
throw "Default application themed icon not set";
let appIconType = firetray.Utils.prefService.getIntPref("app_icon_type");
if (appIconType === FIRETRAY_APPLICATION_ICON_TYPE_THEMED) {
firetray.GtkStatusIcon.setIconImageFromGIcon(
firetray.GtkStatusIcon.themedIconApp);
} else if (appIconType === FIRETRAY_APPLICATION_ICON_TYPE_CUSTOM) {
firetray.Handler.setIconImageCustom("app_icon_custom");
}
};
firetray.Handler.setIconImageNewMail = function() {
firetray.GtkStatusIcon.setIconImageFromGIcon(
firetray.GtkStatusIcon.themedIconNewMail);
};
firetray.Handler.setIconImageCustom = function(prefname) {
let prefCustomIconPath = firetray.Utils.prefService.getCharPref(prefname);
firetray.GtkStatusIcon.setIconImageFromFile(prefCustomIconPath);
};
// GTK bug: Gdk-CRITICAL **: IA__gdk_window_get_root_coords: assertion `GDK_IS_WINDOW (window)' failed
firetray.Handler.setIconTooltip = function(toolTipStr) {
if (!firetray.GtkStatusIcon.trayIcon)
return false;
log.debug("setIconTooltip, toolTipStr="+toolTipStr);
try {
gtk.gtk_status_icon_set_tooltip_text(firetray.GtkStatusIcon.trayIcon,
toolTipStr);
} catch (x) {
log.error(x);
return false;
}
return true;
};
firetray.Handler.setIconText = function(text, color) { // FIXME: function too long
log.debug("setIconText, color="+color);
if (typeof(text) != "string")
throw new TypeError();
try {
// build background from image
let specialIcon = gdk.gdk_pixbuf_new_from_file(
firetray.GtkStatusIcon.FILENAME_BLANK, null); // GError **error);
let dest = gdk.gdk_pixbuf_copy(specialIcon);
let w = gdk.gdk_pixbuf_get_width(specialIcon);
let h = gdk.gdk_pixbuf_get_height(specialIcon);
// prepare colors/alpha
let colorMap = gdk.gdk_screen_get_system_colormap(gdk.gdk_screen_get_default());
let visual = gdk.gdk_colormap_get_visual(colorMap);
let visualDepth = visual.contents.depth;
log.debug("colorMap="+colorMap+" visual="+visual+" visualDepth="+visualDepth);
let fore = new gdk.GdkColor;
fore.pixel = fore.red = fore.green = fore.blue = 0;
let alpha = new gdk.GdkColor;
alpha.pixel = alpha.red = alpha.green = alpha.blue = 0xFFFF;
if (!fore || !alpha)
log.warn("Undefined GdkColor fore or alpha");
gdk.gdk_color_parse(color, fore.address());
if(fore.red == alpha.red && fore.green == alpha.green && fore.blue == alpha.blue) {
alpha.red=0; // make sure alpha is different from fore
}
gdk.gdk_colormap_alloc_color(colorMap, fore.address(), true, true);
gdk.gdk_colormap_alloc_color(colorMap, alpha.address(), true, true);
// build pixmap with rectangle
let pm = gdk.gdk_pixmap_new(null, w, h, visualDepth);
let pmDrawable = ctypes.cast(pm, gdk.GdkDrawable.ptr);
let cr = gdk.gdk_cairo_create(pmDrawable);
gdk.gdk_cairo_set_source_color(cr, alpha.address());
cairo.cairo_rectangle(cr, 0, 0, w, h);
cairo.cairo_set_source_rgb(cr, 1, 1, 1);
cairo.cairo_fill(cr);
// build text
let scratch = gtk.gtk_window_new(gtk.GTK_WINDOW_TOPLEVEL);
let layout = gtk.gtk_widget_create_pango_layout(scratch, null);
gtk.gtk_widget_destroy(scratch);
let fnt = pango.pango_font_description_from_string("Sans 18");
pango.pango_font_description_set_weight(fnt,pango.PANGO_WEIGHT_SEMIBOLD);
pango.pango_layout_set_spacing(layout,0);
pango.pango_layout_set_font_description(layout, fnt);
log.debug("layout="+layout);
log.debug("text="+text);
pango.pango_layout_set_text(layout, text,-1);
let tw = new ctypes.int;
let th = new ctypes.int;
let sz;
let border = 4;
pango.pango_layout_get_pixel_size(layout, tw.address(), th.address());
log.debug("tw="+tw.value+" th="+th.value);
// fit text to the icon by decreasing font size
while ( tw.value > (w - border) || th.value > (h - border) ) {
sz = pango.pango_font_description_get_size(fnt);
if(sz < firetray.GtkStatusIcon.MIN_FONT_SIZE) {
sz = firetray.GtkStatusIcon.MIN_FONT_SIZE;
break;
}
sz -= pango.PANGO_SCALE;
pango.pango_font_description_set_size(fnt,sz);
pango.pango_layout_set_font_description(layout, fnt);
pango.pango_layout_get_pixel_size(layout, tw.address(), th.address());
}
log.debug("tw="+tw.value+" th="+th.value);
pango.pango_font_description_free(fnt);
// center text
let px = (w-tw.value)/2;
let py = (h-th.value)/2;
// draw text on pixmap
gdk.gdk_cairo_set_source_color(cr, fore.address());
cairo.cairo_move_to(cr, px, py);
pangocairo.pango_cairo_show_layout(cr, layout);
cairo.cairo_destroy(cr);
gobject.g_object_unref(layout);
let buf = gdk.gdk_pixbuf_get_from_drawable(null, pmDrawable, null, 0, 0, 0, 0, w, h);
gobject.g_object_unref(pm);
log.debug("alpha="+alpha);
let alphaRed = gobject.guint16(alpha.red);
let alphaRed_guchar = ctypes.cast(alphaRed, gobject.guchar);
let alphaGreen = gobject.guint16(alpha.green);
let alphaGreen_guchar = ctypes.cast(alphaGreen, gobject.guchar);
let alphaBlue = gobject.guint16(alpha.blue);
let alphaBlue_guchar = ctypes.cast(alphaBlue, gobject.guchar);
let bufAlpha = gdk.gdk_pixbuf_add_alpha(buf, true, alphaRed_guchar, alphaGreen_guchar, alphaBlue_guchar);
gobject.g_object_unref(buf);
// merge the rendered text on top
gdk.gdk_pixbuf_composite(bufAlpha,dest,0,0,w,h,0,0,1,1,gdk.GDK_INTERP_BILINEAR,255);
gobject.g_object_unref(bufAlpha);
log.debug("gtk_status_icon_set_from_pixbuf="+dest);
gtk.gtk_status_icon_set_from_pixbuf(firetray.GtkStatusIcon.trayIcon, dest);
} catch (x) {
log.error(x);
return false;
}
return true;
};
firetray.Handler.setIconVisibility = function(visible) {
if (!firetray.GtkStatusIcon.trayIcon)
return false;
gtk.gtk_status_icon_set_visible(firetray.GtkStatusIcon.trayIcon, visible);
return true;
};