/* -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* GdkWindow and GtkWindow are totally different things. A GtkWindow is a "standalone" window. A GdkWindow is just a region on the screen that can capture events and has certain attributes (such as a cursor, and a coordinate system). Basically a GdkWindow is an X window, in the Xlib sense, and GtkWindow is a widget used for a particular UI effect. (http://mail.gnome.org/archives/gtk-app-devel-list/1999-January/msg00138.html) */ 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/ctypesMap.jsm"); Cu.import("resource://firetray/ctypes/linux/gobject.jsm"); Cu.import("resource://firetray/ctypes/linux/gdk.jsm"); Cu.import("resource://firetray/ctypes/linux/gtk.jsm"); Cu.import("resource://firetray/ctypes/linux/libc.jsm"); Cu.import("resource://firetray/ctypes/linux/x11.jsm"); Cu.import("resource://firetray/commons.js"); firetray.Handler.subscribeLibsForClosing([gobject, gdk, gtk, libc, x11, glib]); let log = firetray.Logger.getLogger("firetray.Window"); if ("undefined" == typeof(firetray.Handler)) log.error("This module MUST be imported from/after FiretrayHandler !"); const Services2 = {}; XPCOMUtils.defineLazyServiceGetter( Services2, "uuid", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator" ); const FIRETRAY_XWINDOW_HIDDEN = 1 << 0; // when minimized also const FIRETRAY_XWINDOW_MAXIMIZED = 1 << 1; /** * custum type used to pass data in to and out of findGtkWindowByTitleCb */ var _find_data_t = ctypes.StructType("_find_data_t", [ { inTitle: ctypes.char.ptr }, { outWindow: gtk.GtkWindow.ptr } ]); // 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), firetray.Handler.gtkPopupMenuWindowItems = new ctypesMap(gtk.GtkImageMenuItem.ptr), firetray.Window = { init: function() { this.initialized = true; }, shutdown: function() { this.initialized = false; }, /** * Iterate over all Gtk toplevel windows to find a window. We rely on * Service.wm to watch windows correctly: we should find only one window. * * @author Nils Maier (stolen from MiniTrayR) * @param window nsIDOMWindow from Services.wm * @return a gtk.GtkWindow.ptr */ getGtkWindowFromChromeWindow: function(window) { let baseWindow = window .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIBaseWindow); // Tag the base window let oldTitle = baseWindow.title; log.debug("oldTitle="+oldTitle); baseWindow.title = Services2.uuid.generateUUID().toString(); try { // Search the window by the *temporary* title let widgets = gtk.gtk_window_list_toplevels(); let that = this; let findGtkWindowByTitleCb = gobject.GFunc_t(that._findGtkWindowByTitle); var userData = new _find_data_t( ctypes.char.array()(baseWindow.title), null ).address(); log.debug("userData="+userData); gobject.g_list_foreach(widgets, findGtkWindowByTitleCb, userData); gobject.g_list_free(widgets); if (userData.contents.outWindow.isNull()) throw new Error("Window not found!"); log.debug("found window: "+userData.contents.outWindow); } catch (x) { log.error(x); } finally { // Restore baseWindow.title = oldTitle; } return userData.contents.outWindow; }, /** * compares a GtkWindow's title with a string passed in userData * @param gtkWidget: GtkWidget from gtk_window_list_toplevels() * @param userData: _find_data_t */ _findGtkWindowByTitle: function(gtkWidget, userData) { let data = ctypes.cast(userData, _find_data_t.ptr); let inTitle = data.contents.inTitle; let gtkWin = ctypes.cast(gtkWidget, gtk.GtkWindow.ptr); let winTitle = gtk.gtk_window_get_title(gtkWin); if (!winTitle.isNull()) { log.debug(inTitle+" = "+winTitle); if (libc.strcmp(inTitle, winTitle) == 0) data.contents.outWindow = gtkWin; } }, getGdkWindowFromGtkWindow: function(gtkWin) { try { let gtkWid = ctypes.cast(gtkWin, gtk.GtkWidget.ptr); return gtk.gtk_widget_get_window(gtkWid); } catch (x) { log.error(x); } return null; }, getXIDFromGdkWindow: function(gdkWin) { return gdk.gdk_x11_drawable_get_xid(ctypes.cast(gdkWin, gdk.GdkDrawable.ptr)); }, getXIDFromGtkWidget: function(gtkWid) { try { let gdkWin = gtk.gtk_widget_get_window(gtkWid); return gdk.gdk_x11_drawable_get_xid(ctypes.cast(gdkWin, gdk.GdkDrawable.ptr)); } catch (x) { log.error(x); } return null; }, addrPointedByInHex: function(ptr) { return "0x"+ctypes.cast(ptr, ctypes.uintptr_t.ptr).contents.toString(16); }, getGdkWindowFromNativeHandle: function(nativeHandle) { let gdkw = new gdk.GdkWindow.ptr(ctypes.UInt64(nativeHandle)); // a new pointer to the GdkWindow log.debug("gdkw="+gdkw+" *gdkw="+this.addrPointedByInHex(gdkw)); return gdkw; }, getGtkWindowFromGdkWindow: function(gdkWin) { let gptr = new gobject.gpointer; gdk.gdk_window_get_user_data(gdkWin, gptr.address()); log.debug("gptr="+gptr+" *gptr="+this.addrPointedByInHex(gptr)); let gtkw = ctypes.cast(gptr, gtk.GtkWindow.ptr); let gtkw_voidp = ctypes.cast(gtkw, ctypes.void_t.ptr); let gtkwid_top = gtk.gtk_widget_get_toplevel(ctypes.cast(gtkw, gtk.GtkWidget.ptr)); gtkw = ctypes.cast(gtkwid_top, gtk.GtkWindow.ptr); log.debug("gtkw="+gtkw+" *gtkw="+this.addrPointedByInHex(gtkw)); return gtkw; }, /* consider using getXIDFromChromeWindow() if you only need the XID */ getWindowsFromChromeWindow: function(win) { let baseWin = firetray.Handler.getWindowInterface(win, "nsIBaseWindow"); let nativeHandle = baseWin.nativeHandle; // Moz' private pointer to the GdkWindow log.debug("nativeHandle="+nativeHandle); let gtkWin, gdkWin; if (nativeHandle) { // Gecko 17+ gdkWin = firetray.Window.getGdkWindowFromNativeHandle(nativeHandle); gtkWin = firetray.Window.getGtkWindowFromGdkWindow(gdkWin); } else { gtkWin = firetray.Window.getGtkWindowFromChromeWindow(win); gdkWin = firetray.Window.getGdkWindowFromGtkWindow(gtkWin); } let xid = firetray.Window.getXIDFromGdkWindow(gdkWin); log.debug("XID="+xid); return [baseWin, gtkWin, gdkWin, xid]; }, getXIDFromChromeWindow: function(win) { for (let xid in firetray.Handler.windows) if (firetray.Handler.windows[xid].chromeWin === win) return xid; log.error("unknown window while lookup"); return null; }, unregisterWindowByXID: function(xid) { firetray.Handler.windowsCount -= 1; if (firetray.Handler.windows[xid].visible) firetray.Handler.visibleWindowsCount -= 1; if (firetray.Handler.windows.hasOwnProperty(xid)) { if (!delete firetray.Handler.windows[xid]) throw new DeleteError(); firetray.Handler.gtkWindows.remove(xid); firetray.Handler.gdkWindows.remove(xid); firetray.PopupMenu.removeWindowItem(xid); } else { log.error("can't unregister unknown window "+xid); return false; } log.debug("window "+xid+" unregistered"); return true; }, show: function(xid) { log.debug("show xid="+xid); // try to restore previous state. TODO: z-order respected ? firetray.Window.restorePositionAndSize(xid); firetray.Window.restoreStates(xid); // better visual effect if visibility set after restorePosition, but some // WMs like compiz seem not to honor position setting if window not visible firetray.Window.setVisibility(xid, true); // after show firetray.Window.restoreDesktop(xid); if (firetray.Utils.prefService.getBoolPref('show_activates')) firetray.Window.activate(xid); firetray.PopupMenu.hideWindowItemAndSeparatorMaybe(xid); firetray.Handler.showHideIcon(); }, /* FIXME: hiding windows should also hide child windows */ hide: function(xid) { log.debug("hide"); firetray.Window.savePositionAndSize(xid); firetray.Window.saveStates(xid); firetray.Window.saveDesktop(xid); firetray.Window.setVisibility(xid, false); firetray.PopupMenu.showWindowItem(xid); firetray.Handler.showHideIcon(); }, startupHide: function(xid) { log.debug('startupHide: '+xid); firetray.Handler.windows[xid].baseWin.visibility = false; firetray.Handler.windows[xid].visible = false; firetray.Handler.visibleWindowsCount -= 1; firetray.PopupMenu.showWindowItem(xid); firetray.Handler.showHideIcon(); }, savePositionAndSize: function(xid) { let gx = {}, gy = {}, gwidth = {}, gheight = {}; firetray.Handler.windows[xid].baseWin.getPositionAndSize(gx, gy, gwidth, gheight); firetray.Handler.windows[xid].savedX = gx.value; firetray.Handler.windows[xid].savedY = gy.value; firetray.Handler.windows[xid].savedWidth = gwidth.value; firetray.Handler.windows[xid].savedHeight = gheight.value; log.debug("save: gx="+gx.value+", gy="+gy.value+", gwidth="+gwidth.value+", gheight="+gheight.value); }, restorePositionAndSize: function(xid) { if ("undefined" === typeof(firetray.Handler.windows[xid].savedX)) return; // windows[xid].saved* may not be initialized log.debug("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); firetray.Handler.windows[xid].baseWin.setPositionAndSize( firetray.Handler.windows[xid].savedX, firetray.Handler.windows[xid].savedY, firetray.Handler.windows[xid].savedWidth, firetray.Handler.windows[xid].savedHeight, false); // repaint ['savedX', 'savedX', 'savedWidth', 'savedHeight'].forEach(function(element, index, array) { delete firetray.Handler.windows[xid][element]; }); }, saveStates: function(xid) { let winStates = firetray.Window.getXWindowStates(x11.Window(xid)); firetray.Handler.windows[xid].savedStates = winStates; log.debug("save: windowStates="+winStates); }, restoreStates: function(xid) { let winStates = firetray.Handler.windows[xid].savedStates; log.debug("restored WindowStates: " + winStates); if (winStates & FIRETRAY_XWINDOW_MAXIMIZED) { firetray.Handler.windows[xid].chromeWin.maximize(); log.debug("restored maximized"); } if (winStates & FIRETRAY_XWINDOW_HIDDEN) { firetray.Handler.windows[xid].chromeWin.minimize(); log.debug("restored minimized"); } /* helps prevent getting iconify event following show() */ if (firetray.Utils.prefService.getBoolPref('hides_on_minimize')) firetray.Handler.windows[xid].chromeWin.restore(); delete firetray.Handler.windows[xid].savedStates; }, saveDesktop: function(xid) { if (!firetray.Utils.prefService.getBoolPref('remember_desktop')) return; let winDesktop = firetray.Window.getXWindowDesktop(x11.Window(xid)); firetray.Handler.windows[xid].savedDesktop = winDesktop; log.debug("save: windowDesktop="+winDesktop); }, restoreDesktop: function(xid) { if (!firetray.Utils.prefService.getBoolPref('remember_desktop')) return; let desktopDest = firetray.Handler.windows[xid].savedDesktop; if (desktopDest === null || "undefined" === typeof(desktopDest)) return; let dataSize = 1; let data = ctypes.long(dataSize); data[0] = desktopDest; this.xSendClientMessgeEvent(xid, x11.current.Atoms._NET_WM_DESKTOP, data, dataSize); log.debug("restored to desktop: "+desktopDest); delete firetray.Handler.windows[xid].savedDesktop; }, setVisibility: function(xid, visibility) { log.debug("setVisibility="+visibility); let gtkWidget = ctypes.cast(firetray.Handler.gtkWindows.get(xid), gtk.GtkWidget.ptr); if (visibility) gtk.gtk_widget_show_all(gtkWidget); else gtk.gtk_widget_hide(gtkWidget); firetray.Handler.windows[xid].visible = visibility; firetray.Handler.visibleWindowsCount = visibility ? firetray.Handler.visibleWindowsCount + 1 : firetray.Handler.visibleWindowsCount - 1 ; }, xSendClientMessgeEvent: function(xid, atom, data, dataSize) { let xev = new x11.XClientMessageEvent; xev.type = x11.ClientMessage; xev.window = x11.Window(xid); xev.message_type = atom; xev.format = 32; for (let i=0; i