August, 2008

Larry Osterman's WebLog

Confessions of an Old Fogey
  • Larry Osterman's WebLog

    Larry’s new favorite windows message – WM_PRINTCLIENT

    • 18 Comments

    As I’ve mentioned before, I’ve been fuddling around doing UI programming recently – it’s a bit different from my usual work deep in the bowels of the <insert whatever subsystem Larry happens to be working on (redirector, audio stack, POP3 server, whatever)>.

     

    I’ve been having a great deal of fun doing the whole “UI programmer” thingy (although my grandboss is right – once you start doing UI, everyone thinks that you’ve done it wrong) recently, and I’ve learned a ton of stuff.

    Most of the UI I work with is written as dialog box applications – for various reasons, many of the audio UI elements are actually dialog boxes[1].  As such, the system controls the actual drawing of the UI elements, especially if you specify the WS_CLIPCHILDREN window style.

    I recently had a bug where one of the UI elements wasn’t being painted correctly.  For a number of reasons, the element was being overwritten by another element.

     

    It took a lot of work to get to the scenario where the UI element got messed up, so I was going to ignore the problem (it was ugly but transient).  But then I got to browsing the web and I ran into this article.  The article had nothing to do with the order of painting in a dialog, but it DID mention a windows message that I hadn’t heard of before, WM_PRINTCLIENT.

    Hmm, that’s interesting.  WM_PRINTCLIENT says “The WM_PRINTCLIENT message is sent to a window to request that it draw its client area in the specified device context, most commonly in a printer device context.”.  Ooh, that’s interesting.  If I can just send a WM_PRINTCLIENT to each of the controls in the window, maybe I can control the order in which the controls are rendered, which would allow me to fix the problem (and remove a long-standing issue with the program in question).

    On Monday of last week, I came in and decided that I’d dedicate a couple of days to see if my idea might work – worst case I’d lose a couple of days of work, but if it did work it would be really cool.

    By mid-afternoon on Monday, I had come to the point where I realized that my crazy idea would actually work. 

    Skip forward one very long week of debugging and tweaking and I had fixed the problem – I had rebuilt the painting algorithm for this application and it worked! 

     

    Right now I’m highly enamored of the WM_PRINTCLIENT message simply because it’s so darned useful in scenarios like mine.

     

     

    PS: It’s possible that I might be able to achieve similar results with the WS_EX_COMPOSITED window style but it’s not clear given some of the UI requirements for the application – I may play around with that idea over the next couple of weeks.

     

    [1] One of the reasons for the apps being dialog box based is that it makes it easier for localizers if the application is built on dialog boxes because it gives the localizers greater flexibility when translating the OS – if the preferred text in the destination language doesn’t fit, they can adjust the layout of the dialog to make the text fit.

  • Larry Osterman's WebLog

    Linus Torvalds is “Fed up with the ‘security circus’”

    • 23 Comments

    There’s been a lot of discussion on the intertubes about some comments that Linus Torvalds, the creator of Linux has made about security vulnerabilities and disclosure.

    Not surprisingly, there’s been a fair amount of discussion amongst the various MSFT security folks about his comments and about the comments about his comments (are those meta-comments?).

     

    The whole thing started with a posting from Linus where he says:

    Btw, and you may not like this, since you are so focused on security, one reason I refuse to bother with the whole security circus is that I think it glorifies - and thus encourages - the wrong behavior.

    It makes "heroes" out of security people, as if the people who don't just fix normal bugs aren't as important.

    He also made some (IMHO) unprofessional comments about the OpenBSD community, but I don’t think that’s relevant to my point.

    Linus has followed up his initial post with an interview with Network World where he commented:

    “You need to fix things early, and that requires a certain level of disclosure for the developers," Torvalds states, adding, "You also don't need to make a big production out of it."”

    and

    "What does the whole security labeling give you? Except for more fodder for either of the PR camps that I obviously think are both idiots pushing for their own agenda?" Torvalds says. "It just perpetrates that whole false mind-set" and is a waste of resources, he says.

    As a part of our internal discussion, Crispin Cowan pointed out that Linus doesn’t issue security updates for Linux, instead the downstream distributions that contain the Linux kernel issue security fixes.

    That comment was the catalyst – after he made the comment, I realized that I think I understand the meaning behind Linus’ comments.

    IMHO, Linus is thinking about security bugs as an engineer.  And as an engineer, he’s completely right (cue the /. trolls: “MSFT engineer thinks that Linux inventor is right about something!”). 

    As a software engineer, I fully understand where Linus is coming from: From a strict engineering standpoint, security bugs are no different from any other bugs, and treating them as somehow “special” denigrates other bugs.  It’s only when you consider the consequences of security bugs that they become more interesting.

    A non security bug can result in an unbootable system or the loss of data on the affected machine.  And they can be very, very bad.  But security bugs are special because they’re bugs that allow a 3rd party to mess with your system in ways that you didn’t intend.

    Simply put, your customers data is at risk from security bugs in a way that normal defects aren’t.  There are lots of bad people out there who would just love to exploit any security defect in your product.  Security updates are more than just “PR”, they provide critical information that customers use to help determine the risk associated with taking a fix.

    Every time your customer needs to update the software on their computer, they take the risk that the update will break something (that’s a large part of the reason that that MSFT takes it’s time when producing security fixes – we test the heck out of stuff to reduce the risk to our customers).  But because the bad guys can use security vulnerabilities to compromise their customers data, your customers want to roll out security fixes faster than they roll out other fixes.

    That’s why it’s so important to identify security fixes – your customers use this information for risk management.  It’s also why Microsoft’s security bulletins carry mitigating factors that would help identify if customers are at risk.  For example MS08-045 which contains a fix for CVE-2008-2257 has a mitigating factor that mentions that in Windows Server 2003 and Windows Server 2008 the enhanced security configuration mode mitigates this vulnerability.  A customer can use that information to know if they will be affected by MS08-045.

    But Linus’ customers aren’t the users of Linux.  They are the people who package up Linux distribution.  As Crispin commented, the distributions are the ones that issue the security bulletins and they’re the ones that work with their customers to ensure that the users of the distribution are kept safe.

    By not clearly identifying which fixes are security related fixes, IMHO Linus does his customers a disservice – it makes the job of the distribution owner harder because they can’t report security defects to their customers.  And that’s why reporting security bug fixes is so important.

    Edit: cleared out some crlfs

    Edit2: s/Linus/Linux/ :)

  • Larry Osterman's WebLog

    What’s wrong with this code, part 22 – Drawing Text…

    • 43 Comments

    Recently I’ve been working on something that I’ve never done before in my almost 24 years at Microsoft. 

     

    For the past 23ish years, I’ve been a plumber – all the work I’ve done has been under the covers.  But for the next version of Windows, I decided to stretch my boundaries a bit and try some UI programming.  I’ve just spent the past few days working on a cool change to the volume control (it’s not important what it is, and most people will never know about the change, but those that do will probably agree with me :)).

    As part of the change, I needed to measure the dimensions of a text string.  This is a dummy version of some code I wrote, I simply called DrawText with the DT_CALCRECT into a memory DC that I created.

    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
    {
       HWND hWnd;
    
       hInst = hInstance; // Store instance handle in our global variable
    
       hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
          CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
    
       if (!hWnd)
       {
          return FALSE;
       }
    <BEGIN LARRYS CODE>
       HDC hdc = CreateCompatibleDC(NULL);
    
       RECT rcText = {0, 0, 88, 34};
    
       DrawText(hdc, L"My Text String", -1, &rcText, DT_CENTER | DT_END_ELLIPSIS | DT_EDITCONTROL | DT_WORDBREAK | DT_NOPREFIX | DT_CALCRECT);
    
       CAtlString string;
       string.Format(L"Text String occupies: %d x %d pixels", rcText.right - rcText.left, rcText.bottom - rcText.top);
       MessageBox(hWnd, string, L"String Size", 0);
    <END LARRYS CODE> 
       ShowWindow(hWnd, nCmdShow);
       UpdateWindow(hWnd);
    
       return TRUE;
    }

    This is just code I took by using Visual Studio to create a Windows Win32 project and inserting the code between “BEGIN LARRYS CODE” and “END LARRYS CODE”.  The meat of the code is just 3 lines of code.

    Even though there’s almost no code here, it still has a bug in it that was quite subtle and took me several hours to find.

  • Larry Osterman's WebLog

    THIS is what a Windows PC should look like…

    • 18 Comments

    We had a neighborhood picnic on Saturday at our neighbors house.  While we were chatting in the kitchen, I noticed their new computer.

    They had an HP TouchSmart computer, and I have to say that I was blown away by it.  I really liked the industrial design and the touch interface is really smooth.

     

    6-24-08-touchsmart-2

    All in all a machine that I’d be happy put in my kitchen.  It wouldn’t work as a desktop PC for me (I prefer to have more customizability than you can get in an all-in-one), but for our kitchen PC (which we almost never upgrade) it would be absolutely perfect.  

     

    I wish more OEMs spent as much time as HP clearly has on making their machines beautiful.

  • Larry Osterman's WebLog

    Why doesn’t Windows support amplification of audio samples?

    • 15 Comments

    Nils Arthur asked in another post:

    While we are talking volume controls. Could you explain why it's only possible to lower the volume in Windows (i.e. setting a volume between 0% and 100%) and not raise it (i.e setting it higher than 100%)?

    Before I get into the the answer, let me define some terms:  Attenuation means reducing the amplitude of a signal from a baseline - so if the signal is a full range sine wave going from 1.0 to -1.0, if you attenuate it to 50%, you get a sine wave from 0.5 to -0.5.  I wrote about it (with pictures :)) in this post.

    The answer to Nils' question is both simple and complicated.

    The simple part of the answer: Because most PC audio hardware only supports attenuation and not amplification.

    Now for the complicated parts of the answer: We only support what the hardware allows for master volume.  And most hardware only supports attenuation.  There are a lot of reasons for that, but the primary one is that it's dramatically cheaper (and uses less power) to attenuate signals than it is to amplify them.

    The other issue w.r.t. amplification/attenuation is signal quality. As I mentioned in the post on volume above, you can attenuate a sample in the digital domain without loss of fidelity.  However when you attempt to amplify a signal in the digital domain, it clips.   That means that amplification MUST be done in the analog domain.  Again, this goes to hardware costs - because amplification needs to be done in the analog domain, it means that the audio hardware needs to have an amplifier that can be digitally controlled, which is (again) more expensive.  The audio hardware doesn't even have to support hardware volume - if Windows doesn't find a hardware volume control, it simply inserts a master volume into the audio pipeline.

    Some audio hardware DOES support amplification, but the audio volume controls map the volume control from low to high into a range from 0..100 because it's dramatically simpler to represent that to the user.

    That means that if an audio solution presents a hardware volume from -96.0dB to +3dB (there are a number of them that do that), we'll map that 99dB range into a 0.0 to 1.0 range that maps nicely into a slider.  We've thought about differentiating between attenuation and amplification in the volume UI, but the reality is that the net effect is the same whether we represent amplification or not.

    You can see if your audio hardware supports amplification by going to the multimedia control panel.  Select the audio endpoint you want to check, go to the "Properties" dialog.  On that dialog, check the "Levels" tab, the hardware master volume control is present there.  You can right click on the text box and change the units from linear to dB, you can then move the slider around to see the dB range.  Or you could write some code and call the IAudioEndpointVolume::GetVolumeRange API, which will return the information directly.

  • Larry Osterman's WebLog

    What makes a bug a security bug?

    • 22 Comments

    In my last post, I mentioned that security bugs were different from other bugs.  Daniel Prochnow asked:

    What is the difference between bug and vulnerability?

    In my point of view, in a production enviroment, every bug that may lead to a loss event (CID, image, $) must be considered a security incident.

    What do you think?

    I answered in the comments, but I think the answer deserves a bit more commentary, especially when Evan asked:

    “I’m curious to hear an elaboration of this.  System A takes information from System B.  The information read from System A causes a[sic] System B to act in a certain way (which may or may not lead to leakage of data) that is unintended.  Is this a security issue or just a bug?”

    Microsoft Technet has a definition for a security vulnerability:

    “A security vulnerability is a flaw in a product that makes it infeasible – even using the product properly – to prevent an attacker from usurping privileges on the user’s system, regulating it’s operation, compromising data on it or assuming ungranted trust.”

    IMHO, that’s a bit too lawyerly, although the article does an excellent job of breaking down the definition and making it understandable.

    Crispin Cowan gave me an alternate definition, which I like much better:

    Security is the preservation of:

    · Confidentiality: your secret stuff stays secret

    · Integrity: your data stays intact

    · Availability: your systems and data remain available

    A vulnerability is a bug such that an attacker can compromise one or more of the above properties

     

    In Evan’s example, I think there is a security bug, but maybe not.  For instance, it’s possible that System A validates (somehow) that System B hasn’t been compromised.  In that case, it might be ok to trust the data read from System B.  That’s part of the reason for the wishy-washy language of the official vulnerability definition.

    To me, the key concept in determining if a bug is a security bug or not is that of an unauthorized actor.  If an authorized user performs operations on a file to which the user has access and the filesystem corrupts their data, it’s a bug (a bad bug that MUST be fixed, but a bug nonetheless).  If an unauthorized user can cause the filesystem to corrupt the data of another user, that’s a security bug.

    When a user downloads a file from the Internet, they’re undoubtedly authorized to do that.  They’re also authorized to save the file to the local system.  However the program that reads the file downloaded from the Internet cannot trust the contents of the file (unless it has some way of ensuring that the file contents haven’t been tampered with[1]).  So if there’s a file parsing bug in the program that parses the file, and there’s no check to ensure the integrity of the file, it’s a security bug.

     

    Michael Howard likes using this example:

    char foo[3];
    foo[3] = 0;

    Is it a bug?  Yup.  Is it a security bug?  Nope, because the attacker can’t control anything.  Contrast that with:

    struct
    {
        int value;
    } buf;
    char foo[3];

    _read(fd, &buf, sizeof(buf));
    foo[buf->value] = 0;

    That’s a 100% gen-u-wine security bug.

     

    Hopefully that helps clear this up.

     

     

    [1] If the file is cryptographically signed with a signature from a known CA and the certificate hasn’t been revoked, the chances of the file’s contents being corrupted are very small, and it might be ok to trust the contents of the file without further validation. That’s why it’s so important to ensure that your application updater signs its updates.

  • Larry Osterman's WebLog

    Go see Shrek the Musical. Right now!

    • 9 Comments

    We just got back home from seeing Shrek the Musical which is currently in try-outs at the Fifth Avenue Theatre here in Seattle.

     

    This show's going to be BIG when it hits Broadway.  I'm talking Hairspray big.

    It's one of the funniest shows I've seen in a really long time.  The main characters are brilliant, and the writing is very funny. 

    Brian D'Arcy James was great as Shrek and Sutton Foster as Fiona was uncannily like the movie version.  It was scary at times.

    I was particularly struck by the scene where they introduced Fiona.  They had three actresses playing the part of Fiona - at age 7, 15 and 25ish, they each sang part of the same song.  It was beautiful and really touching.

    Not surprisingly the show had some rough moments, but in it still was one of the best shows I've seen in a long while.

    One very nice touch in the show is that just as the movie of Shrek had lots of clever references to classic fairy tales, the musical version of Shrek is filled with clever references to other Broadway shows[1].   For instance, there's a scene where the fairy tale characters are all interviewed by Lord Farquaad (the villain).  The characters all stand in a line on stage and Farquaad interviews them using the "G_d Mike" (as Daniel calls it).  The characters then sing a clever song about how they want Farquaad to pick them.  It's a pastiche of A Chorus Line.  Other shows we saw referenced were Wicked, The Lion King, Avenue Q[2], High School Musical and others.

     

     

    Over the past few years, I've seen 6 different shows on tryouts in Seattle: Hairspray, Princesses, The Wedding Singer, Young Frankenstein, Lone Star Love and Shrek.  After seeing the shows, I've accurately predicted how 5 of the six would do (Princesses and Lone Star Love disappeared, The Wedding Singer was a pretty good success, Young Frankenstein was funny and was a hit but isn't nearly as good as The Producers was, and of course Hairspray was utterly huge).  I honestly think that Shrek could top Hairspray.  It is THAT good.

     

    So go see Shrek if you can.  If you can't see it in Seattle, get tickets to see it on Broadway.  You won't be disappointed.

     

     

     

    [1] Disclaimer: The show I saw is in VERY early tryouts.  So far, they've had exactly 4 performances in front of a live audience.  It's entirely possible that anything or everything I saw tonight might change before it hits Broadway.

    [2] The Avenue Q reference was when the character of Pinocchio, played by John Tartaglia says that he's going to make a mix tape for someone else (if you've seen Avenue Q, in it Princeton makes a mix tape for Kate Monster).  What makes this bit particularly funny is that John Tartaglia originated the role of Princeton in Avenue Q.

  • Larry Osterman's WebLog

    Gotchas associated with using WM_PRINTCLIENT…

    • 22 Comments

    Yesterday I mentioned WM_PRINTCLIENT and how awesome it is when trying to strictly control the drawing of your application.

    Part of the reason it took over a week to change the drawing model is that there are a number of serious gotcha’s associated with using WM_PRINTCLIENT and controlling your own drawing story.  The first is that not all controls support WM_PRINTCLIENT.  It turns out that some controls don’t support the WM_PRINTCLIENT, however if you search the documentation for WM_PAINT, you’ll find the following comment:

    “For some common controls, the default WM_PAINT message processing checks the wParam parameter. If wParam is non-NULL, the control assumes that the value is an HDC and paints using that device context.”

    That means that if you find a common control that doesn’t support WM_PRINTCLIENT, you can use WM_PAINT specifying wParam as the HDC[1].  Fortunately I didn’t run into this in my control.

    The next gotcha is that some controls (like buttons and toolbars etc) have animations that are launched when you mouse over the control.  These are often subtle (like the glow when you hover over a scrollbar thumb).  In order to continue to have these effects work, you need to let those controls paint themselves – in my experience it generally didn’t cause much of a problem with flickering, but your mileage might vary.

     

    The last gotcha is a very big one and hung me up for about half a day.  The WM_PRINTCLIENT message only paints the client area of a window.  If you have a window with the WM_HSCROLL or WM_VSCROLL style then you won’t be able to paint the scroll bar.  Instead you need to create your own scrollbar control (with CreateWindow) and use that instead of the built-in scroll styles.  There are ways you can convince the window to paint it’s non client region to an HDC but they are fraught with peril (one senior developer I was talking to about this problem described them as “unnatural acts”) and simply not worth the effort.

     

     

     

    [1] There’s also a corollary to that: If you ever send a WM_PAINT to a control you MUST ensure that wParam and lParam are 0 even though they’re documented as “not used”.

  • Larry Osterman's WebLog

    Remembering Aaron Reynolds

    • 8 Comments

    Last Saturday I received an email from Tandy Trower, a long time Microsoft employee letting me know that Aaron Reynolds had unexpectedly passed away.  It threw me for a loop, Aaron isn’t that much older than I am and I'm not used to hearing about people I knew dying unexpectedly.  Even though it’s been over a week, I’m still a bit rocky about it.

     

    Aaron was one of the early MS-DOS and Windows developers, and I looked on him as one of my mentors back when I was a new hire at Microsoft.  He was the original author of the MS-NET redirector which I later inherited, so I spent a fair amount of time asking Aaron what this or that mysterious piece of code did.

    Aaron was often gruff, it was sometimes an adventure going to his office to ask him a question.  You’d knock on the door and if he was busy he would continue to work for as long as 5 to 10 minutes until he had finished whatever it was he was dealing with and only then would he check to see if you were still there.  But once you had his attention, he would patiently explain with great detail everything you needed to know about your problem.  And he always knew the answer.

     

    He was a font of knowledge about Windows and DOS, as Tandy said in his email: “To this day there is probably code inside Windows that only Aaron really understood why it was there.”

     

    I haven’t seen Aaron in a few years, but my boss tells me that he used to see him every week or so at Seattle Mariners games, Aaron had Diamond Club seats and rarely missed a home game.

     

    He will be missed by all who knew him.

  • Larry Osterman's WebLog

    The SS_PATHELLIPSIS, SS_ENDELLIPSIS and SS_WORDELLIPSIS styles force static controls to disable word wrap.

    • 5 Comments

    I didn’t find this in the documentation for the static standard window so I figured I’d put it out in my blog in the hopes that someone else won’t get stuck on this problem.

    The Windows “static” control has a number of styles that control how the control draws text.  They can be extremely useful when controlling how the control lays out text.

    I recently had to write code that draws the name of an audio endpoint into a relatively size constrained static text control.  No problem, right[1]?

    Speakers

    Well what happens if the name of the endpoint is somewhat longer? No problem, the SS_CENTER style will wrap the words.

    Headset Microphone

    But sometimes the endpoint name is longer than “Headset Microphone”.  In fact the user can set the endpoint name to anything they want.

    Long Headset M

    Uck, that looks ugly.

    Fortunately the static control has an option SS_WORDELLIPSIS that looks like it should be perfect for me:

    “Windows NT or later: Truncates any word that does not fit in the rectangle and adds ellipses.”

    That sounds exactly like what I want.  But when I added SS_WORDELLIPSIS, I got:

    Long He...

     

     

    Hold on, that’s not right, what happened to the rest of my text?

    After a bit of digging, I finally figured out what was going on (by trial an error removing all the various static control styles I specified).

     

    It turns out that this behavior is by design, even though I couldn’t find any documentation of it.  If you specify any of the ellipsis styles for the static control, the control becomes a single line control.  The only way I’ve found to fix this is to make the static control an owner draw control and use the DrawThemeText (or DrawText) API specifying the DT_WORDBREAK | DT_ENDELLIPSIS option.

    I’ve filed a documentation bug to ensure that this gets fixed sometime in the near future.

    [1] I’ve approximated what was going on in my UI with HTML tables, this is a rough approximation to show more-or-less what was going on.

    ETA: Thanks to Drew and Lindseth for reviewing this.

    Edit: Fixed word wrap in edit control.

  • Larry Osterman's WebLog

    What’s wrong with this code, Part 22 – the answers

    • 14 Comments

    The other day, I wrote about measuring the dimensions of a piece of text using the DrawText API.

    My code worked just great when I initially tested it (obviously it’s a part of a larger chunk of code that does more complicated work).  The problem showed up when I started testing it on a machine running in High DPI mode (144DPI).

    The code in question measured some text and then used that text to set the size of a button, when working in low DPI mode (96DPI), the font that is chosen had font metrics that closely matched the font that was used when painting the button.  The problem was that the button font that was used in high DPI mode had dramatically larger font metrics than the low DPI font. 

    As a result, the rectangle returned by the initial DrawText call didn’t match the rectangle that was used when the button was painted.  That meant that the button text overflowed the button.

     

    The fix to the code is to retrieve the font that is going to be used to draw the button and to use SelectObject to set the font in the memory DC to match the button font.  Once I made that change, the button drew perfectly.

    Here’s the corrected code (new code in red):

       HDC hdc = CreateCompatibleDC(NULL); 
    
       RECT rcText = {0, 0, 88, 34}; 
    
       HFONT hFont = reinterpret_cast<HFONT>(SendMessage(m_hWndControl, WM_GETFONT, 0, 0)); 
       HFONT hFontOld = reinterpret_cast<HFONT>(SelectObject(hdc, hFont)); 
    
       DrawText(hdc, L"My Text String", -1, &rcText, DT_CENTER | DT_END_ELLIPSIS | DT_EDITCONTROL | DT_WORDBREAK | DT_NOPREFIX | DT_CALCRECT); 
    
       CAtlString string; 
       string.Format(L"Text String occupies: %d x %d pixels", rcText.right - rcText.left, rcText.bottom - rcText.top); 
       MessageBox(hWnd, string, L"String Size", 0); 
    
       SelectObject(hdc, hFontOld); 
       DeleteDC(hdc);

     

    And yeah, I’m sure this is old-hat to experienced Windows UI programmers, but it stumped me for several hours so I figured I’d write it up for the blog in case someone else hits the same problem.

    Kudos and omissions:

    Aaron Ballman caught that I forgot to call DeleteDC when I was done with using the DC.  Stupid boneheaded mistake.

    Shog9 and Ivo caught the root cause (selecting the wrong font).

     

    And Eldan caught the Large Fonts implication.  There are other ways of catching this, but HighDPI is the easiest.

    I also agree with his assertion that font handling of GDI is “complicated”.  On the other hand, the alternative is adding a dozen parameters to the DrawText API (you’d have to add foreground color, background color, font, etc to the call, which would complicate the API dramatically). 

     

    PS For those who care about such things: When I’m testing UI changes, I start with standard DPI, then I retest the changes in HighDPI and again using high contrast mode.  Testing in high contrast mode also tests the code in a non themed mode, which helps to catch bugs where I accidentally depended on the theming logic.

Page 1 of 1 (11 items)