Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

JWinSearch: search-based task and tab switching

4.75/5 (6 votes)
8 Oct 2007CPOL12 min read 1   507  
An article describing the various steps and technologies needed to write JWinSearch.

You'll also need to download the Gecko-SDK from the official page in order to build the Firefox extension.

Usage

Press Windows + W, type a few letters from the application name, tab name, or tab URL, and press Enter to switch to the window / page you wish.

Screenshot - jwinsearch.png

Introduction

Inspired by the blog entry on Coding Horror, I decided to write this little utility with some simple goals:

  • Search windows by title
  • Search browser tabs by title or URL on Firefox and IE
  • Activate either a browser tab or a window using simple keystrokes
  • Keep a very little resource usage profile

What started as a simple project rapidly spawned into a time consuming task for something I was to do on my free time. This was mostly due to my lack of familiarity with both browsers' APIs, especially Firefox's.

Background

In this article, I'll cover the basics of:

  • Hotkeys and control subclassing
  • Win32 window messaging, enumerating, and control
  • Task switching
  • IE tab enumerating and activation
  • Firefox extensions

Table of contents

Hidden windows and hotkeys

In order to keep things simple, the application silently runs on the background using just a hidden window to receive hotkey and control messages. The WIN-W hotkey is registered with a call like:

#define IDH_ALT_WIN 55 // Could be any number

RegisterHotKey(jws_hw_hwnd, IDH_ALT_WIN, MOD_WIN, LOBYTE(VkKeyScan(L'w'))

Once the hidden window receives the hotkey message, it will either open or close the very simple dialog:

static LRESULT CALLBACK jws_hw_proc(HWND hwnd, UINT msg, 
                        WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
  case WM_HOTKEY:
    if(wparam != IDH_ALT_WIN)
      return FALSE;
    if(jws_hwnd)
    {
      EndDialog(jws_hwnd, 0);
      SetForegroundWindow(jws_previous_fg_win);
      return TRUE;
    }
    jws_previous_fg_win = GetForegroundWindow();
    DialogBox(jws_hinstance, MAKEINTRESOURCE(IDD_JWS_DLG), 
              jws_hw_hwnd, &jws_dlg_proc);
    return TRUE;
  }

  return DefWindowProc(hwnd, msg, wparam, lparam);
}

You will certainly notice that, before displaying the main dialog, I save the previous foreground window. This is done so that I can restore it later, but only if the dialog is dismissed without selecting a new task.

During the main dialog startup, the windows are enumerated, and the two main window controls are subclassed in order to trap all keyboard commands so that the application performs as expected:

  • Typing filters the window list
  • Enter key activates the first application on the list
  • ESC key closes the dialog anytime
  • TAB goes from the edit to the list

Control subclassing is quite easy on Windows XP and later Win32 OSs:

static INT_PTR on_init_dlg(HWND hwnd)
{
  if(!SetWindowSubclass(GetDlgItem(hwnd, IDC_EDIT), jws_edit_subproc, 0, 0))
    EndDialog(hwnd, 1);
  if(!SetWindowSubclass(GetDlgItem(hwnd, IDC_LIST), jws_list_subproc, 0, 0))
    EndDialog(hwnd, 1);
  //...

}

static LRESULT CALLBACK jws_list_subproc(HWND hwnd, UINT msg, 
               WPARAM wparam, LPARAM lparam, UINT_PTR , DWORD_PTR)
{
  switch(msg)
  {
  case WM_GETDLGCODE:
    return DLGC_WANTALLKEYS;
  case WM_KEYDOWN:
    if(wparam == VK_ESCAPE)
    {
      EndDialog(jws_hwnd, 0);
      SetForegroundWindow(jws_previous_fg_win);
      return 0;
    }
  }

  return DefSubclassProc(hwnd, msg, wparam, lparam);
}

