FireTray/src/modules/linux/FiretrayStatusIcon.jsm

348 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/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://firetray/ctypes/linux/cairo.jsm");
Cu.import("resource://firetray/ctypes/linux/gobject.jsm");
Cu.import("resource://firetray/ctypes/linux/gdk.jsm");
Cu.import("resource://firetray/ctypes/linux/gio.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/commons.js");
firetray.Handler.subscribeLibsForClosing([cairo, gobject, gdk, gio, gtk, pango, pangocairo]);
let log = firetray.Logging.getLogger("firetray.StatusIcon");
if ("undefined" == typeof(firetray.Handler))
log.error("This module MUST be imported from/after FiretrayHandler !");
firetray.StatusIcon = {
FILENAME_BLANK: null,
initialized: false,
callbacks: {}, // pointers to JS functions. MUST LIVE DURING ALL THE EXECUTION
trayIcon: null,
themedIconApp: null,
themedIconNewMail: null,
prefAppIconNames: null,
prefNewMailIconNames: null,
defaultAppIconName: null,
defaultNewMailIconName: null,
init: function() {
this.FILENAME_BLANK = firetray.Utils.chromeToPath(
"chrome://firetray/skin/blank-icon.png");
Cu.import("resource://firetray/linux/FiretrayGtkIcons.jsm");
firetray.GtkIcons.init();
this.defineIconNames();
this.loadThemedIcons();
this.trayIcon = gtk.gtk_status_icon_new();
firetray.Handler.setIconImageDefault();
firetray.Handler.setIconTooltipDefault();
Cu.import("resource://firetray/linux/FiretrayPopupMenu.jsm");
if (!firetray.PopupMenu.init())
return false;
this.addCallbacks();
this.initialized = true;
return true;
},
shutdown: function() {
log.debug("Disabling StatusIcon");
firetray.PopupMenu.shutdown();
// FIXME: should destroy/hide icon here
firetray.GtkIcons.shutdown();
this.initialized = false;
},
defineIconNames: function() {
this.prefAppIconNames = (function() {
if (firetray.Handler.inMailApp) {
return "app_mail_icon_names";
} else if (firetray.Handler.inBrowserApp) {
return "app_browser_icon_names";
} else {
return "app_default_icon_names";
}
})();
this.defaultAppIconName = firetray.Handler.appName.toLowerCase();
this.prefNewMailIconNames = "new_mail_icon_names";
this.defaultNewMailIconName = "mail-unread";
},
loadThemedIcons: function() {
if (firetray.Handler.inMailApp) {
let newMailIconNames = this.getNewMailIconNames();
if (this.themedIconNewMail) gobject.g_object_unref(this.themedIconNewMail);
this.themedIconNewMail = this.initThemedIcon(newMailIconNames);
}
let appIconNames = this.getAppIconNames();
if (this.themedIconApp) gobject.g_object_unref(this.themedIconApp);
this.themedIconApp = this.initThemedIcon(appIconNames);
},
getAppIconNames: function() {
let appIconNames = firetray.Utils.getArrayPref(this.prefAppIconNames);
appIconNames.push(this.defaultAppIconName);
return appIconNames;
},
getNewMailIconNames: function() {
let newMailIconNames = firetray.Utils.getArrayPref(this.prefNewMailIconNames);
newMailIconNames.push(this.defaultNewMailIconName);
return newMailIconNames;
},
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() {
/* 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);
gobject.g_signal_connect(this.trayIcon, "popup-menu",
firetray.StatusIcon.callbacks.menuPopup, firetray.PopupMenu.menu);
this.callbacks.onScroll = gtk.GCallbackOnScroll_t(firetray.StatusIcon.onScroll);
gobject.g_signal_connect(this.trayIcon, "scroll-event",
firetray.StatusIcon.callbacks.onScroll, null);
log.debug("showHideAllWindows: "+firetray.Handler.hasOwnProperty("showHideAllWindows"));
this.callbacks.iconActivate = gtk.GCallbackStatusIconActivate_t(
firetray.Handler.showHideAllWindows);
let handlerId = gobject.g_signal_connect(firetray.StatusIcon.trayIcon,
"activate", firetray.StatusIcon.callbacks.iconActivate, null);
log.debug("g_connect activate="+handlerId);
this.callbacks.iconMiddleClick = gtk.GCallbackStatusIconMiddleClick_t(
firetray.Handler.activateLastWindowCb);
handlerId = gobject.g_signal_connect(firetray.StatusIcon.trayIcon,
"button-press-event", firetray.StatusIcon.callbacks.iconMiddleClick, null);
log.debug("g_connect middleClick="+handlerId);
},
onScroll: function(icon, event, data) {
if (!firetray.Utils.prefService.getBoolPref("scroll_hides"))
return;
let iconGpointer = ctypes.cast(icon, gobject.gpointer);
let gdkEventScroll = ctypes.cast(event, gdk.GdkEventScroll.ptr);
let scroll_mode = firetray.Utils.prefService.getCharPref("scroll_mode");
let direction = gdkEventScroll.contents.direction;
switch(direction) {
case gdk.GDK_SCROLL_UP:
log.debug("SCROLL UP");
if (scroll_mode === "down_hides")
firetray.Handler.showAllWindows();
else if (scroll_mode === "up_hides")
firetray.Handler.hideAllWindows();
break;
case gdk.GDK_SCROLL_DOWN:
log.debug("SCROLL DOWN");
if (scroll_mode === "down_hides")
firetray.Handler.hideAllWindows();
else if (scroll_mode === "up_hides")
firetray.Handler.showAllWindows();
break;
default:
log.error("SCROLL UNKNOWN");
}
},
setIconImageFromFile: function(filename) {
if (!firetray.StatusIcon.trayIcon)
log.error("Icon missing");
log.debug(filename);
gtk.gtk_status_icon_set_from_file(firetray.StatusIcon.trayIcon,
filename);
},
setIconImageFromGIcon: function(gicon) {
if (!firetray.StatusIcon.trayIcon || !gicon)
log.error("Icon missing");
log.debug(gicon);
gtk.gtk_status_icon_set_from_gicon(firetray.StatusIcon.trayIcon, gicon);
}
}; // firetray.StatusIcon
firetray.Handler.setIconImageDefault = function() {
log.debug("setIconImageDefault");
if (!firetray.StatusIcon.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.StatusIcon.setIconImageFromGIcon(firetray.StatusIcon.themedIconApp);
else if (appIconType === FIRETRAY_APPLICATION_ICON_TYPE_CUSTOM) {
let appIconFilename = firetray.Utils.prefService.getCharPref("app_icon_filename");
firetray.StatusIcon.setIconImageFromFile(appIconFilename);
}
};
firetray.Handler.setIconImageNewMail = function() {
firetray.StatusIcon.setIconImageFromGIcon(firetray.StatusIcon.themedIconNewMail);
};
firetray.Handler.setIconImageFromFile = firetray.StatusIcon.setIconImageFromFile;
// GTK bug: Gdk-CRITICAL **: IA__gdk_window_get_root_coords: assertion `GDK_IS_WINDOW (window)' failed
firetray.Handler.setIconTooltip = function(toolTipStr) {
if (!firetray.StatusIcon.trayIcon)
return false;
try {
gtk.gtk_status_icon_set_tooltip_text(firetray.StatusIcon.trayIcon,
toolTipStr);
} catch (x) {
log.error(x);
return false;
}
return true;
};
firetray.Handler.setIconTooltipDefault = function() {
if (!this.appName)
throw "application name not initialized";
this.setIconTooltip(this.appName);
};
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.StatusIcon.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.StatusIcon.MIN_FONT_SIZE) {
sz = firetray.StatusIcon.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_NEAREST,255);
gobject.g_object_unref(bufAlpha);
gtk.gtk_status_icon_set_from_pixbuf(firetray.StatusIcon.trayIcon, dest);
} catch (x) {
log.error(x);
return false;
}
return true;
};
firetray.Handler.setIconVisibility = function(visible) {
if (!firetray.StatusIcon.trayIcon)
return false;
gtk.gtk_status_icon_set_visible(firetray.StatusIcon.trayIcon, visible);
return true;
};