March, 2008

  • The Old New Thing

    What a drag: Dragging a Uniform Resource Locator (URL) and text

    • 25 Comments

    We've learned how to drag text and how to drag a uniform resource locator, but what if want to drag both? Well, it's actually a simply matter of saying that you have both (and actually producing it on demand).

    Start by extending the enumeration of data types to include both URL and text:

      enum {
        DATA_URL,
        DATA_TEXT,
        DATA_NUM,
        DATA_INVALID = -1,
      };
    

    There is a subtlety in the way we set up this enumeration: We put DATA_URL ahead of DATA_TEXT so that the clipboard formats that are produced by SHCreateStdEnumFormatEtc are generated in priority order (highest quality first). By enumerating URL first, a program that understands both URL and text formats will know to prefer URL. In this particular case, it's not that critical since most text editors do URL auto-detection (at least if the URL begins with "http:"), but in the more general case, enumerating the formats in priority order can make a big difference. For example, your data object might provide text in both Rich Text Format as well as plain text, and it would probably be better if a program that understood both formats used the rich text version by default rather than the plain text version.

    Once we have both formats available, we need to respond to both of them. In our constructor, we have to describe both of these formats so that GetDataIndex and EnumFormatEtc know about them.

    CTinyDataObject::CTinyDataObject() : m_cRef(1)
    {
      SetFORMATETC(&m_rgfe[DATA_URL],
                   RegisterClipboardFormat(CFSTR_SHELLURL));
        SetFORMATETC(&m_rgfe[DATA_TEXT], CF_TEXT);
    }
    

    And then the change to the heart of the data object, the IDataObject::GetData method, is anticlimactic:

    HRESULT CTinyDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pmed)
    {
      ZeroMemory(pmed, sizeof(*pmed));
    
      switch (GetDataIndex(pfe)) {
      case DATA_URL:
      case DATA_TEXT:
        pmed->tymed = TYMED_HGLOBAL;
        return CreateHGlobalFromBlob(c_szURL, sizeof(c_szURL),
                                  GMEM_MOVEABLE, &pmed->hGlobal);
      }
    
      return DV_E_FORMATETC;
    }
    

    Whether the caller asks for either text or a URL, we give them the same string back.

    When you run this program, observe that it has the combined functionality of the two previous programs. You can drag text into Wordpad, drop an URL onto Firefox, and drop an URL onto Internet Explorer.

    These were all just warm-ups. After a short break, we'll roll up our sleeves and begin providing more complicated data in our data object.

  • The Old New Thing

    What a drag: Dragging a Uniform Resource Locator (URL)

    • 27 Comments

    Last time, we dragged some text around and found that the text would be interpreted as a URL if you dropped it onto Firefox, but Internet Explorer was not as willing to accept it. Today, we'll make the data object work for Internet Explorer.

    The only change is that we have to provide the URL in the form of a CFSTR_SHELLURL clipboard format rather than as CF_TEXT. Take the program from last time and make two changes. First, use the handy-dandy search-and-replace function to change DATA_TEXT to DATA_URL throughout. (This step isn't technically necessary, but it's nice to have the name match its usage.) The real work happens in this change to the constructor:

    CTinyDataObject::CTinyDataObject() : m_cRef(1)
    {
      SetFORMATETC(&m_rgfe[DATA_URL],
                   RegisterClipboardFormat(CFSTR_SHELLURL));
    }
    

    That's all; just change the clipboard format from CF_TEXT to CFSTR_SHELLURL. It is important to note that CFSTR_SHELLURL represents an ANSI string. Since "URLs are written only with the graphic printable characters of the US-ASCII coded character set," there is no loss of expressiveness by restricting to ANSI.

    Run this new program and now you can click in the client area and drag/drop the (invisible) object onto Internet Explorer, where it will navigate to Microsoft's home page. (If your system supports Active Desktop, you can also drag/drop the invisible object to the desktop and create an Active Desktop component.)

    Okay, so we have one version of the program that can drag a URL to Internet Explorer, and another version that can drag a URL to FireFox. Next time, we'll combine them to have a single data object that can drop to both. It's quite embarrassingly simple (because I planned it that way).

  • The Old New Thing

    Exercise doesn't have any effect unless you know you're doing it

    • 16 Comments

    Hotel maids began losing weight once they were informed that their normal job activities counted as exercise. I can't want for somebody to test whether this placebo effect works in reverse; that is, whether you will lose weight if you aren't actually exercising but believe that you are. If it works, then sign me up! (Oh rats, they addressed this in the interview.)

  • The Old New Thing

    What a drag: Dragging text

    • 19 Comments

    This week's mini-series was almost titled "It's the least you could do" because I'm going to try to do the absolute least amount of work to accomplish something interesting with drag and drop. The real purpose today is to lay some groundwork, but just to have something to show for our effort, I'll show you how to drag text around.

    We're going to need the CDropSource class from an earlier series on drag-and-drop. Also take the change from CoInitialize to OleInitialize (and similarly CoUninitialize), as well as the line

        HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
    

    Our mission for today is to create the tiniest data object possible.

    #include <strsafe.h> // for StringCchCopy
    #include <shlobj.h> // (will be needed in future articles)
    
    /* note: apartment-threaded object */
    class CTinyDataObject : public IDataObject
    {
    public:
      // IUnknown
      STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj);
      STDMETHODIMP_(ULONG) AddRef();
      STDMETHODIMP_(ULONG) Release();
    
      // IDataObject
      STDMETHODIMP GetData(FORMATETC *pfe, STGMEDIUM *pmed);
      STDMETHODIMP GetDataHere(FORMATETC *pfe, STGMEDIUM *pmed);
      STDMETHODIMP QueryGetData(FORMATETC *pfe);
      STDMETHODIMP GetCanonicalFormatEtc(FORMATETC *pfeIn,
                                         FORMATETC *pfeOut);
      STDMETHODIMP SetData(FORMATETC *pfe, STGMEDIUM *pmed,
                           BOOL fRelease);
      STDMETHODIMP EnumFormatEtc(DWORD dwDirection,
                                 LPENUMFORMATETC *ppefe);
      STDMETHODIMP DAdvise(FORMATETC *pfe, DWORD grfAdv,
                        IAdviseSink *pAdvSink, DWORD *pdwConnection);
      STDMETHODIMP DUnadvise(DWORD dwConnection);
      STDMETHODIMP EnumDAdvise(LPENUMSTATDATA *ppefe);
    
      CTinyDataObject();
    
    private:
      enum {
        DATA_TEXT,
        DATA_NUM,
        DATA_INVALID = -1,
      };
    
      int GetDataIndex(const FORMATETC *pfe);
    
    private:
      ULONG m_cRef;
      FORMATETC m_rgfe[DATA_NUM];
    };
    

    We'll learn more about those private members later. Let's start with the boring stuff: The IUnknown interface.

    HRESULT CTinyDataObject::QueryInterface(REFIID riid, void **ppv)
    {
      IUnknown *punk = NULL;
      if (riid == IID_IUnknown) {
        punk = static_cast<IUnknown*>(this);
      } else if (riid == IID_IDataObject) {
        punk = static_cast<IDataObject*>(this);
      }
    
      *ppv = punk;
      if (punk) {
        punk->AddRef();
        return S_OK;
      } else {
        return E_NOINTERFACE;
      }
    }
    
    ULONG CTinyDataObject::AddRef()
    {
      return ++m_cRef;
    }
    
    ULONG CTinyDataObject::Release()
    {
      ULONG cRef = --m_cRef;
      if (cRef == 0) delete this;
      return cRef;
    }
    

    Yawners. The constructor is interesting, though, because we use our constructor to build the array of supported FORMATETCs which other members will consult.

    void SetFORMATETC(FORMATETC *pfe, UINT cf,
                      TYMED tymed = TYMED_HGLOBAL, LONG lindex = -1,
                      DWORD dwAspect = DVASPECT_CONTENT,
                      DVTARGETDEVICE *ptd = NULL)
    {
      pfe->cfFormat = (CLIPFORMAT)cf;
      pfe->tymed    = tymed;
      pfe->lindex   = lindex;
      pfe->dwAspect = dwAspect;
      pfe->ptd      = ptd;
    }
    
    CTinyDataObject::CTinyDataObject() : m_cRef(1)
    {
      SetFORMATETC(&m_rgfe[DATA_TEXT], CF_TEXT);
    }
    

    Our data object contains only thing: plain text. We set the clipboard format to CF_TEXT, indicating that that's the data we have. The type medium is TYMED_HGLOBAL because we are going to provide the text in the form of an HGLOBAL. The other fields are boilerplate that you will rarely have to change: The aspect is DVASPECT_CONTENT because we are going to provide the actual data content. The DVTARGETDEVICE is NULL because our content is device-independent. And the lindex is -1 because we're going to provide all the data. I've created a helper function which uses the boilerplate values as default parameters.

    The first member function that will use this helper array is one that we will use quite a bit to do the preliminary validation of incoming FORMATETC structures.

    int CTinyDataObject::GetDataIndex(const FORMATETC *pfe)
    {
      for (int i = 0; i < ARRAYSIZE(m_rgfe); i++) {
        if (pfe->cfFormat == m_rgfe[i].cfFormat &&
           (pfe->tymed    &  m_rgfe[i].tymed)   &&
            pfe->dwAspect == m_rgfe[i].dwAspect &&
            pfe->lindex   == m_rgfe[i].lindex) {
          return i;
        }
      }
      return DATA_INVALID;
    }
    

    The GetDataIndex method takes a candidate FORMATETC and looks to see whether it matches any of the ones in our table of supported formats, m_rgfe, returning its index or DATA_INVALID indicating that there was no match. Note that we consider it a match if any of the requested type media match the supported type media. For example, the caller might pass TYMED_HGLOBAL | TYMED_STREAM, indicating that the caller can handle receiving either an HGLOBAL or an IStream. If our format matches either one, then we'll call that a success.

    Before we continue, here's a handy helper function when working with clipboard data: It takes a block of memory and turns it into a HGLOBAL.

    HRESULT CreateHGlobalFromBlob(const void *pvData, SIZE_T cbData,
                                  UINT uFlags, HGLOBAL *phglob)
    {
      HGLOBAL hglob = GlobalAlloc(uFlags, cbData);
      if (hglob) {
        void *pvAlloc = GlobalLock(hglob);
        if (pvAlloc) {
          CopyMemory(pvAlloc, pvData, cbData);
          GlobalUnlock(hglob);
        } else {
          GlobalFree(hglob);
          hglob = NULL;
        }
      }
      *phglob = hglob;
      return hglob ? S_OK : E_OUTOFMEMORY;
    }
    

    The money in a data object lies in the IDataObject::GetData method, because this is where the data object client gets to see what all the excitement is about.

    CHAR c_szURL[] = "http://www.microsoft.com/";
    
    HRESULT CTinyDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pmed)
    {
      ZeroMemory(pmed, sizeof(*pmed));
    
      switch (GetDataIndex(pfe)) {
      case DATA_TEXT:
        pmed->tymed = TYMED_HGLOBAL;
        return CreateHGlobalFromBlob(c_szURL, sizeof(c_szURL),
                                  GMEM_MOVEABLE, &pmed->hGlobal);
      }
    
      return DV_E_FORMATETC;
    }
    

    Wow, that was deceptively simple. We ask GetDataIndex to look up the FORMATETC; if it's DATA_TEXT, we return the desired text in the form of an HGLOBAL. Otherwise, it's not supported, so we return an appropriate error code. Note that CF_TEXT is specifically ANSI text. For Unicode text, use CF_UNICODE.

    Very closely related to IDataObject::GetData is IDataObject::QueryGetData, which is just like GetData except that it doesn't actually get the data. It just says whether the data object contains data in the specified format.

    HRESULT CTinyDataObject::QueryGetData(FORMATETC *pfe)
    {
      return GetDataIndex(pfe) == DATA_INVALID ? S_FALSE : S_OK;
    }
    

    The only other interesting method is IDataObject::EnumFormatEtc, which can be asked to return an enumerator that lists all the formats contained in the data object.

    HRESULT CTinyDataObject::EnumFormatEtc(DWORD dwDirection,
                                           LPENUMFORMATETC *ppefe)
    {
      if (dwDirection == DATADIR_GET) {
        return SHCreateStdEnumFmtEtc(ARRAYSIZE(m_rgfe), m_rgfe, ppefe);
      }
      *ppefe = NULL;
      return E_NOTIMPL;
    }
    

    If the caller is asking for the formats that it can "get", then we return an enumerator created from the shell stock format enumerator. Otherwise, we say that we don't have one.

    The rest of the methods are just stubs.

    HRESULT CTinyDataObject::GetDataHere(FORMATETC *pfe,
                                         STGMEDIUM *pmed)
    {
        return E_NOTIMPL;
    }
    
    HRESULT CTinyDataObject::GetCanonicalFormatEtc(FORMATETC *pfeIn,
                                                   FORMATETC *pfeOut)
    {
      *pfeOut = *pfeIn;
      pfeOut->ptd = NULL;
      return DATA_S_SAMEFORMATETC;
    }
    
    HRESULT CTinyDataObject::SetData(FORMATETC *pfe, STGMEDIUM *pmed,
                                                       BOOL fRelease)
    {
        return E_NOTIMPL;
    }
    
    HRESULT CTinyDataObject::DAdvise(FORMATETC *pfe, DWORD grfAdv,
                         IAdviseSink *pAdvSink, DWORD *pdwConnection)
    {
        return OLE_E_ADVISENOTSUPPORTED;
    }
    
    HRESULT CTinyDataObject::DUnadvise(DWORD dwConnection)
    {
        return OLE_E_ADVISENOTSUPPORTED;
    }
    
    HRESULT CTinyDataObject::EnumDAdvise(LPENUMSTATDATA *ppefe)
    {
        return OLE_E_ADVISENOTSUPPORTED;
    }
    

    And we're done. Let's take it for a spin.

    void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                       int x, int y, UINT keyFlags)
    {
      IDataObject *pdto = new CTinyDataObject();
      if (pdto) {
        IDropSource *pds = new CDropSource();
        if (pds) {
          DWORD dwEffect;
          DoDragDrop(pdto, pds, DROPEFFECT_COPY, &dwEffect);
          pds->Release();
        }
        pdto->Release();
      }
    }
    

    Fire up Wordpad and then click in the client area of our scratch program and drag and drop the invisible text over to the Wordpad window. Ta-da, the text is inserted.

    This even works with Firefox to drag a URL into a Firefox window. But it doesn't work for Internet Explorer. We'll see why next time.

    Exercise: Why didn't we also have to set CF_UNICODE text?

    Pre-emptive Igor Levicki comment: "Windows Vista should be dragged and dropped to the trash can."

  • The Old New Thing

    Why isn't there a GetDlgItemFloat function?

    • 62 Comments

    Jonathan Wilson asks, "Do you know why there is a GetDlgItemInt and a SetDlgItemInt but not a GetDlgItemFloat and a SetDlgItemFloat?"

    Give people five dollars and they'll ask why you didn't give them ten.

    Let's start with the first question. Why is there a GetDlgItemInt function? After all, GetDlgItemInt doesn't do anything you couldn't already do with GetDlgItemText and atoi. Well, reading integers out of dialog boxes is a rather common operation, something that the built-in Windows components do quite a lot of, so making this two-part helper function available more generally available seemed like a reasonable thing to do in the cause of reducing code size. On a 256KB machine, reducing code size is a big deal.

    So why stop there? Why not also do floating point? Well, the 8086 processor doesn't have floating point support. If you wanted hardware floating point support, you had to shell out the extra bucks for an 8087 coprocessor. To run on computers that didn't have an 8087 coprocessor, you had to include a floating point emulator, which is not a small library. It would have been excessive to add the entire floating point emulator to the window manager for the benefit of two functions most programs never call. "You're telling me that over 10% of memory is being consumed by some math library that I never use?"

    Which leads to the next point: Most computer programs don't use floating point anyway. I don't think a single Windows program I've written in the past ten years has had need for floating point much less needed to read one from a dialog box. Sure, if you're doing numerical work, then you need floating point, but most Windows programs are like Regedit and Notepad, not Excel. Adding GetDlgItemFloat to the window manager would have required adding the floating point emulator to the window manager (a big kick in the teeth since the window manager is always loaded), as well as atof (which is not an easy function to write).

    If you want GetDlgItemFloat you can write it yourself, and then you can shoulder the burden of runtime floating point support and handling floating point exceptions. (We'll learn more about the scary world of floating point exceptions in a few months.)

    (For a similar reason, the wsprintf function does not support floating point.)

    Pre-emptive Igor Levicki comment: "Windows Vista is a bloated piece of junk."

  • The Old New Thing

    The art of losing things: Keep moving them around

    • 39 Comments

    The worst thing about losing things is that when you eventually find them, they're always where you left them. (This assumes you live alone or otherwise can control who touches your stuff.) I have a mental place for most things, and I keep them there, which is great, because when I need, say, my passport, I know where to go.

    Except that I undermine my own organizational system. For example, the passport will be in the passport drawer for months, and then a trip is coming up, so I go to the passport drawer and confirm, yup it's still there. And then here's where I undermine myself: I take the passport out of the passport drawer and move it to a "better" location. In this moment of temporary insanity, I've convinced myself that this new location is better because it's more convenient, more obvious, more... something.

    And then shortly before the trip, I go to the passport drawer and... it's not there! I then realize, "Oh right, I moved it a few weeks ago to a better place. Where was that better place?" I spend the next hour or so searching through my entire house looking for the passport. Eventually, I find it, but it's a few hours of my life I'll never get back.

    After repeating this comedy of errors year after year, I finally figured it out: Don't move stuff around! Leave it where it is, even if where it is happens to be, in your momentary opinion, suboptimal.

    That rule has served me well, but some time ago it backfired. I needed to recharge the battery in my digital camera, and I looked in the obvious locations without success. And then I remembered, "Wait, I saw the recharger two weeks ago, and I resisted the urge to move it to a 'better' location." Unfortunately, I couldn't remember the original location.

    I didn't find the recharger for two days.

    Reminder: This Web site will be on autopilot next week and most of the week after that because I will be out of the country. Assuming I can find my passport.

    Hopefully nobody will post a comment that gets me fired.

  • The Old New Thing

    Why can't I convert a time zone abbreviation into a time zone?

    • 19 Comments
    Public Service Announcement
    Daylight Savings Time begins this weekend in most parts of the United States.

    Suppose you have a string of the form "Thu Mar 27 03:46:20 CST 2003". How can you parse this into something your program can manipulate, like say a SYSTEMTIME or a FILETIME?

    Basically, you can't in the general case. The time zone abbreviation CST is ambiguous. It could mean U.S./Canada Central Standard Time, Australian Central Standard Time, China Standard Time, or Cuba Summer Time. There may be other possibilities as well. Without any other context, the time zone abbreviation CST could mean any of those time zones. (This doesn't stop people from asking for the feature anyway. Maybe they want fuzzy logic or psychic powers.) Note also that there is an international standard for representing dates and times in text form, including the time zone.

    Okay, back to the original problem. In order to get a definite answer, you will need to restrict your domain to resolve the ambiguity. You might decide, for example, that you only care about the four continental United States time zones. Believe it or not, this is what JScript does!

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var f = fso.GetFolder("C:\\");
    var fc = new Enumerator(f.files);
    for (; !fc.atEnd(); fc.moveNext()) {
      WScript.echo("File: " + fc.item());
      WScript.echo("Date: " + fc.item().DateLastModified);
    }
    

    When I run this program via cscript testprogram.js, I get the following:

    File: C:\AUTOEXEC.BAT
    Date: Sun Jan 9 17:13:09 PST 2005
    File: C:\boot.ini
    Date: Tue Nov 22 11:53:48 PST 2005
    File: C:\CONFIG.SYS
    Date: Sun Jan 9 17:13:09 PST 2005
    File: C:\hiberfil.sys
    Date: Thu Dec 14 23:29:00 PST 2006
    File: C:\IO.SYS
    Date: Sun Jan 9 17:13:09 PST 2005
    File: C:\MSDOS.SYS
    Date: Sun Jan 9 17:13:09 PST 2005
    File: C:\NTDETECT.COM
    Date: Tue Aug 10 11:00:00 PDT 2004
    File: C:\ntldr
    Date: Tue Aug 10 11:00:00 PDT 2004
    File: C:\pagefile.sys
    Date: Thu Dec 14 23:28:59 PST 2006
    

    Hey, look, that time zone got inserted. But if we make a tiny change to the way we print the date

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var f = fso.GetFolder("C:\\");
    var fc = new Enumerator(f.files);
    for (; !fc.atEnd(); fc.moveNext()) {
      WScript.echo("File: " + fc.item());
      WScript.echo("Date:", fc.item().DateLastModified);
    }
    

    the results are quite different:

    File: C:\AUTOEXEC.BAT
    Date: 1/9/2005 5:13:09 PM
    File: C:\boot.ini
    Date: 11/22/2005 11:53:48 AM
    File: C:\CONFIG.SYS
    Date: 1/9/2005 5:13:09 PM
    File: C:\hiberfil.sys
    Date: 12/14/2006 11:29:00 PM
    File: C:\IO.SYS
    Date: 1/9/2005 5:13:09 PM
    File: C:\MSDOS.SYS
    Date: 1/9/2005 5:13:09 PM
    File: C:\NTDETECT.COM
    Date: 8/10/2004 11:00:00 AM
    File: C:\ntldr
    Date: 8/10/2004 11:00:00 AM
    File: C:\pagefile.sys
    Date: 12/14/2006 11:28:59 PM
    

    In this modified version, we're print the time and date directly instead of using the JScript conversion. This time, the hard-coded U.S. English days of the week, months, and time zones aren't present. (I haven't checked whether OLE Automation follows local settings.)

    (Reminder: Whether daylight saving time is a good idea has already been discussed, so please don't waste everybody's time by bringing it up again. Thanks.)

  • The Old New Thing

    How do I log on using a dial-up connection on Windows Vista?

    • 31 Comments

    Mike Stephens from the Group Policy Team Blog explains how to get "Log on using dial-up connections" working on Windows Vista.

    But I'm posting to respond to a comment on that page, since that falls under the category of "When people ask for security holes as features."

    The only problem is all users need to have access to an account with local admin privileges [in order to set this up].

    The implied request is that non-administrative users be allowed to create dial-up connections that can be used for logging on. This request falls into the category of When people ask for security holes as features; in this case, it's a repudiation security vulnerability. Here's how.

    A non-administrative user creates a dial-up networking connectoid and marks it as available for use during logon. For the phone number, the non-administrative user uses a voting number for a television reality show, one that charges $2 per call. (If you are more mercenary, you can arrange to set up a phone number that charges $50/minute and agree to split the profits.) The non-administrative user then logs off and waits.

    When the show starts, the non-administrative user then goes up to the computer and instead of logging on normally, goes to the dial-up connection button and selects the dial-up connectoid. The non-administrative user then proceeds to make dozens of failed logon attempts with that connectoid, under bogus user names like SanjayaRocks or WilliamHung4Ever. Each failed logon attempt casts a vote for the contestant, and (here's the important part) since nobody is actually logged on, you can't prove who made the calls.

    Some time later, the non-administrative user logs on and deletes the dial-up networking connectoid, to clean up afterward.

    The next month, the system administrator gets the phone bill and sees $100 worth of calls to the television show. The system administrator goes to the audit logs to see who made those calls, only to find that they were made by nobody. Even if the system administrator finds the logs for the non-administrative user having created and subsequently deleted the offending dial-up networking connectoid, that's just circumstantial evidence. "I created those for fun, as a joke. I never actually used them. It must've been just somebody walking past the machine who saw that they could use it to vote for Sanjaya."

  • The Old New Thing

    Not my finest hour: Using the wrong mouse

    • 36 Comments

    I used the wrong mouse for nearly an entire day.

    There are a good number of computers in my office, but they all funnel through to two sets of mice, keyboards, and monitors. One set is connected to the machine I use for day-to-day activities; the other set is connected through a switch box to a collection of machines which I used for testing and a variety of other secondary purposes.

    The switch box that controls the second set of mouse, keyboard and monitor makes no attempt to be smart, and if I move the mouse while it is connected to a computer that is broken into the kernel debugger, the switch box will often lose track of the mouse state, rendering it non-functional in all of the machines. To get the mouse to work in a particular machine, I have to reboot it. Since the machines are doing useful (although secondary) work, I don't want to reboot them. Whenever the mouse gets messed up like this, I just rely much more heavily on keyboard shortcuts, and when there's something that really needs a mouse, I fire up MouseKeys and use the numeric keypad to simulate a physical mouse.

    One day, I spilled my drink on my desk, and I had to mop it up with some paper towels. And then after I cleaned up the mess, I found that the mouse didn't work. This didn't phase me much, since the mouse had a tendency to wonk out and I just went into my fallback mode of relying on the keyboard for getting stuff done. It's a bit more cumbersome, but it's not the end of the world.

    It wasn't until near the end of the day that I realized that my mouse was just fine. It was a simple PEBKAC: I was using the wrong mouse.

    There are three mice on my desk, even though there are only two mouse/keyboard/monitor sets. Originally, the mouse on the first set was a PS/2 mouse, but I later replaced it with a USB laser mouse since I was getting tired of cleaning the gunk off of the mouse ball. As a belt-and-suspenders sort of thing, I left the PS/2 mouse plugged in even though I didn't use it any more, figuring I could use it if this whole USB thing didn't pan out. Of course, the USB mouse works just fine, so the PS/2 mouse just sat on my desk, taking up space but otherwise not causing trouble.

    Until I started using the PS/2 mouse thinking that it belonged to the second mouse/keyboard/monitor set. I didn't notice that the mouse was actually controlling the first computer because I wasn't looking at the first monitor when I used the PS/2 mouse; I was looking at the second monitor. And the cursor didn't move on the second monitor.

    Once I realized what I was doing, I went ahead and unplugged the PS/2 mouse. The USB mouse on the first computer runs just fine.

  • The Old New Thing

    I hadn't realized it's an entire genre: Music made from Windows system sounds

    • 22 Comments

    One. Two. Three.

    Pre-emptive snarky comment: "That's about all Microsoft is good for: making a song out of their critical stop and error tones."

Page 3 of 4 (33 items) 1234