static LRESULT CALLBACK jws_edit_subproc(HWND hwnd, UINT msg, 
               WPARAM wparam, LPARAM lparam, UINT_PTR , DWORD_PTR)
{
  // ...

    else if(wparam == VK_DOWN || wparam == VK_TAB)
    {
      HWND lv_hwnd = GetDlgItem(jws_hwnd, IDC_LIST);
      if(!ListView_GetItemCount(lv_hwnd))
        return 0;
      ListView_SetItemState(lv_hwnd, 0, LVIS_SELECTED | 
           LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
      SetFocus(lv_hwnd);
      return 0;
    }
    // ...

    else if(wparam == VK_RETURN)
    {
      HWND lv_hwnd = GetDlgItem(jws_hwnd, IDC_LIST);
      if(!ListView_GetItemCount(lv_hwnd))
      {
        EndDialog(jws_hwnd, 0);
        SetForegroundWindow(jws_previous_fg_win);
        return 0;
      }
      on_task_selected(0);
    }
  // ...

}

Enumerating windows

Enumerating top-level windows is supposed to be very simple: one should just call EnumWindows(), indicating the intent to enumerate top-level windows by passing NULL as the parent window.

The tricky part lies on gathering information from the window (icon and title) and on excluding windows which are not of interest.

On the EnumWindows() callback, I:

  1. Exclude non-visible windows (WS_VISIBLE)
  2. Get the title (GetWindowText())
  3. Exclude windows with an empty title
  4. Exclude windows with the toolbar extended style (WS_EX_TOOLWINDOW)
  5. Get the icon using the WM_GETICON message
  6. If that fails, get the window class icon with GetClassLongPtr()
  7. If that fails too, load a default icon

After enumerating the windows, I just sort the retrieved window list so that displays will always be ordered by window name.

The code below is approximately the way to do it:

static INT_PTR on_init_dlg(HWND hwnd)
{
  //...


  EnumWindows(&jws_enum_windows_cb, 0);
  std::sort(jws_windows.begin(), jws_windows.end());
  
  // ...

}

static BOOL CALLBACK jws_enum_windows_cb(HWND hwnd, LPARAM )
{
  wchar_t title[512];

  DWORD dwStyle = GetWindowLongPtr(hwnd, GWL_STYLE);
  if(!(dwStyle & WS_VISIBLE))
    return TRUE;
  GetWindowText(hwnd, title, _countof(title));
  if(!title[0])
    return TRUE;
  LONG exstyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
  if(exstyle & WS_EX_TOOLWINDOW)
    return TRUE;
  HICON hicon = (HICON)SendMessage(hwnd, WM_GETICON, JWS_WND_ICON_TYPE, 0);
  if(!hicon)
    hicon = (HICON)(UINT_PTR)GetClassLongPtr(hwnd, JWS_CLS_ICON_TYPE);
  if(!hicon)
    hicon = LoadIcon(NULL, IDI_APPLICATION);
  
  jws_windows.push_back(JWS_WINDOW_DATA(hwnd, hicon, title));
    
  // [Tab enumeration code] ...

  
  return TRUE;
}

The last useful line on the code snippet above adds a window to the window list global variable. This global window list is cleared once the dialog is closed. It is used to rapidly reconstruct the list view's content once the text changes inside the line edit, thereby showing just the windows matching the user's search query.

The type JWS_WINDOW_DATA is just a struct which stores window display data (icon and title), window handle, and other information concerning tabs:

struct JWS_WINDOW_DATA
{
  JWS_WINDOW_DATA(const JWS_WINDOW_DATA &other)
  {
    *this = other;
  }

  // Standard windows

  JWS_WINDOW_DATA(HWND _hwnd, HICON hicon, const wchar_t *_name)
    : hwnd(_hwnd), icon(hicon), name(_name), type(JWS_WT_NORMAL) { }

  // [ Contructors for IE and FF ] ...


  ~JWS_WINDOW_DATA()
  {
    // ...

  }

  void operator = (const JWS_WINDOW_DATA &other)
  {
    // ...

  }

  int icon_index;  // inside the listview's image list


  HWND hwnd;
  HICON icon;
  std::wstring name;
  std::wstring parent_name;
  JWS_WINDOW_TYPE type;
  // ...


  std::wstring comp_name(void) const
  {
    if(type == JWS_WT_NORMAL)
      return name;
    return parent_name + name;
  }
  
  bool operator  < (const JWS_WINDOW_DATA &d) const 
  { 
    return comp_name() < d.comp_name(); 
  }
};

The = operator and the copy constructor above are needed for IE-related data, which I'll explain later on this article. The struct has a comparison operator so that the std::vector can be sorted, as seen above. On the comp_name() function, I prepend the parent name on tabs so that they show, always, after the browser main window.

Populating the list view

First of all, I needed to setup the list view upon dialog initialization. This step consists of creating a column, creating and populating an image list, and assigning it to the list view. Note some size-related macros on the code below. They allow for compile-time easy icon size selection; just define JWS_BIG_ICONS to use 32x32 icons instead of the default 16x16 ones I used.

This initialization is done on the WM_INIT handler:

static INT_PTR on_init_dlg(HWND hwnd)
{
  // ...

  LVCOLUMN col;
  memset(&col, 0, sizeof(LVCOLUMN));
  ListView_InsertColumn(GetDlgItem(jws_hwnd, IDC_LIST), 0, &col);

  HIMAGELIST image_list = ImageList_Create(JWS_ICON_SIZE, JWS_ICON_SIZE, 
                          ILC_MASK | ILC_COLOR32, 
                          (int)jws_windows.size(), 0);
  JWS_WINDOW_DATA *w, *w_end;
  int index = 0;
  for(w = &jws_windows.front(), w_end = w + 
      jws_windows.size(); w < w_end; w++)
    w->icon_index = ImageList_AddIcon(image_list, w->icon);
  ListView_SetImageList(GetDlgItem(jws_hwnd, IDC_LIST), 
                        image_list, JWS_IMAGE_LIST_SIZE);
  jws_display_window_list();
  // ...

  return TRUE;
}

On initialization and each time the user types something on the edit panel, the jws_display_window_list() function is called so that it can filter which windows and tabs match the user search criteria. The list view is cleared and refilled in this function.

Note how easy it is to indent the browser tabs. Remember, I sorted the windows in a way that guarantees the tabs are positioned right after the browser main window.

Although I set one column up and hide its columns, it is still there. If I don't call ListView_SetColumnWidth(LVSCW_AUTOSIZE), things get really truncated.

static void jws_display_window_list()
{
  HWND lv_hwnd = GetDlgItem(jws_hwnd, IDC_LIST);
  const JWS_WINDOW_DATA *base, *w, *w_end;
  wchar_t filter[128];

  GetDlgItemText(jws_hwnd, IDC_EDIT, filter, _countof(filter));

  ListView_DeleteAllItems(lv_hwnd);

  LRESULT ret;

  int n_item = 0;
  for(w = base = &jws_windows.front(), w_end = 
      w + jws_windows.size(); w < w_end; w++)
  {
    if(filter[0] && !stristrW(w->name.c_str(), filter))
      continue;

    LVITEM it;
    memset(&it, 0, sizeof(LVITEM));
    it.iItem = n_item++;
    it.mask = LVIF_IMAGE | LVIF_TEXT | LVIF_PARAM | LVIF_INDENT;
    it.iSubItem = 0;
    it.pszText = (LPWSTR)w->name.c_str();
    it.cchTextMax = (int)w->name.length() + 1;
    it.iImage = w->icon_index;
    it.lParam = (LPARAM)w;
    it.iIndent = w->type == JWS_WT_NORMAL ? 0 : 1;

    ret = SendMessage(lv_hwnd, LVM_INSERTITEM, 0, (LPARAM)&it);
  }
  ListView_SetColumnWidth(lv_hwnd, 0, LVSCW_AUTOSIZE);
}

Please note that the LVITEM::lParam field is used to hold a reference to the item's data. This means, you can't sort or add items to the vector after filling the list view.

Activating another task

Upon item activation, I have to bring the window to front. This can be very tricky, depending on the current window state. Since XP was launched, there is this feature which avoids other windows from taking focus away from anything. To circumvent it, I had to attach to the destination thread's input queue, by issuing a call to AttachThreadInput().

Last, if the selected window is iconified, one needs to restore it to the normal size. This is done with a simple WM_SYSCOMMAND(SC_RESTORE) message.

In order to activate a window, I do:

  1. BringWindowToTop()
  2. SetWindowPos(HWND_TOP)
  3. SetForegroundWindow()
  4. If needed, AttachThreadInput() and SetForegroundWindow() again
  5. If iconic, WM_SYSCOMMAND(SC_RESTORE)
static INT_PTR on_task_selected(int item_idx)
{
  JWS_WINDOW_DATA *w;
  HWND fg_hwnd;
  LVITEM item;

  ShowWindow(jws_hwnd, SW_HIDE);
  memset(&item, 0, sizeof(LVITEM));
  item.iItem = item_idx;
  item.mask = LVIF_PARAM ;
  ListView_GetItem(GetDlgItem(jws_hwnd, IDC_LIST), &item);

  w = (JWS_WINDOW_DATA *)item.lParam;
  BringWindowToTop(w->hwnd);
  SetWindowPos(w->hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  fg_hwnd = GetForegroundWindow();
  if(!SetForegroundWindow(w->hwnd))
  {
    if(!fg_hwnd)
      fg_hwnd = FindWindow(L"Shell_TrayWnd", NULL);
    DWORD pid_fg_win = GetWindowThreadProcessId(fg_hwnd, NULL);
    DWORD pid_win = GetWindowThreadProcessId(w->hwnd, NULL);
    {
      wchar_t title[256];
      GetWindowText(w->hwnd, title, _countof(title));
      title[255] = 0;
    }
    AttachThreadInput(pid_fg_win, pid_win, TRUE);
    SetForegroundWindow(w->hwnd);
    AttachThreadInput(pid_fg_win, pid_win, FALSE);
  }
  if(IsIconic(w->hwnd))
    SendMessage(w->hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);

  ShowWindow(jws_hwnd, SW_HIDE);
  // [ Tab activation code ] ...

  EndDialog(jws_hwnd, 0);
  return TRUE;
}

Browser integration

By now, you should have a pretty good idea of this simple utility regarding simple top-level window switching. The piece which makes JWinSearch stand out from similar applications is its ability to consider browser tabs as applications.

There are two tasks which have to be performed for each of the two supported browsers:

  • Enumerate tabs
  • Activate a tab

I soon discovered, Firefox has almost no external API, so I had to write an extension. For this project, this meant using both JavaScript and native code.

When it came to IE's ground, I soon discovered there is no tab-aware external API. Moreover, the tabs on the top of the screen are a single control, they share a single HWND. Thanks to a post by Dan Morris, it was quite simple to enumerate IE's tabs, but not to activate them. That's the place where I performed a horrible hack you'll easily notice when using lots of tabs: I simulate the CTRL+TAB key press on the browser window.

When I described how to enumerate windows, I presented the jws_enum_windows_cb() callback. After inserting the window on the JWS_WINDOW_DATA vector, I get the window class and, if the class name matches IE's or Firefox's, I call a dedicated function for each browser:

static BOOL CALLBACK jws_enum_windows_cb(HWND hwnd, LPARAM )
{
  // ...

  wchar_t className[512];
  if(GetClassName(hwnd, className, _countof(className)))
  {
    if(!wcscmp(className, L"MozillaUIWindowClass"))
      jws_enum_ff_tabs(title, hwnd, hicon);
    else if(!(wcscmp(className, L"IEFrame")))
      jws_enum_ie_tabs(title, hwnd, hicon);
  }
  
  return TRUE;
}

Enumerating IE tabs

As Mr. Dan Morris pointed out, enumerating IE tabs can be easily done with a little bit of COM code:

  1. Create a ShellWindows object. Each window is either a browser TAB or a Windows Explorer window
  2. Enumerate all its windows, casting them to a IWebBrowser2 interface
  3. Get the main browser's window handle by calling IWebBrowser2::get_HWND()
  4. Compare to the enumerated main browser window

This algorithm is the main part of the jws_enum_ie_tabs below. The other two parts are:

  • Getting a window handle used to determine if the tab is active. This handle and the browser object are saved inside the window's (task) list.
  • Getting a tab's name and URI. Those are straightforward calls to IWebBrowser2, as seen below.
static void jws_enum_ie_tabs(const std::wstring &parent_name, 
                             HWND hwnd, HICON hicon)
{
  if(!jws_shellwindows && 
      S_OK != CoCreateInstance(CLSID_ShellWindows, 0, 
              CLSCTX_INPROC_SERVER, IID_IShellWindows, 
              (void **)&jws_shellwindows))
    return;
  VARIANT v;
  V_VT(&v) = VT_I4;
  IDispatchPtr disp(0);
  HRESULT hr;
  for(V_I4(&v) = 0; S_OK == 
     (hr = jws_shellwindows->Item(v, &disp)); V_I4(&v)++)
  {
    // Cast to IWebBrowser2

    IWebBrowser2Ptr browser(0);
    if(S_OK != disp->QueryInterface(IID_IWebBrowser2, (void **)&browser))
      continue;
    // Verify if it is inside the desired window

    HWND browser_hwnd;
    if(S_OK != (browser->get_HWND((SHANDLE_PTR *)&browser_hwnd) ) || 
                browser_hwnd != hwnd)
      continue;
    
    // OK, we got a TAB from this browser


    // Get the tab's HWND, it will be used
    // to determine if the table is active

    IServiceProviderPtr servProv(0);
    IOleWindowPtr oleWnd(0);
    HWND tab_hwnd;
    _bstr_t title, uri;
    BSTR title_b, uri_b;
    if(S_OK != browser->QueryInterface(IID_IServiceProvider, (void **)&servProv))
      continue;
    if(S_OK != servProv->QueryService(SID_SShellBrowser, 
               IID_IOleWindow, (void **)&oleWnd))
      continue;
    if(S_OK != oleWnd->GetWindow(&tab_hwnd))
      continue;

    // Add to the window list

    if(S_OK != browser->get_LocationName(&title_b))
      title = _bstr_t(L"");
    else
      title.Attach(title_b);
    if(S_OK != browser->get_LocationURL(&uri_b))
      uri = _bstr_t(L"");
    else
      uri.Attach(uri_b);

    jws_windows.push_back(JWS_WINDOW_DATA(parent_name, hwnd, 
                     tab_hwnd, hicon, title, uri, browser));
  }
}

When explaining the JWS_WINDOW_DATA above, I omitted the tabs-related fields and methods for the sake of simplicity. Now, it's time to go back an introduce those. You'll see that the ie::browser member field is not used. It is there in the hope that someone finds a clever way of switching the active IE tab.

I needed to write an explicit copy constructor and assignment operator because of this COM pointer. Since the pointer is inside a union, I couldn't use _com_ptr_t, which would have simplified the JWS_WINDOW_DATA code a lot.

struct JWS_WINDOW_DATA
{
  JWS_WINDOW_DATA(const JWS_WINDOW_DATA &other)
  {
    *this = other;
  }

  // IE tabs

  JWS_WINDOW_DATA(const std::wstring &_parent_name, HWND ie_hwnd, 
                  HWND ie_tab_hwnd, HICON ie_hicon, const wchar_t *title, 
                  const wchar_t *uri, IWebBrowser2 *browser)
    : hwnd(ie_hwnd), icon(ie_hicon), type(JWS_WT_IE), parent_name(_parent_name)
  {
    if(!*title)
      name = uri;
    else
    {
      name = title;
      name += L" -- ";
      name += uri;
    }
    ie.tab_window = ie_tab_hwnd;
    ie.browser = browser;
    browser->AddRef();
  }
  // ...

    ~JWS_WINDOW_DATA()
  {
    if(type == JWS_WT_IE && ie.browser)
    {
      ie.browser->Release();
      ie.browser = 0;
    }
  }

  // ...

  
  void operator = (const JWS_WINDOW_DATA &other)
  {
    name = other.name;
    hwnd = other.hwnd;
    icon = other.icon;
    type = other.type;
    parent_name = other.parent_name;
    if(type == JWS_WT_IE)
    {
      ie.tab_window = other.ie.tab_window;
      ie.browser = other.ie.browser;
      if(ie.browser)
        ie.browser->AddRef();
    }
    else if(type == JWS_WT_FIREFOX)
    {
      firefox.hid_window = other.firefox.hid_window;
      firefox.tab_index = other.firefox.tab_index;
    }
  }

  // ...

    
  union 
  {
    struct
    {
      HWND hid_window;
      int tab_index;
    } firefox;
    struct
    {
      HWND tab_window;
      IWebBrowser2 *browser;
    } ie;
  } ;
}

Activating IE's tabs

** Horrible hack below, proceed with caution **

Since I couldn't find any other way of switching IE's active tab, I brute-forced it, emulating the CTRL+TAB key press. This is very cumbersome to say the least: it depends on timings and other non-deterministic factors. That's the reason the code below is full of wait-conditions, timeouts, and (argh) calls to Sleep().

The algorithm is pretty simple, though: it consists of emitting CTRL+TAB until the desired tab becomes the active tab on the window. The code is self-explanatory:

static INT_PTR on_task_selected(int item_idx)
{
  // ...

  if(IsIconic(w->hwnd))
    SendMessage(w->hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);

  ShowWindow(jws_hwnd, SW_HIDE);

  if(w->type == JWS_WT_FIREFOX)
    PostMessage(w->firefox.hid_window, JFFE_WM_ACTIVATE, 
                w->firefox.tab_index, (LPARAM)w->hwnd);
  else if(w->type == JWS_WT_IE)
    select_ie_tab(w);

  EndDialog(jws_hwnd, 0);
  return TRUE;
}

// Horrible way of selecting IE tab. Couldn't find anything better.

static void select_ie_tab(JWS_WINDOW_DATA *w)
{
  int n_tab_tries, n_tries = JWS_MAX_IE_RAISE_TIME;

  // Wait until IE Window raises

  while(GetForegroundWindow() != w->hwnd && n_tries-- > 0)
    Sleep(1);
  if(n_tries <= 0) // give up

    return;

  // Send CTRL+TAB until the correct tab is active

  INPUT ctrl_tab[4];

  ctrl_tab[0].type = INPUT_KEYBOARD;
  ctrl_tab[0].ki.wVk = VK_CONTROL;
  ctrl_tab[0].ki.wScan = 0x1d;
  ctrl_tab[0].ki.dwFlags = 0;
  ctrl_tab[0].ki.dwExtraInfo = 0;
  ctrl_tab[0].ki.time = 0;

  ctrl_tab[1] = ctrl_tab[0];
  ctrl_tab[1].ki.wVk = VK_TAB;
  ctrl_tab[1].ki.wScan = 0x0f;

  ctrl_tab[2] = ctrl_tab[1];
  ctrl_tab[2].ki.dwFlags |= KEYEVENTF_KEYUP;

  ctrl_tab[3] = ctrl_tab[0];
  ctrl_tab[3].ki.dwFlags |= KEYEVENTF_KEYUP;

  n_tab_tries = 200;
  HWND cur_tab_hwnd = FindWindowEx(w->hwnd, 0, L"TabWindowClass", 0);
  HWND new_tab_hwnd;
  UINT n_keys_sent;
  while(!IsWindowEnabled(w->ie.tab_window) && n_tab_tries-- > 0)
  {
    // If IE goes ou of focus, don't send CTRL+TAB to anyone

    if(GetForegroundWindow() != w->hwnd)
      return;
    // Send the 4 keys

    for(n_keys_sent = 0; 
        n_keys_sent < 4; 
        n_keys_sent += SendInput(4 - n_keys_sent, ctrl_tab + 
                       n_keys_sent, sizeof(INPUT)), Sleep(1));
    // Give IE a chance

    Sleep(JWS_MIN_IE_TAB_SWITCH_TIME);
    // Wait for the TAB to change

    for(n_tries = JWS_MAX_IE_TAB_SWITCH_TIME / 5; 
        cur_tab_hwnd == (new_tab_hwnd = FindWindowEx(w->hwnd, 0, 
                L"TabWindowClass", 0)) && n_tries > 0; 
        n_tries--)
      Sleep(5);
    if(n_tries > 0) // OK, tab switched

      cur_tab_hwnd = new_tab_hwnd;
  }
}

Firefox extension

Since Firefox has almost no external interface like IE, the only useful approach to interacting with its tabs was writing an extension. It was a little bit more complicated than IE's solution, but the result was much better, without any kind of ugly hack.

Binary component

Besides communicating with JWinSearch, the Firefox extension I wrote has just two simple functions:

  • enumerating tabs, and
  • activating a new tab.

Communicating with JWinSearch could be done in a number of ways, including using COM, but the simplest one is by using a hidden window inside the extension and exchange messages. To do so, I needed to write a native XPCOM component. There are lots of tutorials and information regarding this on the Mozilla Developer Center XPCOM page.

The binary component is just a communication module that:

  • Creates a hidden window to communicate with JWinSearch.
  • Uses the nsIObserverService to notify the JavaScript part of the extension of incoming tab enumeration and activation requests.
  • Uses WM_COPYDATA messages to transmit the tab list back to JWinSearch.

First of all, the binary component JFFEnumerator has to find out the main window inside which it has been created. Since the XUL + JavaScript extension side guarantees a new instance will be created for every Firefox browser window (I'll explain that below), I use a global std::map<hwnd,jffenumerator />. This works because Firefox is a multi-window but single-process browser. If you use multiple Firefox processes somehow, the extension won't work correctly. This is a rare situation, though.

The code presented at the Mozilla developer connection does not work if one opens more than one Firefox window. Take a look at it yourself, and you'll easily discover why.

My solution looks like this:

typedef std::map<hwnd,jffenumerator> EnumMap;
typedef EnumMap::value_type EnumMapPair;

static EnumMap jffe_map;
JFFEnumerator::JFFEnumerator()
: m_hwnd(0),
  m_h_hwnd(0)
{
  EnumThreadWindows(GetCurrentThreadId(), 
        &JFFEnumerator::enum_win_cb, (LPARAM)this);

  if(!m_hwnd)
    return;

  // ...

}

BOOL CALLBACK JFFEnumerator::enum_win_cb(HWND hwnd, LPARAM lp_this)
{
  wchar_t classname[512];
  if(!GetClassName(hwnd, classname, _countof(classname)))
    return TRUE;
  if(wcscmp(classname, L"MozillaUIWindowClass"))
    return TRUE;
  if(jffe_map.find(hwnd) != jffe_map.end())
    return TRUE;
  JFFEnumerator *jffe = (JFFEnumerator *)lp_this;
  jffe->m_hwnd = hwnd;
  sprintf_s(jffe->m_select_topic, "S:%x", hwnd);
  sprintf_s(jffe->m_enumerate_topic, "E:%x", hwnd) ;
  jffe_map.insert(EnumMapPair(hwnd, jffe));

  return FALSE;
}
</hwnd,jffenumerator>

The two topics above are the "signals" emitted through the nsiObserverService instance. The first one means "enumerate", the other means "select tab".

When the hidden window receives messages, it just notifies the JavaScript code. Please keep in mind, these notifications are synchronous, i.e., the nsiObserverService::observe() method doesn't return until the JavaScript function has returned itself.

The hidden window's WindowProc handles these messages:

  • JFFE_WM_ENUMERATE: asks the extension to enumerate. This is done by the JavaScript side of the extension, which calls JFFEnumerator::ReportTab() for each tab encountered, and returns. Before it returns from the window procedure, it posts a JFFE_WM_REPLY_ENUMERATE to itself in order to avoid dead-locks.
  • JFFE_WM_REPLY_ENUMERATE: internally used to avoid a dead-lock. This message simply sends a WM_COPYDATA message with the tab list back to JWinSearch.
  • JFFE_WM_ACTIVATE: asks the extension to activate a tab. This is done by the JavaScript side of the extension.
LRESULT CALLBACK JFFEnumerator::hidden_win_proc(HWND hwnd, UINT msg, 
                                                WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
  default:
    return DefWindowProc(hwnd, msg, wparam, lparam);
  case JFFE_WM_ENUMERATE:
    {
      EnumMap::iterator it = jffe_map.find((HWND)lparam);
      if(it == jffe_map.end())
        return 1;
      JFFEnumerator *jffe = it->second;
      
      jffe->m_tab_list.clear();
      nsresult rv = jffe->notify(jffe->m_enumerate_topic, L"");
      // Must defer processing with PostMessage because 

      // JWinSearch is waiting for message reply inside SendMessage()

      PostMessage(jffe->m_h_hwnd, JFFE_WM_REPLY_ENUMERATE, 
                  wparam, (LPARAM)jffe);
    }
    return 0;
  case JFFE_WM_REPLY_ENUMERATE:
    {
      JFFEnumerator *jffe = (JFFEnumerator *)lparam;
      COPYDATASTRUCT copy;

      copy.dwData = jffe->m_tab_list.size();
      copy.cbData = (DWORD)(copy.dwData * sizeof(JFFE_TabInfo));
      copy.lpData = &(jffe->m_tab_list[0]);
      
      SendMessage((HWND)wparam, WM_COPYDATA, 
                 (WPARAM)jffe->m_h_hwnd, (LPARAM)©);

      jffe->m_tab_list.clear();
    }
    return 0;
  case JFFE_WM_ACTIVATE:
    {
      EnumMap::iterator it = jffe_map.find((HWND)lparam);
      if(it == jffe_map.end())
        return 1;
      JFFEnumerator *jffe = it->second;

      wchar_t extra_data[20];
      swprintf_s(extra_data, L"%x", (int)wparam);
      jffe->notify(jffe->m_select_topic, extra_data);
    }
    return 0;
  }
}

nsresult JFFEnumerator::notify(const char *topic, 
         const PRUnichar *data)
{
  nsCOMPtr<nsiservicemanager> servMan;
  nsresult rv = NS_GetServiceManager(getter_AddRefs(servMan));
  if (NS_FAILED(rv))
    return rv; 

  nsCOMPtr<nsiobserverservice> observerService;
  rv = servMan->GetServiceByContractID("@mozilla.org/observer-service;1", 
                      NS_GET_IID(nsIObserverService), 
                      getter_AddRefs(observerService));
  if (NS_FAILED(rv))
    return rv;

  rv = observerService->NotifyObservers(this, topic, data);
  
  return rv;
}
</nsiobserverservice></nsiservicemanager>

JavaScript / XUL component

The JavaScript / XUL component is surprisingly similar to a DHTML + JavaScript page. Basically, the XUL part is like an HTML page and the JavaScript class is a DOM-helper. In XUL, I describe an overlay, which is much like a hooked page snippet. This overlay will just create an instance of my JavaScript class for every browser window:

XML
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" 
         id="JWinSearchFFTabEnumeratorOverlay">
  <script id="JWSFF_SRC"  src="jwsff.js"/>
  <script id="JWSFF_START" >
   <![CDATA[var jfftabenumerator = new JFFTabEnumeratorJS;]]>
  </script>
</overlay>

The JavaScript component is just an event handler for notifications from the binary component described above, and for the window unload event. To enumerate the tabs, I used the sample code provided by the Mozilla Developer Center. To set the active tab, I just set the corresponding property on the tabContainer DOM object.

All this could be done in C++ code, but it would be orders of magnitude harder because of the lack of interface flattening, which only makes sense in a script language like JS.

JavaScript
function JFFTabEnumeratorJS()
{
  this.start();
}

JFFTabEnumeratorJS.prototype = 
{
  ffTabEnumerator:null,
  
  start: function() 
  {
    try 
    {
      const cid = "@jorge.vasquez/fftabenumerator;1";
      var cobj = Components.classes[cid].createInstance();
      ffTabEnumerator = 
        cobj.QueryInterface(Components.interfaces.IJFFEnumerator);

      var sel_topic = ffTabEnumerator.getSelectTopic();
      var enum_topic = ffTabEnumerator.getEnumerateTopic();
     
      var observerService = Components.classes["@mozilla.org" + 
          "/observer-service;1"].getService(
          Components.interfaces.nsIObserverService);
      observerService.addObserver(this, sel_topic, false);
      observerService.addObserver(this, enum_topic, false);
      
      window.addEventListener('unload', 
         function() { this.unload(event); }, false );
    }
    catch (err) 
    {
      alert(err);
      return;
    }
  },
  
  unload: function(event)
  {
      var observerService = Components.classes["@mozilla.org/" + 
          "observer-service;1"].getService(
          Components.interfaces.nsIObserverService);
      observerService.removeObserver(this, sel_topic);
      observerService.removeObserver(this, enum_topic);
      window.removeEventListener('unload', 
          function() { this.unload(event); }, false );
      ffTabEnumerator = null;
  },
  
  observe: function(subject, topic, data) 
  {
    if(topic == ffTabEnumerator.getEnumerateTopic())
    {
      var n_tabs = getBrowser().browsers.length;
      
      for (var i = 0; i < n_tabs; i++) 
      {
        var b = gBrowser.getBrowserAtIndex(i);
        ffTabEnumerator.reportTab(i, b.contentDocument.title, 
                                  b.currentURI.spec);
      }
    }
    else if(topic == ffTabEnumerator.getSelectTopic())
    {
      var tab_index = parseInt(data, 16);    
      if(isNaN(tab_index))
        return;
      gBrowser.mTabContainer.selectedIndex = tab_index;
    }
  }
}

In order to install the extension, it suffices to create a registry entry on HKLU\Software\Mozilla\Firefox\Extension. To finalize the extension, I also needed to create the directory structure and the RDF manifest. Please take a look at this article for detailed instructions on this subject.

Communicating with the Firefox extension

Inside the jws_enum_windows_cb() function, the jws_enum_ff_tabs will be called for windows with the class "MozillaUIWindowClass". Since all actual work is done inside the extension, this function is much simpler than the one needed for IE tab enumeration. It will just:

  1. Find the browser hidden window, which has the browser main window in its name. This is a nice way to avoid creating a hidden window which is not a child of HWND_MESSAGE.
  2. Sends a message to it, using the synchronous SendMessage() API.
  3. Runs an event-loop to wait for the WM_COPYDATA message, which is itself handled by the main dialog's dialog procedure. This event-loop is a little bit different from the usual GetMessage() based one, in that it will only run for a predefined amount of time, and blocks on MsgWaitForMultipleObjects().
static void jws_enum_ff_tabs(std::wstring parent_name, HWND hwnd, HICON hicon)
{
  wchar_t winName[20];
  HWND hid_hwnd;

  // Tell extension to enumerate its tab and report back to us

  swprintf_s(winName, L"W:%x", hwnd);
  hid_hwnd = FindWindow(jffe_hid_win_classname, winName);
  if(!hid_hwnd)
    return;
  LRESULT r = SendMessage(hid_hwnd, JFFE_WM_ENUMERATE, 
                         (WPARAM)jws_hwnd, (LPARAM)hwnd);
  if(r)
    return;

  // Process all message for up
  // to JWS_FF_TIMEOUT or an answer is received

  jws_ff_answered = FALSE;
  jws_ff_icon = hicon;
  jws_ff_hwnd = hwnd;
  jws_ff_parent_name = parent_name;

  DWORD start;
  start = GetTickCount();
  do
  {
    MsgWaitForMultipleObjects(0, 0, 0, JWS_FF_TIMEOUT - 
      (GetTickCount() - start), QS_ALLINPUT | QS_ALLPOSTMESSAGE);
    MSG msg;
    while(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
    {
      if(msg.message == WM_QUIT)
      {
        PostMessage(jws_hw_hwnd, msg.message, msg.wParam, msg.lParam);
        return;
      }
      if(IsDialogMessage(jws_hwnd, &msg))
        continue;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  } while(!jws_ff_answered && GetTickCount() - start < JWS_FF_TIMEOUT );
}

When the WM_COPYDATA message is received, the on_ff_answer() will be called to perform the following steps:

  1. Validate received data from outside the current process.
  2. Add each tab to the task list, which saves its hidden window handle and tab index.
static LRESULT on_ff_answer(HWND sender_hwnd, COPYDATASTRUCT *reply)
{
  JFFE_TabInfo *i, *end;

  // validate reply->dwData

  if(reply->dwData * sizeof(JFFE_TabInfo) != reply->cbData)
    return TRUE;

  for(i = (JFFE_TabInfo *)reply->lpData, 
      end = i + reply->dwData; i < end; i++)
  {
    i->tab_name[JFFE_TAB_NAME_LEN - 1] = 0;
    i->tab_uri[JFFE_TAB_URI_LEN - 1] = 0;
    jws_windows.push_back(JWS_WINDOW_DATA(jws_ff_parent_name, 
                jws_ff_hwnd, sender_hwnd, jws_ff_icon, i));
  }

  jws_ff_answered = TRUE;

  return TRUE;
}

On task selection, a simple message is sent to the Firefox extension binary component to activate the correct tab. Again, all the work is done inside the extension; I just needed to notify it.

static INT_PTR on_task_selected(int item_idx)
{
  JWS_WINDOW_DATA *w;
  // ...

  
  if(w->type == JWS_WT_FIREFOX)
    PostMessage(w->firefox.hid_window, JFFE_WM_ACTIVATE, 
                w->firefox.tab_index, (LPARAM)w->hwnd);
    
  // ...

}

History

  • October 1, 2007 -- Version 1.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)