home *** CD-ROM | disk | FTP | other *** search
- /*
- AutoHotkey
-
- Copyright 2003-2007 Chris Mallett (support@autohotkey.com)
-
- This program is free software; you can redistribute it and/or
- modify it under the terms of the GNU General Public License
- as published by the Free Software Foundation; either version 2
- of the License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- */
-
- #include "stdafx.h" // pre-compiled headers
- #include "WinGroup.h"
- #include "window.h" // for several lower level window functions
- #include "globaldata.h" // for DoWinDelay
- #include "application.h" // for DoWinDelay's MsgSleep()
-
- // Define static members data:
- WinGroup *WinGroup::sGroupLastUsed = NULL;
- HWND *WinGroup::sAlreadyVisited = NULL;
- int WinGroup::sAlreadyVisitedCount = 0;
-
-
- ResultType WinGroup::AddWindow(char *aTitle, char *aText, Label *aJumpToLabel, char *aExcludeTitle, char *aExcludeText)
- // Caller should ensure that at least one param isn't NULL/blank.
- // GroupActivate will tell its caller to jump to aJumpToLabel if a WindowSpec isn't found.
- // This function is not thread-safe because it adds an entry to the list of window specs.
- // In addition, if this function is being called by one thread while another thread is calling IsMember(),
- // the thread-safety notes in IsMember() apply.
- {
- // v1.0.41: If a window group can ever be deleted (or its window specs), that might defeat the
- // thread-safety of WinExist/WinActive.
- // v1.0.36.05: If all four window parameters are blank, allow it to be added but provide
- // a non-blank ExcludeTitle so that the window-finding routines won't see it as the
- // "last found window". 99.99% of the time, it is undesirable to have Program Manager
- // in a window group of any kind, so that is used as the placeholder:
- if (!(*aTitle || *aText || *aExcludeTitle || *aExcludeText))
- aExcludeTitle = "Program Manager";
-
- // Though the documentation is clear on this, some users will still probably execute
- // each GroupAdd statement more than once. Thus, to prevent more and more memory
- // from being allocated for duplicates, do not add the window specification if it
- // already exists in the group:
- if (mFirstWindow) // Traverse the circular linked-list to look for a match.
- for (WindowSpec *win = mFirstWindow
- ; win != NULL; win = (win->mNextWindow == mFirstWindow) ? NULL : win->mNextWindow)
- if (!strcmp(win->mTitle, aTitle) && !strcmp(win->mText, aText) // All are case sensitive.
- && !strcmp(win->mExcludeTitle, aExcludeTitle) && !strcmp(win->mExcludeText, aExcludeText))
- return OK;
-
- // SimpleHeap::Malloc() will set these new vars to the constant empty string if their
- // corresponding params are blank:
- char *new_title, *new_text, *new_exclude_title, *new_exclude_text;
- if (!(new_title = SimpleHeap::Malloc(aTitle))) return FAIL; // It already displayed the error for us.
- if (!(new_text = SimpleHeap::Malloc(aText)))return FAIL;
- if (!(new_exclude_title = SimpleHeap::Malloc(aExcludeTitle))) return FAIL;
- if (!(new_exclude_text = SimpleHeap::Malloc(aExcludeText))) return FAIL;
-
- // The precise method by which the follows steps are done should be thread-safe even if
- // some other thread calls IsMember() in the middle of the operation. But any changes
- // must be carefully reviewed:
- WindowSpec *the_new_win = new WindowSpec(new_title, new_text, aJumpToLabel, new_exclude_title, new_exclude_text);
- if (the_new_win == NULL)
- return g_script.ScriptError(ERR_OUTOFMEM);
- if (mFirstWindow == NULL)
- mFirstWindow = the_new_win;
- else
- mLastWindow->mNextWindow = the_new_win; // Formerly it pointed to First, so nothing is lost here.
- // This must be done after the above:
- mLastWindow = the_new_win;
- // Make it circular: Last always points to First. It's okay if it points to itself:
- mLastWindow->mNextWindow = mFirstWindow;
- ++mWindowCount;
- return OK;
- }
-
-
-
- ResultType WinGroup::ActUponAll(ActionTypeType aActionType, int aTimeToWaitForClose)
- {
- if (IsEmpty())
- return OK; // OK since this is the expected behavior in this case.
- // Don't need to call Update() in this case.
- WindowSearch ws;
- ws.mFirstWinSpec = mFirstWindow; // Act upon all windows that match any WindowSpec in the group.
- ws.mActionType = aActionType; // Set the type of action to be performed on each window.
- ws.mTimeToWaitForClose = aTimeToWaitForClose; // Only relevant for WinClose and WinKill.
- EnumWindows(EnumParentActUponAll, (LPARAM)&ws);
- if (ws.mFoundParent) // It acted upon least one window.
- DoWinDelay;
- return OK;
- }
-
-
-
- ResultType WinGroup::CloseAndGoToNext(bool aStartWithMostRecent)
- // If the foreground window is a member of this group, close it and activate
- // the next member.
- {
- if (IsEmpty())
- return OK; // OK since this is the expected behavior in this case.
- // Otherwise:
- // Don't call Update(), let (De)Activate() do that.
-
- HWND fore_win = GetForegroundWindow();
- // Even if it's NULL, don't return since the legacy behavior is to continue on to the final part below.
-
- WindowSpec *win_spec = IsMember(fore_win, g);
- if ( (mIsModeActivate && win_spec) || (!mIsModeActivate && !win_spec) )
- {
- // If the user is using a GroupActivate hotkey, we don't want to close
- // the foreground window if it's not a member of the group. Conversely,
- // if the user is using GroupDeactivate, we don't want to close a
- // member of the group. This precaution helps prevent accidental closing
- // of windows that suddenly pop up to the foreground just as you've
- // realized (too late) that you pressed the "close" hotkey.
- // MS Visual Studio/C++ gets messed up when it is directly sent a WM_CLOSE,
- // probably because the wrong window (it has two mains) is being sent the close.
- // But since that's the only app I've ever found that doesn't work right,
- // it seems best not to change our close method just for it because sending
- // keys is a fairly high overhead operation, and not without some risk due to
- // not knowing exactly what keys the user may have physically held down.
- // Also, we'd have to make this module dependent on the keyboard module,
- // which would be another drawback.
- // Try to wait for it to close, otherwise the same window may be activated
- // again before it has been destroyed, defeating the purpose of the
- // "ActivateNext" part of this function's job:
- // SendKeys("!{F4}");
- if (fore_win)
- {
- WinClose(fore_win, 500);
- DoWinDelay;
- }
- }
- //else do the activation below anyway, even though no close was done.
- return mIsModeActivate ? Activate(aStartWithMostRecent, win_spec) : Deactivate(aStartWithMostRecent);
- }
-
-
-
- ResultType WinGroup::Activate(bool aStartWithMostRecent, WindowSpec *aWinSpec, Label **aJumpToLabel)
- {
- if (aJumpToLabel) // Initialize early in case of early return.
- *aJumpToLabel = NULL;
- if (IsEmpty())
- return OK; // OK since this is the expected behavior in this case.
- // Otherwise:
- if (!Update(true)) // Update our private member vars.
- return FAIL; // It already displayed the error for us.
- WindowSpec *win, *win_to_activate_next = aWinSpec;
- bool group_is_active = false; // Set default.
- HWND activate_win, active_window = GetForegroundWindow(); // This value is used in more than one place.
- if (win_to_activate_next)
- {
- // The caller told us which WindowSpec to start off trying to activate.
- // If the foreground window matches that WindowSpec, do nothing except
- // marking it as visited, because we want to stay on this window under
- // the assumption that it was newly revealed due to a window on top
- // of it having just been closed:
- if (win_to_activate_next == IsMember(active_window, g))
- {
- group_is_active = true;
- MarkAsVisited(active_window);
- return OK;
- }
- // else don't mark as visited even if it's a member of the group because
- // we're about to attempt to activate a different window: the next
- // unvisited member of this same WindowSpec. If the below doesn't
- // find any of those, it continue on through the list normally.
- }
- else // Caller didn't tell us which, so determine it.
- {
- if (win_to_activate_next = IsMember(active_window, g)) // Foreground window is a member of this group.
- {
- // Set it to activate this same WindowSpec again in case there's
- // more than one that matches (e.g. multiple notepads). But first,
- // mark the current window as having been visited if it hasn't
- // already by marked by a prior iteration. Update: This method
- // doesn't work because if a unvisted matching window became the
- // foreground window by means other than using GroupActivate
- // (e.g. launching a new instance of the app: now there's another
- // matching window in the foreground). So just call it straight
- // out. It has built-in dupe-checking which should prevent the
- // list from filling up with dupes if there are any special
- // situations in which that might otherwise happen:
- //if (!sAlreadyVisitedCount)
- group_is_active = true;
- MarkAsVisited(active_window);
- }
- else // It's not a member.
- {
- win_to_activate_next = mFirstWindow; // We're starting fresh, so start at the first window.
- // Reset the list of visited windows:
- sAlreadyVisitedCount = 0;
- }
- }
-
- // Activate any unvisited window that matches the win_to_activate_next spec.
- // If none, activate the next window spec in the series that does have an
- // existing window:
- // If the spec we're starting at already has some windows marked as visited,
- // set this variable so that we know to retry the first spec again in case
- // a full circuit is made through the window specs without finding a window
- // to activate. Note: Using >1 vs. >0 might protect against any infinite-loop
- // conditions that may be lurking:
- bool retry_starting_win_spec = (sAlreadyVisitedCount > 1);
- bool retry_is_in_effect = false;
- for (win = win_to_activate_next;;)
- {
- // Call this in the mode to find the last match, which makes things nicer
- // because when the sequence wraps around to the beginning, the windows will
- // occur in the same order that they did the first time, rather than going
- // backwards through the sequence (which is counterintuitive for the user):
- if ( activate_win = WinActivate(g, win->mTitle, win->mText, win->mExcludeTitle, win->mExcludeText
- // This next line is whether to find last or first match. We always find the oldest
- // (bottommost) match except when the user has specifically asked to start with the
- // most recent. But it only makes sense to start with the most recent if the
- // group isn't currently active (i.e. we're starting fresh), because otherwise
- // windows would be activated in an order different from what was already shown
- // the first time through the enumeration, which doesn't seem to be ever desirable:
- , !aStartWithMostRecent || group_is_active
- , sAlreadyVisited, sAlreadyVisitedCount) )
- {
- // We found a window to activate, so we're done.
- // Probably best to do this before WinDelay in case another hotkey fires during the delay:
- MarkAsVisited(activate_win);
- DoWinDelay;
- //MsgBox(win->mText, 0, win->mTitle);
- break;
- }
- // Otherwise, no window was found to activate.
- if (aJumpToLabel && win->mJumpToLabel && !sAlreadyVisitedCount)
- {
- // Caller asked us to return in this case, so that it can
- // use this value to execute a user-specified Gosub:
- *aJumpToLabel = win->mJumpToLabel; // Set output param for the caller.
- return OK;
- }
- if (retry_is_in_effect)
- // This was the final attempt because we've already gone all the
- // way around the circular linked list of WindowSpecs. This check
- // must be done, otherwise an infinite loop might result if the windows
- // that formed the basis for determining the value of
- // retry_starting_win_spec have since been destroyed:
- break;
- // Otherwise, go onto the next one in the group:
- win = win->mNextWindow;
- // Even if the above didn't change the value of <win> (because there's only
- // one WinSpec in the list), it's still correct to reset this count because
- // we want to start the fresh again after all the windows have been
- // visited. Note: The only purpose of sAlreadyVisitedCount as used by
- // this function is to indicate which windows in a given WindowSpec have
- // been visited, not which windows altogether (i.e. it's not necessary to
- // remember which windows have been visited once we move on to a new
- // WindowSpec).
- sAlreadyVisitedCount = 0;
- if (win == win_to_activate_next)
- {
- // We've made one full circuit of the circular linked list without
- // finding an existing window to activate. At this point, the user
- // has pressed a hotkey to do a GroupActivate, but nothing has happened
- // yet. We always want something to happen unless there's absolutely
- // no existing windows to activate, or there's only a single window in
- // the system that matches the group and it's already active.
- if (retry_starting_win_spec)
- {
- // Mark the foreground window as visited so that it won't be
- // mistakenly activated again by the next iteration:
- MarkAsVisited(active_window);
- retry_is_in_effect = true;
- // Now continue with the next iteration of the loop so that it
- // will activate a different instance of this WindowSpec rather
- // than getting stuck on this one.
- }
- else
- break;
- }
- }
- return OK;
- }
-
-
-
- ResultType WinGroup::Deactivate(bool aStartWithMostRecent)
- {
- if (IsEmpty())
- return OK; // OK since this is the expected behavior in this case.
- // Otherwise:
- if (!Update(false)) // Update our private member vars.
- return FAIL; // It already displayed the error for us.
-
- HWND active_window = GetForegroundWindow();
- if (IsMember(active_window, g))
- sAlreadyVisitedCount = 0;
-
- // Activate the next unvisited non-member:
- WindowSearch ws;
- ws.mFindLastMatch = !aStartWithMostRecent || sAlreadyVisitedCount;
- ws.mAlreadyVisited = sAlreadyVisited;
- ws.mAlreadyVisitedCount = sAlreadyVisitedCount;
- ws.mFirstWinSpec = mFirstWindow;
-
- EnumWindows(EnumParentFindAnyExcept, (LPARAM)&ws);
-
- if (ws.mFoundParent)
- {
- // If the window we're about to activate owns other visble parent windows, it can
- // never truly be activated because it must always be below them in the z-order.
- // Thus, instead of activating it, activate the first (and usually the only?)
- // visible window that it owns. Doing this makes things nicer for some apps that
- // have a pair of main windows, such as MS Visual Studio (and probably many more),
- // because it avoids activating such apps twice in a row as the user progresses
- // through the sequence:
- HWND first_visible_owned = WindowOwnsOthers(ws.mFoundParent);
- if (first_visible_owned)
- {
- MarkAsVisited(ws.mFoundParent); // Must mark owner as well as the owned window.
- // Activate the owned window instead of the owner because it usually
- // (probably always, given the comments above) is the real main window:
- ws.mFoundParent = first_visible_owned;
- }
- SetForegroundWindowEx(ws.mFoundParent);
- // Probably best to do this before WinDelay in case another hotkey fires during the delay:
- MarkAsVisited(ws.mFoundParent);
- DoWinDelay;
- }
- else // No window was found to activate (they have all been visited).
- {
- if (sAlreadyVisitedCount)
- {
- bool wrap_around = (sAlreadyVisitedCount > 1);
- sAlreadyVisitedCount = 0;
- if (wrap_around)
- {
- // The user pressed a hotkey to do something, yet nothing has happened yet.
- // We want something to happen every time if there's a qualifying
- // "something" that we can do. And in this case there is: we can start
- // over again through the list, excluding the foreground window (which
- // the user has already had a chance to review):
- MarkAsVisited(active_window);
- // Make a recursive call to self. This can't result in an infinite
- // recursion (stack fault) because the called layer will only
- // recurse a second time if sAlreadyVisitedCount > 1, which is
- // impossible with the current logic:
- Deactivate(false); // Seems best to ignore aStartWithMostRecent in this case?
- }
- }
- }
- // Even if a window wasn't found, we've done our job so return OK:
- return OK;
- }
-
-
-
- inline ResultType WinGroup::Update(bool aIsModeActivate)
- {
- mIsModeActivate = aIsModeActivate;
- if (sGroupLastUsed != this)
- {
- sGroupLastUsed = this;
- sAlreadyVisitedCount = 0; // Since it's a new group, reset the array to start fresh.
- }
- if (!sAlreadyVisited) // Allocate the array on first use.
- // Getting it from SimpleHeap reduces overhead for the avg. case (i.e. the first
- // block of SimpleHeap is usually never fully used, and this array won't even
- // be allocated for short scripts that don't even using window groups.
- if ( !(sAlreadyVisited = (HWND *)SimpleHeap::Malloc(MAX_ALREADY_VISITED * sizeof(HWND))) )
- return FAIL; // It already displayed the error for us.
- return OK;
- }
-
-
-
- WindowSpec *WinGroup::IsMember(HWND aWnd, global_struct &aSettings)
- // Thread-safety: This function is thread-safe even when the main thread happens to be calling AddWindow()
- // and changing the linked list while it's being traversed here by the hook thread. However, any subsequent
- // changes to this function or AddWindow() must be carefully reviewed.
- // Although our caller may be a WindowSearch method, and thus we might make
- // a recursive call back to that same method, things have been reviewed to ensure that
- // thread-safety is maintained, even if the calling thread is the hook.
- {
- if (!aWnd)
- return NULL; // Some callers on this.
- WindowSearch ws;
- ws.SetCandidate(aWnd);
- for (WindowSpec *win = mFirstWindow; win != NULL;) // v1.0.41: "win != NULL" was added for thread-safety.
- {
- if (ws.SetCriteria(aSettings, win->mTitle, win->mText, win->mExcludeTitle, win->mExcludeText) && ws.IsMatch())
- return win;
- // Otherwise, no match, so go onto the next one:
- win = win->mNextWindow;
- if (!win || win == mFirstWindow) // v1.0.41: The check of !win was added for thread-safety.
- // We've made one full circuit of the circular linked list,
- // discovering that the foreground window isn't a member
- // of the group:
- break;
- }
- return NULL; // Because it would have returned already if a match was found.
- }
-
-
- /////////////////////////////////////////////////////////////////////////
-
-
- BOOL CALLBACK EnumParentFindAnyExcept(HWND aWnd, LPARAM lParam)
- // Find the first parent window that doesn't match any of the WindowSpecs in
- // the linked list, and that hasn't already been visited.
- {
- // Since the following two sections apply only to GroupDeactivate (since that's our only caller),
- // they both seem okay even in light of the ahk_group method.
-
- if (!IsWindowVisible(aWnd))
- // Skip these because we alwayswant them to stay invisible, regardless
- // of the setting for g.DetectHiddenWindows:
- return TRUE;
-
- // UPDATE: Because the window of class Shell_TrayWnd (the taskbar) is also always-on-top,
- // the below prevents it from ever being activated too, which is almost always desirable.
- // However, this prevents the addition of WS_DISABLED as an extra criteria for skipping
- // a window. Maybe that's best for backward compatibility anyway.
- // Skip always-on-top windows, such as SplashText, because probably shouldn't
- // be activated (especially in this mode, which is often used to visit the user's
- // "non-favorite" windows). In addition, they're already visible so the user already
- // knows about them, so there's no need to have them presented for review.
- if (GetWindowLong(aWnd, GWL_EXSTYLE) & WS_EX_TOPMOST)
- return TRUE;
-
- // It probably would have been better to use the class name (ProgMan?) for this instead,
- // but there is doubt that the same class name is used across all OSes. The reason for
- // doing that is to avoid ambiguity with other windows that just happen to be called
- // "Program Manager". See similar section in EnumParentActUponAll().
- // Skip "Program Manager" too because activating it would serve no purpose. This is probably
- // the same HWND that GetShellWindow() returns, but GetShellWindow() isn't supported on
- // Win9x or WinNT, so don't bother using it. And GetDeskTopWindow() apparently doesn't
- // return "Program Manager" (something with a blank title I think):
- char win_title[20]; // Just need enough size to check for Program Manager
- if (GetWindowText(aWnd, win_title, sizeof(win_title)) && !stricmp(win_title, "Program Manager"))
- return TRUE;
-
- WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
- ws.SetCandidate(aWnd);
-
- // Check this window's attributes against each set of criteria present in the group. If
- // it's a match for any set of criteria, it's a member of the group and thus should be
- // excluded since we want only NON-members:
- for (WindowSpec *win = ws.mFirstWinSpec;;)
- {
- // For each window in the linked list, check if aWnd is a match for it:
- if (ws.SetCriteria(g, win->mTitle, win->mText, win->mExcludeTitle, win->mExcludeText) && ws.IsMatch(true))
- // Match found, so aWnd is a member of the group. But we want to find non-members only,
- // so keep searching:
- return TRUE;
- // Otherwise, no match, but keep checking until aWnd has been compared against
- // all the WindowSpecs in the group:
- win = win->mNextWindow;
- if (win == ws.mFirstWinSpec)
- {
- // We've made one full circuit of the circular linked list without
- // finding a match. So aWnd is the one we're looking for unless
- // it's in the list of exceptions:
- for (int i = 0; i < ws.mAlreadyVisitedCount; ++i)
- if (aWnd == ws.mAlreadyVisited[i])
- return TRUE; // It's an exception, so keep searching.
- // Otherwise, this window meets the criteria, so return it to the caller and
- // stop the enumeration. UPDATE: Rather than stopping the enumeration,
- // continue on through all windows so that the last match is found.
- // That makes things nicer because when the sequence wraps around to the
- // beginning, the windows will occur in the same order that they did
- // the first time, rather than going backwards through the sequence
- // (which is counterintuitive for the user):
- ws.mFoundParent = aWnd; // No need to increment ws.mFoundCount in this case.
- return ws.mFindLastMatch;
- }
- } // The loop above is infinite unless a "return" is encountered inside.
- }
-
-
-
- BOOL CALLBACK EnumParentActUponAll(HWND aWnd, LPARAM lParam)
- // Caller must have ensured that lParam isn't NULL and that it contains a non-NULL mFirstWinSpec.
- {
- WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
-
- // Skip windows the command isn't supposed to detect. ACT_WINSHOW is exempt because
- // hidden windows are always detected by the WinShow command:
- if (!(g.DetectHiddenWindows || ws.mActionType == ACT_WINSHOW || IsWindowVisible(aWnd)))
- return TRUE;
-
- int nCmdShow;
- ws.SetCandidate(aWnd);
-
- for (WindowSpec *win = ws.mFirstWinSpec;;)
- {
- // For each window in the linked list, check if aWnd is a match for it:
- if (ws.SetCriteria(g, win->mTitle, win->mText, win->mExcludeTitle, win->mExcludeText) && ws.IsMatch())
- {
- // Match found, so aWnd is a member of the group. In addition, IsMatch() has set
- // the value of ws.mFoundParent to tell our caller that at least one window was acted upon.
- // See Line::PerformShowWindow() for comments about the following section.
- nCmdShow = SW_NONE; // Set default each time.
- switch (ws.mActionType)
- {
- case ACT_WINCLOSE:
- case ACT_WINKILL:
- // mTimeToWaitForClose is not done in a very efficient way here: to keep code size
- // in check, it closes each window individually rather than sending WM_CLOSE to all
- // the windows simultaneously and then waiting until all have vanished:
- WinClose(aWnd, ws.mTimeToWaitForClose, ws.mActionType == ACT_WINKILL); // DoWinDelay is done by our caller.
- return TRUE; // All done with the current window, so fetch the next one.
-
- case ACT_WINMINIMIZE:
- if (IsWindowHung(aWnd))
- {
- if (g_os.IsWin2000orLater())
- nCmdShow = SW_FORCEMINIMIZE;
- }
- else
- nCmdShow = SW_MINIMIZE;
- break;
-
- case ACT_WINMAXIMIZE: if (!IsWindowHung(aWnd)) nCmdShow = SW_MAXIMIZE; break;
- case ACT_WINRESTORE: if (!IsWindowHung(aWnd)) nCmdShow = SW_RESTORE; break;
- case ACT_WINHIDE: nCmdShow = SW_HIDE; break;
- case ACT_WINSHOW: nCmdShow = SW_SHOW; break;
- }
-
- if (nCmdShow != SW_NONE)
- ShowWindow(aWnd, nCmdShow);
- // DoWinDelay is not done here because our caller will do it once only, which
- // seems best when there are a lot of windows being acted upon here.
-
- // Now that this matching window has been acted upon (or avoided due to being hung),
- // continue the enumeration to get the next candidate window:
- return TRUE;
- }
- // Otherwise, no match, keep checking until aWnd has been compared against all the WindowSpecs in the group:
- win = win->mNextWindow;
- if (win == ws.mFirstWinSpec)
- // We've made one full circuit of the circular linked list without
- // finding a match, so aWnd is not a member of the group and
- // should not be closed.
- return TRUE; // Continue the enumeration.
- }
- }
-