From aadf7278161e361e1d85b7ee84011e94e73cc05b Mon Sep 17 00:00:00 2001 From: foudfou Date: Sun, 1 Jan 2012 01:45:48 +0100 Subject: [PATCH] * complete per-window registration and show/hide handeling * fix storage of Gdk-/GtkWindows pointers (ctypesMap.jsm) --- src/chrome/content/overlay.js | 17 +- src/install.rdf | 2 +- src/modules/FiretrayHandler.jsm | 34 ++- src/modules/commons.js | 8 + src/modules/ctypesMap.jsm | 68 ++++++ src/modules/gtk2/FiretrayStatusIcon.jsm | 1 + src/modules/gtk2/FiretrayWindow.jsm | 291 +++++++++++++----------- 7 files changed, 272 insertions(+), 149 deletions(-) create mode 100644 src/modules/ctypesMap.jsm diff --git a/src/chrome/content/overlay.js b/src/chrome/content/overlay.js index 72ff241..532b856 100644 --- a/src/chrome/content/overlay.js +++ b/src/chrome/content/overlay.js @@ -24,7 +24,10 @@ var firetrayChrome = { return false; } + LOG("Handler initialized: "+firetray.Handler.initialized); let init = firetray.Handler.initialized || firetray.Handler.init(); + + LOG("ONLOAD"); firetray.Handler.dumpWindows(); firetray.Handler.registerWindow(win); // update unread messages count @@ -55,15 +58,19 @@ var firetrayChrome = { // TODO: prevent preceding warning about closing multiple tabs (browser.tabs.warnOnClose) onClose: function(event) { LOG('Firetray CLOSE'); + let win = event.originalTarget; + if (!win instanceof ChromeWindow) + throw new TypeError('originalTarget not a ChromeWindow'); + let hides_on_close = firetray.Utils.prefService.getBoolPref('hides_on_close'); let hides_single_window = firetray.Utils.prefService.getBoolPref('hides_single_window'); LOG('hides_on_close: '+hides_on_close+', hides_single_window='+hides_single_window); - LOG('event.originalTarget: '+event.originalTarget); if (hides_on_close) { - if (hides_single_window) - firetray.Window.hideWindow(window); - else - firetray.Handler.hideAllWindows(window); + if (hides_single_window) { + let winId = firetray.Handler.getWindowIdFromChromeWindow(win); + firetray.Handler.hideSingleWindow(winId); + } else + firetray.Handler.hideAllWindows(); event && event.preventDefault(); // no event when called directly (xul) } }, diff --git a/src/install.rdf b/src/install.rdf index 23cfd80..cc219c0 100644 --- a/src/install.rdf +++ b/src/install.rdf @@ -5,7 +5,7 @@ true 2 FireTray - 0.4.0a5 + 0.4.0a6 Hua Luo, Francesco Solero, Foudil BRÉTEL Hua Luo, Francesco Solero (Firetray original authors) https://github.com/foudfou/firetray diff --git a/src/modules/FiretrayHandler.jsm b/src/modules/FiretrayHandler.jsm index c061b4c..69e387b 100644 --- a/src/modules/FiretrayHandler.jsm +++ b/src/modules/FiretrayHandler.jsm @@ -20,7 +20,7 @@ if ("undefined" == typeof(firetray)) { }; /** - * Singleton object and abstraction for tray icon management. + * Singleton object and abstraction for windows and tray icon management. */ // NOTE: modules work outside of the window scope. Unlike scripts in the // chrome, modules don't have access to objects such as window, document, or @@ -35,7 +35,9 @@ firetray.Handler = { FILENAME_NEWMAIL: null, runtimeOS: null, inMailApp: false, - windows: [], + windows: {}, + windowsCount: 0, + visibleWindowsCount: 0, init: function() { // does creates icon this.appName = Services.appinfo.name.toLowerCase(); @@ -58,17 +60,16 @@ firetray.Handler = { LOG('FiretrayStatusIcon imported'); Cu.import("resource://firetray/gtk2/FiretrayWindow.jsm"); LOG('FiretrayWindow imported'); - - // instanciate tray icon - firetray.StatusIcon.init(); - LOG('StatusIcon initialized'); - break; default: ERROR("FIRETRAY: only Linux platform supported at this time. Firetray not loaded"); return false; } + // instanciate tray icon + firetray.StatusIcon.init(); + LOG('StatusIcon initialized'); + // check if in mail app var mozAppId = Services.appinfo.ID; if (mozAppId === THUNDERBIRD_ID || mozAppId === SEAMONKEY_ID) { @@ -107,15 +108,30 @@ firetray.Handler = { return true; }, - // these get overridden in OS-specific Icon handlers + // these get overridden in OS-specific Window handlers setImage: function(filename) {}, setImageDefault: function() {}, setText: function(text, color) {}, setTooltip: function(localizedMessage) {}, setTooltipDefault: function() {}, - showHideAllWindows: function() {}, registerWindow: function(win) {}, unregisterWindow: function(win) {}, + getWindowIdFromChromeWindow: function(win) {}, + hideSingleWindow: function(winId) {}, + showSingleWindow: function(winId) {}, + showHideAllWindows: function() {}, + + showAllWindows: function() { + for (let winId in firetray.Handler.windows) + if (!firetray.Handler.windows[winId].visibility) + firetray.Handler.showSingleWindow(winId); + }, + hideAllWindows: function() { + for (let winId in firetray.Handler.windows) { + if (firetray.Handler.windows[winId].visibility) + firetray.Handler.hideSingleWindow(winId); + } + }, _getBaseOrXULWindowFromDOMWindow: function(win, winType) { let winInterface, winOut; diff --git a/src/modules/commons.js b/src/modules/commons.js index 3fe7372..28ff8a5 100644 --- a/src/modules/commons.js +++ b/src/modules/commons.js @@ -198,3 +198,11 @@ function isEmpty(obj) { function strEquals(obj1, obj2) { return obj1.toString() === obj2.toString(); } + +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types +function DeleteError(message) { + this.name = "DeleteError"; + this.message = message || "Could not delete object memeber"; +} +DeleteError.prototype = new Error(); +DeleteError.prototype.constructor = DeleteError; diff --git a/src/modules/ctypesMap.jsm b/src/modules/ctypesMap.jsm new file mode 100644 index 0000000..6fc430c --- /dev/null +++ b/src/modules/ctypesMap.jsm @@ -0,0 +1,68 @@ +/* -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +var EXPORTED_SYMBOLS = [ "ctypesMap", "CTYPES_ARRAY_MAX_SIZE" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/ctypes.jsm"); +Cu.import("resource://firetray/logging.jsm"); +Cu.import("resource://firetray/commons.js"); + +const FIRETRAY_WINDOW_COUNT_MAX = 64; + +/** + * basic Hash mapping a key (of any type) to a cell in a ctypes array + */ +function ctypesMap(t) { + this.array = ctypes.ArrayType(t)(FIRETRAY_WINDOW_COUNT_MAX); + this.indexLast = -1; + this.freedCells = []; // indices of freed cells + this.count = 0; // count of actually stored things + this.map = {}; // map key -> index +}; + +ctypesMap.prototype.get = function(key) { + if (!this.map.hasOwnProperty(key)) + throw new RangeError('Unknown key: '+key); + + return this.array[this.map[key]]; +}; + +ctypesMap.prototype.insert = function(key, item) { + if (this.map.hasOwnProperty(key)) { // replace + LOG("REPLACE"); + this.array[this.map[key]] = item; + + } else if (this.freedCells.length) { + LOG("USE FREE CELL"); + let idx = this.freedCells.shift(); + this.array[idx] = item; + this.map[key] = idx; + this.count += 1; + + } else { + let indexNext = this.indexLast + 1; + if (indexNext >= FIRETRAY_WINDOW_COUNT_MAX) + throw new RangeError('Array overflow'); + + this.indexLast = indexNext; + this.array[this.indexLast] = item; + this.map[key] = this.indexLast; + this.count += 1; + } +}; + +ctypesMap.prototype.remove = function(key) { + if (!this.map.hasOwnProperty(key)) + throw new RangeError('Unknown key: '+key); + LOG("FREE CELL"); + + let idx = this.map[key]; + if (!delete this.map[key]) + throw new DeleteError(); + this.freedCells.unshift(idx); + this.count -= 1; +}; + diff --git a/src/modules/gtk2/FiretrayStatusIcon.jsm b/src/modules/gtk2/FiretrayStatusIcon.jsm index 30f589d..2428891 100644 --- a/src/modules/gtk2/FiretrayStatusIcon.jsm +++ b/src/modules/gtk2/FiretrayStatusIcon.jsm @@ -112,6 +112,7 @@ firetray.StatusIcon = { }; // firetray.StatusIcon + firetray.Handler.setImage = function(filename) { if (!firetray.StatusIcon.trayIcon) return false; diff --git a/src/modules/gtk2/FiretrayWindow.jsm b/src/modules/gtk2/FiretrayWindow.jsm index 325e475..d3278a3 100644 --- a/src/modules/gtk2/FiretrayWindow.jsm +++ b/src/modules/gtk2/FiretrayWindow.jsm @@ -16,6 +16,7 @@ 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/ctypesMap.jsm"); Cu.import("resource://firetray/gobject.jsm"); Cu.import("resource://firetray/gdk.jsm"); Cu.import("resource://firetray/gtk.jsm"); @@ -42,95 +43,6 @@ var _find_data_t = ctypes.StructType("_find_data_t", [ ]); -firetray.Handler.registerWindow = function(win) { - LOG("register window"); - let that = this; - - // register - let [gtkWin, gdkWin, xid] = firetray.Window.getWindowsFromChromeWindow(win); - this.windows[xid] = {}; - this.windows[xid].win = win; - this.windows[xid].gtkWin = gtkWin; - this.windows[xid].gdkWin = gdkWin; - LOG("window "+xid+" registered"); - /* NOTE: it should not be necessary to gtk_widget_add_events(gtkWin, - gdk.GDK_ALL_EVENTS_MASK); */ - - try { - /* NOTE: we could try to catch the "delete-event" here and block - delete_event_cb (in gtk2/nsWindow.cpp), but we prefer to use the - provided "close" JS event */ - - /* we'll catch minimize events with Gtk: - http://stackoverflow.com/questions/8018328/what-is-the-gtk-event-called-when-a-window-minimizes */ - this.windows[xid].windowStateCb = gtk.GCallbackWindowStateEvent_t(firetray.Window.windowState); - this.windows[xid].windowStateCbId = gobject.g_signal_connect(gtkWin, "window-state-event", this.windows[xid].windowStateCb, null); - LOG("g_connect window-state-event="+this.windows[xid].windowStateCbId); - - } catch (x) { - this._unregisterWindowByXID(xid); - ERROR(x); - return false; - } - - return true; -}; - -firetray.Handler.unregisterWindow = function(win) { - LOG("unregister window"); - - try { - let xid = firetray.Window.getXIDFromChromeWindow(win); - return this._unregisterWindowByXID(xid); - } catch (x) { - ERROR(x); - } - return false; -}; - -firetray.Handler._unregisterWindowByXID = function(xid) { - try { - if (this.windows.hasOwnProperty(xid)) - delete this.windows[xid]; - else { - ERROR("can't unregister unknown window "+xid); - return false; - } - } catch (x) { - ERROR(x); - return false; - } - LOG("window "+xid+" unregistered"); - return true; -}; - -firetray.Handler.showSingleWindow = function(xid) { - try { - // keep z-order - and try to restore previous state - LOG("gdkWin="+firetray.Handler.windows[xid].gdkWin); - gdk.gdk_window_show_unraised(firetray.Handler.windows[xid].gdkWin); - // need to restore *after* showing for correction - // firetray.Window._restoreWindowPositionSizeState(xid); - } catch (x) { - ERROR(x); - } -}; - -firetray.Handler.showHideAllWindows = function(gtkStatusIcon, userData) { - LOG("showHideAllWindows: "+userData); - - // NOTE: showHideAllWindows being a callback, we need to use 'firetray.Handler' - // explicitely instead of 'this' - for (let xid in firetray.Handler.windows) { - LOG("show xid="+xid); - firetray.Handler.showSingleWindow(xid); - } - - let stopPropagation = true; - return stopPropagation; -}; - - firetray.Window = { /** @@ -143,9 +55,9 @@ firetray.Window = { */ getGtkWindowHandle: function(window) { let baseWindow = window - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIBaseWindow); + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIBaseWindow); // Tag the base window let oldTitle = baseWindow.title; @@ -194,14 +106,10 @@ firetray.Window = { let gtkWin = ctypes.cast(gtkWidget, gtk.GtkWindow.ptr); let winTitle = gtk.gtk_window_get_title(gtkWin); - try { - if (!winTitle.isNull()) { - LOG(inTitle+" = "+winTitle); - if (libc.strcmp(inTitle, winTitle) == 0) - data.contents.outWindow = gtkWin; - } - } catch (x) { - ERROR(x); + if (!winTitle.isNull()) { + LOG(inTitle+" = "+winTitle); + if (libc.strcmp(inTitle, winTitle) == 0) + data.contents.outWindow = gtkWin; } }, @@ -232,9 +140,7 @@ firetray.Window = { /** consider using getXIDFromChromeWindow() if you only need the XID */ getWindowsFromChromeWindow: function(win) { let gtkWin = firetray.Window.getGtkWindowHandle(win); - LOG("gtkWin="+gtkWin); let gdkWin = firetray.Window.getGdkWindowFromGtkWindow(gtkWin); - LOG("gdkWin="+gdkWin); let xid = firetray.Window.getXIDFromGdkWindow(gdkWin); LOG("XID="+xid); return [gtkWin, gdkWin, xid]; @@ -248,33 +154,17 @@ firetray.Window = { return null; }, - hideWindow: function(win) { - LOG("hideWindow"); - let xid = this.getXIDFromChromeWindow(win); - LOG("found xid="+xid); - try { - firetray.Window._saveWindowPositionSizeState(xid); - - // hide window - NOTE: we don't use BaseWindow.visibility to have full - // control - gdk.gdk_window_hide(firetray.Handler.windows[xid].gdkWin); - } catch (x) { - ERROR(x); - } - }, - - _saveWindowPositionSizeState: function(xid) { - let gdkWin = firetray.Handler.windows[xid].gdkWin; + saveWindowPositionSizeState: function(xid) { + let gtkWin = firetray.Handler.gtkWindows.get(xid); + let gdkWin = firetray.Handler.gdkWindows.get(xid); try { let gx = new gobject.gint; let gy = new gobject.gint; - // gtk.gtk_window_get_position(gtkWin, gx.address(), gy.address()); - gdk.gdk_window_get_position(gdkWin, gx.address(), gy.address()); + gtk.gtk_window_get_position(gtkWin, gx.address(), gy.address()); let gwidth = new gobject.gint; let gheight = new gobject.gint; - // gtk.gtk_window_get_size(gtkWin, gwidth.address(), gheight.address()); - gdk.gdk_drawable_get_size(ctypes.cast(gdkWin, gdk.GdkDrawable.ptr), gwidth.address(), gheight.address()); - let windowState = gdk.gdk_window_get_state(firetray.Handler.windows[xid].gdkWin); - LOG("gx="+gx+", gy="+gy+", gwidth="+gwidth+", gheight="+gheight+", windowState="+windowState); + gtk.gtk_window_get_size(gtkWin, gwidth.address(), gheight.address()); + let windowState = gdk.gdk_window_get_state(gdkWin); + LOG("save: gx="+gx+", gy="+gy+", gwidth="+gwidth+", gheight="+gheight+", windowState="+windowState); firetray.Handler.windows[xid].savedX = gx; firetray.Handler.windows[xid].savedY = gy; firetray.Handler.windows[xid].savedWidth = gwidth; @@ -286,25 +176,29 @@ firetray.Window = { }, - _restoreWindowPositionSizeState: function(xid) { - let gdkWin = firetray.Handler.windows[xid].gdkWin; + restoreWindowPositionSizeState: function(xid) { if (!firetray.Handler.windows[xid].savedX) return; // windows[xid].saved* may not be initialized - LOG("restore gdkWin: "+gdkWin+", x="+firetray.Handler.windows[xid].savedX+", y="+firetray.Handler.windows[xid].savedY+", w="+firetray.Handler.windows[xid].savedWidth+", h="+firetray.Handler.windows[xid].savedHeight); + let gtkWin = firetray.Handler.gtkWindows.get(xid); + let gdkWin = firetray.Handler.gdkWindows.get(xid); + + LOG("restore: x="+firetray.Handler.windows[xid].savedX+", y="+firetray.Handler.windows[xid].savedY+", w="+firetray.Handler.windows[xid].savedWidth+", h="+firetray.Handler.windows[xid].savedHeight); + // NOTE: unfortunately, this is the best way I found *inside GTK* to + // restore position and size: gdk.gdk_window_move_resize doesn't work + // well. And unfortunately, we need to show the window before restoring + // position and size :-( TODO: Might be worth trying with x11 or + // BaseWindow.visibility ? try { - gdk.gdk_window_move_resize(gdkWin, - firetray.Handler.windows[xid].savedX, - firetray.Handler.windows[xid].savedY, - firetray.Handler.windows[xid].savedWidth, - firetray.Handler.windows[xid].savedHeight); + gtk.gtk_window_move(gtkWin, firetray.Handler.windows[xid].savedX, firetray.Handler.windows[xid].savedY); + gtk.gtk_window_resize(gtkWin, firetray.Handler.windows[xid].savedWidth, firetray.Handler.windows[xid].savedHeight); // firetray.Handler.windows[xid].savedState } catch (x) { ERROR(x); } }, - windowState: function(gtkWidget, gdkEventState, userData){ + onWindowState: function(gtkWidget, gdkEventState, userData){ // LOG("window-state-event"); // if(event->new_window_state & GDK_WINDOW_STATE_ICONIFIED){ let stopPropagation = true; @@ -312,3 +206,132 @@ firetray.Window = { } }; // firetray.Window + + +///////////////////////// firetray.Handler overriding ///////////////////////// + +// NOTE: storing ctypes pointers into a JS object doesn't work: pointers are +// "evolving" after a while (maybe due to back and forth conversion). So we +// need to store them into a real ctypes array ! +firetray.Handler.gtkWindows = new ctypesMap(gtk.GtkWindow.ptr), +firetray.Handler.gdkWindows = new ctypesMap(gdk.GdkWindow.ptr), + +/** debug facility */ +firetray.Handler.dumpWindows = function() { + LOG(firetray.Handler.windowsCount); + for (let winId in firetray.Handler.windows) + LOG(winId+"="+firetray.Handler.gtkWindows.get(winId)); +}; + +firetray.Handler.registerWindow = function(win) { + LOG("register window"); + + // register + let [gtkWin, gdkWin, xid] = firetray.Window.getWindowsFromChromeWindow(win); + this.windows[xid] = {}; + this.windows[xid].win = win; + this.gtkWindows.insert(xid, gtkWin); + this.gdkWindows.insert(xid, gdkWin); + this.windowsCount += 1; + this.visibleWindowsCount += 1; + this.windows[xid].visibility = true; + LOG("window "+xid+" registered"); + /* NOTE: it should not be necessary to gtk_widget_add_events(gtkWin, + gdk.GDK_ALL_EVENTS_MASK); */ + + try { + /* NOTE: we could try to catch the "delete-event" here and block + delete_event_cb (in gtk2/nsWindow.cpp), but we prefer to use the + provided 'close' JS event */ + + /* we'll catch minimize events with Gtk: + http://stackoverflow.com/questions/8018328/what-is-the-gtk-event-called-when-a-window-minimizes */ + this.windows[xid].onWindowStateCb = gtk.GCallbackWindowStateEvent_t(firetray.Window.onWindowState); + this.windows[xid].onWindowStateCbId = gobject.g_signal_connect(gtkWin, "window-state-event", this.windows[xid].onWindowStateCb, null); + LOG("g_connect window-state-event="+this.windows[xid].onWindowStateCbId); + + } catch (x) { + this._unregisterWindowByXID(xid); + ERROR(x); + return false; + } + + LOG("AFTER"); firetray.Handler.dumpWindows(); + + return true; +}; + +firetray.Handler._unregisterWindowByXID = function(xid) { + this.windowsCount -= 1; + if (this.windows[xid].visibility) this.visibleWindowsCount -= 1; + if (this.windows.hasOwnProperty(xid)) { + if (!delete this.windows[xid]) + throw new DeleteError(); + this.gtkWindows.remove(xid); + this.gdkWindows.remove(xid); + } else { + ERROR("can't unregister unknown window "+xid); + return false; + } + LOG("window "+xid+" unregistered"); + return true; +}; + +firetray.Handler.unregisterWindow = function(win) { + LOG("unregister window"); + + try { + let xid = firetray.Window.getXIDFromChromeWindow(win); + return this._unregisterWindowByXID(xid); + } catch (x) { + ERROR(x); + } + return false; +}; + +firetray.Handler.getWindowIdFromChromeWindow = firetray.Window.getXIDFromChromeWindow; + +firetray.Handler.showSingleWindow = function(xid) { + LOG("show xid="+xid); + try { + // try to restore previous state. TODO: z-order respected ? + gdk.gdk_window_show_unraised(firetray.Handler.gdkWindows.get(xid)); + // need to restore *after* showing for correctness + firetray.Window.restoreWindowPositionSizeState(xid); + } catch (x) { + ERROR(x); + } + firetray.Handler.windows[xid].visibility = true; + firetray.Handler.visibleWindowsCount += 1; +}; + +firetray.Handler.hideSingleWindow = function(xid) { + LOG("hideSingleWindow"); + try { + firetray.Window.saveWindowPositionSizeState(xid); + // NOTE: we don't use BaseWindow.visibility to have full control + gdk.gdk_window_hide(firetray.Handler.gdkWindows.get(xid)); + } catch (x) { + ERROR(x); + } + firetray.Handler.windows[xid].visibility = false; + firetray.Handler.visibleWindowsCount -= 1; +}; + +firetray.Handler.showHideAllWindows = function(gtkStatusIcon, userData) { + LOG("showHideAllWindows: "+userData); + // NOTE: showHideAllWindows being a callback, we need to use + // 'firetray.Handler' explicitely instead of 'this' + + LOG("visibleWindowsCount="+firetray.Handler.visibleWindowsCount); + LOG("windowsCount="+firetray.Handler.windowsCount); + let visibilityRate = firetray.Handler.visibleWindowsCount/firetray.Handler.windowsCount; + LOG("visibilityRate="+visibilityRate); + if (visibilityRate > 0.5) // TODO: should be configurable + firetray.Handler.hideAllWindows(); + else + firetray.Handler.showAllWindows(); + + let stopPropagation = true; + return stopPropagation; +};