//********************************************************************************
// Mozilla GBM plug-in: GbmBitmapViewer.cpp
//
// Copyright (C) 2006-2008 Heiko Nitzsche
//
//   This software is provided 'as-is', without any express or implied
//   warranty.  In no event will the author be held liable for any damages
//   arising from the use of this software.
//
//   Permission is granted to anyone to use this software for any purpose,
//   including commercial applications, and to alter it and redistribute it
//   freely, subject to the following restrictions:
//
//   1. The origin of this software must not be misrepresented; you must not
//      claim that you wrote the original software. If you use this software
//      in a product, an acknowledgment in the product documentation would be
//      appreciated but is not required.
//   2. Altered source versions must be plainly marked as such, and must not be
//      misrepresented as being the original software.
//   3. This notice may not be removed or altered from any source distribution.
//
//********************************************************************************
//
// Requires: GBM.DLL version 1.35 or higher (with multipage support)
//
//********************************************************************************
//
// History:
//
//   16-Jul-2006: Initial release
//
//   30-Jul-2006: Add FULL mode viewer with a load of features
//                * menus, popup menu, scrollers
//                * variable scaled preview
//                * transformations mirror/transpose/rotate
//                * multipage navigation
//                * save the shown page to any GBM supported bitmap file format
//                * uses GBM File Dialog if found, otherwise OS/2 File Dialog is used
//                * English and German localized with autodetection via LANG variable
//
//                Add EMBED mode viewer with reduced functionality of FULL mode viewer
//                * popup menu
//                * transformations mirror
//                * multipage navigation
//                * save the shown page to any GBM supported bitmap file format
//                * uses GBM File Dialog if found, otherwise OS/2 File Dialog is used
//                * printing of embedded bitmaps is supported on Mozilla/Firefox/SeaMonkey
//                * English and German localized with autodetection via LANG variable
//
//   02-Aug-2006: Add bitmap information dialog box
//
//   08-Sep-2006  1bpp bitmaps were shown inverted (colors swapped)
//
//   21-Sep-2006  * Add help support for the GBM file dialog
//                * Allow all dialogs to be moved outside the plugin window
//                * Improved detection of bitmap file formats
//
//   09-Sep-2007  * 8bpp grayscale and true color images are now scaled with a high
//                  high quality algorithm to improve appearance.
//
//   27-Jan-2008  * Resampling scaler now supports all grayscale bitmaps <=8bpp
//
//   02-Feb-2008  * Move HPS rendering code to common package GbmRenderer
//                  to save memory (now renders directly from cache buffer)
//                  without temporary subrectangle buffer
//                * Add support for configurable scaling filter
//
//********************************************************************************

#ifdef __IBMCPP__
  #pragma strings( readonly )
#endif

#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

#define INCL_WIN
#include "os2defs.h"
#include "npgbmrc.h"

#include "GbmDocument.hpp"
#include "GbmDocumentPage.hpp"
#include "GbmException.hpp"
#include "GbmDialogAccessor.hpp"

#include "GbmBitmapViewer.hpp"

// ---------------------------------------------------------

// Keep in sync with Resource file npgbm.rc
static const int GBM_PLUGIN_VERSION = 134;  // 1.34

// Window title for the viewer and the help window
static const char * GBMBITMAPVIEWER_Title = "GBM Bitmap Viewer";
static const char * WC_GBMVIEWER          = "GBMBitmapViewer";

// Keep in sync with npgbm.rc and npgbmrc.h resource IDs !!!

// Index of language specific resource ID into lookup table.
#define RES_RID_GBMVIEWER                    0
#define RES_RID_GBMVIEWER_SMALL_POPUP        1
#define RES_RID_GBMVIEWER_FULL_POPUP         2
#define RES_RID_GBMVIEWER_FORMATINFO         3
#define RES_SID_GBMVIEWER_UNSUPPORTED_FMT    4
#define RES_SID_GBMVIEWER_SAVEAS             5
#define RES_SID_GBMVIEWER_OVERRIDE_FILE      6

// Keep in sync with npgbm.rc and npgbmrc.h resource IDs !!!

struct GbmBitmapViewer::LANG_LOOKUP_TABLE_DEF GbmBitmapViewer::LANG_LOOKUP_TABLE [] =
{
    { "_EN", { RID_GBMVIEWER_EN,
               RID_GBMVIEWER_SMALL_POPUP_EN,
               RID_GBMVIEWER_FULL_POPUP_EN,
               RID_GBMVIEWER_FORMATINFO_EN,
               SID_GBMVIEWER_STRING_UNSUPPORTED_FMT_EN,
               SID_GBMVIEWER_STRING_SAVEAS_EN,
               SID_GBMVIEWER_STRING_OVERRIDE_FILE_EN
             }
    },
    { "_DE", { RID_GBMVIEWER_DE,
               RID_GBMVIEWER_SMALL_POPUP_DE,
               RID_GBMVIEWER_FULL_POPUP_DE,
               RID_GBMVIEWER_FORMATINFO_DE,
               SID_GBMVIEWER_STRING_UNSUPPORTED_FMT_DE,
               SID_GBMVIEWER_STRING_SAVEAS_DE,
               SID_GBMVIEWER_STRING_OVERRIDE_FILE_DE
             }
    }
};

int GbmBitmapViewer::LANG_LOOKUP_TABLE_LENGTH = sizeof(GbmBitmapViewer::LANG_LOOKUP_TABLE) /
                                                sizeof(GbmBitmapViewer::LANG_LOOKUP_TABLE[0]);

// ---------------------------------------------------------
// ---------------------------------------------------------

#define MIN(a,b)  (((a) < (b)) ? (a) : (b))
#define MAX(a,b)  (((a) > (b)) ? (a) : (b))

// ---------------------------------------------------------
// ---------------------------------------------------------

//
// Here is the window procedure for the client window.
//
static MRESULT APIENTRY GlobalWindowProcedure(HWND   hwnd,
                                              ULONG  msg,
                                              MPARAM mp1,
                                              MPARAM mp2)
{
    GbmBitmapViewer * pViewer = (GbmBitmapViewer *) WinQueryWindowULong(hwnd, QWL_USER);
    if (pViewer != NULL)
    {
        return pViewer->instanceWindowProcedure(hwnd, msg, mp1, mp2);
    }
    return (MRESULT) FALSE;
}

//
// Here is the window procedure for the GBM file dialog box.
//
static MRESULT APIENTRY GlobalGbmDlgWindowProcedure(HWND   hwnd,
                                                    ULONG  msg,
                                                    MPARAM mp1,
                                                    MPARAM mp2)
{
    const FILEDLG * fild = (const FILEDLG *) WinQueryWindowULong(hwnd, QWL_USER);
    if (fild != NULL)
    {
        GbmBitmapViewer * pViewer = (GbmBitmapViewer *) fild->ulUser;
        return pViewer->instanceGbmDlgWindowProcedure(hwnd, msg, mp1, mp2);
    }
    return (MRESULT) FALSE;
}

//
// Here is the window procedure for the bitmap info dialog box.
//
static MRESULT APIENTRY GlobalInfoDlgWindowProcedure(HWND   hwnd,
                                                     ULONG  msg,
                                                     MPARAM mp1,
                                                     MPARAM mp2)
{
    switch(msg)
    {
        case WM_INITDLG:
        {
            char numberBuffer[20] = { 0 };

            GbmBitmapViewer * pViewer = (GbmBitmapViewer *) PVOIDFROMMP(mp2);

            // set the info into the text fields
            WinSetDlgItemText(hwnd, TID_FORMATINFO_FORMAT, pViewer->getLongDescription());

            if (pViewer->hasBitmap())
            {
                sprintf(numberBuffer, "%d", pViewer->getPageWidth());
                WinSetDlgItemText(hwnd, TID_FORMATINFO_WIDTH, numberBuffer);

                sprintf(numberBuffer, "%d", pViewer->getPageHeight());
                WinSetDlgItemText(hwnd, TID_FORMATINFO_HEIGHT, numberBuffer);

                sprintf(numberBuffer, "%d", pViewer->getPageColorDepth());
                WinSetDlgItemText(hwnd, TID_FORMATINFO_BPP, numberBuffer);

                sprintf(numberBuffer, "%d", pViewer->getNumberOfPages());
                WinSetDlgItemText(hwnd, TID_FORMATINFO_PAGES, numberBuffer);
            }
            else
            {
                WinSetDlgItemText(hwnd, TID_FORMATINFO_WIDTH , "-");
                WinSetDlgItemText(hwnd, TID_FORMATINFO_HEIGHT, "-");
                WinSetDlgItemText(hwnd, TID_FORMATINFO_BPP   , "-");
                WinSetDlgItemText(hwnd, TID_FORMATINFO_PAGES , "-");
            }

            SWP   swpDlg    = { 0 };
            RECTL rclParent = { 0 };

            // center the dialog window
            WinQueryWindowRect(pViewer->getFrameHwnd(), &rclParent);
            WinQueryWindowPos (hwnd, &swpDlg);

            // set the horizontal coordinate of the lower-left corner
            const LONG x = ((LONG) rclParent.xRight - swpDlg.cx) / 2 + swpDlg.x;

            // set vertical coordinate of the lower-left corner
            const LONG y = ((LONG) rclParent.yTop - swpDlg.cy) / 2 + swpDlg.y;

            // move, size, and show the window
            WinSetWindowPos(hwnd, HWND_TOP, x, y, 0, 0, SWP_MOVE);
        }

        case WM_COMMAND:
           switch (SHORT1FROMMP(mp1))
           {
              case DID_OK:
                  WinDismissDlg(hwnd, DID_OK);
                  return 0;

              default:
                  break;
           }
           break;

        // -------------------------

        default:
            break;
    }

    return WinDefDlgProc(hwnd, msg, mp1, mp2);
}

//----------------------------------------------------------------------------
//----------------------------------------------------------------------------

static VOID EnableMenuItem(HWND hWndMenu, SHORT idMenuItem, BOOL fEnabled)
{
    WinSendMsg(hWndMenu, MM_SETITEMATTR,
           MPFROM2SHORT(idMenuItem, TRUE),
           MPFROM2SHORT(MIA_DISABLED, ( fEnabled ) ? 0 : MIA_DISABLED));
}

static VOID CheckMenuItem(HWND hWndMenu, SHORT idMenuItem, BOOL fChecked)
{
    WinSendMsg(hWndMenu, MM_SETITEMATTR,
           MPFROM2SHORT(idMenuItem, TRUE),
           MPFROM2SHORT(MIA_CHECKED, ( fChecked ) ? MIA_CHECKED : 0));
}

//----------------------------------------------------------------------------

static void SetScrollBar(HWND hwndScroll, SHORT pos, SHORT min, SHORT max)
{
     /* Set the scroll-bar value and range. */
     WinSendMsg(hwndScroll, SBM_SETSCROLLBAR,
                MPFROM2SHORT(pos, 0),
                MPFROM2SHORT(min, max));
}

static void SetScrollBarPos(HWND hwndScroll, SHORT pos)
{
     /* Set the vertical scroll-bar value. */
     WinSendMsg(hwndScroll, SBM_SETPOS, MPFROMSHORT(pos), (MPARAM) NULL);
}

static void SetScrollThumbSize(HWND hwndScroll, SHORT visible, SHORT total)
{
     /* Set the vertical scroll-bar value. */
     WinSendMsg(hwndScroll, SBM_SETTHUMBSIZE, MPFROM2SHORT(visible, total), (MPARAM) NULL);
}

// ---------------------------------------------------------

static BOOL CheckFileExistsAndQueryOverride(HWND hwnd, const CHAR * filename, const CHAR * message)
{
  struct stat buf;

  if (stat(filename, &buf) != -1)
  {
    if (MBID_YES != WinMessageBox(HWND_DESKTOP,
                                 hwnd,
                                 message,
                                 GBMBITMAPVIEWER_Title,
                                 0, MB_YESNO | MB_DEFBUTTON2 | MB_QUERY | MB_MOVEABLE))
    {
      return FALSE;
    }
  }
  return TRUE;
}

// ---------------------------------------------------------
// ---------------------------------------------------------

GbmBitmapViewer::GbmBitmapViewer(      HMODULE             hMod,
                                       GbmDocument       & document,
                                 const GbmDialogAccessor * pDlgAccessor)
    : mpLanguage(&LANG_LOOKUP_TABLE[0]),
      mDllModule(hMod),
      mGbmDocument(document),
      mpDlgAccessor(pDlgAccessor),
      mCurrentPopupMenuId(-1),
      mCurrentPageIndex(0),
      mScaleX(1.0),
      mScaleY(1.0),
      mZoomMode(GbmBitmapViewer::ZoomMode_NONE),
      mScalerType(GbmRenderer::ScalerType_SIMPLE),
      mLoadProgress(0),
      mLoadProgressMax(0),
      mHwndFrame(0),
      mHwndClient(0),
      mHwndHScroll(0),
      mHwndVScroll(0),
      mHwndMenu(0),
      mHwndPopupMenu(0),
      mHwndHelp(0),
      mHScrollMax(0),
      mHScrollPos(0),
      mHScrollInc(0),
      mVScrollMax(0),
      mVScrollPos(0),
      mVScrollInc(0),
      mCxClient(0),
      mCyClient(0)
{
    // init string buffers
    memset(mSaveAsFilename      , 0, sizeof(mSaveAsFilename));
    memset(mSaveAsOptions       , 0, sizeof(mSaveAsOptions));
    memset(mStringUnsupportedFmt, 0, sizeof(mStringUnsupportedFmt));
    memset(mStringSaveAs        , 0, sizeof(mStringSaveAs));
    memset(mStringOverrideFile  , 0, sizeof(mStringOverrideFile));

    // check system language and lookup the matching resource set
    const char * systemLanguage    = getenv("LANG");
    const int    systemLanguageLen = strlen(systemLanguage);

    char * systemLanguageUpperCase = new char[systemLanguageLen + 1];
    memset(systemLanguageUpperCase, 0, systemLanguageLen + 1);

    for (int c = 0; c < systemLanguageLen; c++)
    {
        systemLanguageUpperCase[c] = toupper(systemLanguage[c]);
    }
    for (int i = 0; i < LANG_LOOKUP_TABLE_LENGTH; i++)
    {
        if (strstr(systemLanguageUpperCase, LANG_LOOKUP_TABLE[i].language) != NULL)
        {
            mpLanguage = &LANG_LOOKUP_TABLE[i];
            break;
        }
    }
    // if the system language was found, keep English as default

    delete [] systemLanguageUpperCase;
    systemLanguageUpperCase = NULL;
}

// ---------------------------------------------------------

GbmBitmapViewer::~GbmBitmapViewer()
{
    if (mHwndFrame != 0)
    {
        destroyHelpInstance();
        WinDestroyWindow(mHwndFrame);
    }
    mHwndFrame     = 0;
    mHwndClient    = 0;
    mHwndHScroll   = 0;
    mHwndVScroll   = 0;
    mHwndMenu      = 0;
    mHwndPopupMenu = 0;
    mCurrentPopupMenuId = -1;
}

// ---------------------------------------------------------

/*
 * Create help instance for GBM file dialog context help.
 */
BOOL GbmBitmapViewer::createHelpInstance(const HAB hab)
{
    if (mpDlgAccessor != NULL)
    {
        if (mpDlgAccessor->hasPublicGbmDefFileDlgProc())
        {
            HELPINIT helpinit;

            /* Setup the help initialization structure */
            helpinit.cb                       = sizeof(HELPINIT);
            helpinit.ulReturnCode             = 0L;
            helpinit.pszTutorialName          = (PSZ)NULL;

            /* Help table in plugin resource */
            helpinit.phtHelpTable             = (HELPTABLE *) (0xffff0000L | RID_GBMVIEWER_GBMDLG_HELPTABLE);
            helpinit.hmodHelpTableModule      = mDllModule;

            /* Default action bar and accelerators */
            helpinit.hmodAccelActionBarModule = NULLHANDLE;
            helpinit.idAccelTable             = 0;
            helpinit.idActionBar              = 0;
            helpinit.pszHelpWindowTitle       = (PSZ) GBMBITMAPVIEWER_Title;
            helpinit.fShowPanelId             = CMIC_HIDE_PANEL_ID;
            helpinit.pszHelpLibraryName       = (PSZ) mpDlgAccessor->getHelpFileName();

            /* Create and associate the help instance */
            mHwndHelp = WinCreateHelpInstance(hab, &helpinit);
            return (mHwndHelp != NULLHANDLE) ? TRUE : FALSE;
        }
    }
    mHwndHelp = NULLHANDLE;
    return FALSE;
}

void GbmBitmapViewer::destroyHelpInstance()
{
    if (mHwndHelp != NULLHANDLE)
    {
        WinDestroyHelpInstance(mHwndHelp);
        mHwndHelp = NULLHANDLE;
    }
}

// ---------------------------------------------------------

void GbmBitmapViewer::setParentWindow(HWND hwnd, BOOL fullMode)
{
    if (mHwndFrame != 0)
    {
        destroyHelpInstance();
        WinDestroyWindow(mHwndFrame);
        mHwndFrame     = 0;
        mHwndClient    = 0;
        mHwndHScroll   = 0;
        mHwndVScroll   = 0;
        mHwndMenu      = 0;
        mHwndPopupMenu = 0;
        mCurrentPopupMenuId = -1;
    }

    if (hwnd == 0)
    {
        return;
    }

    const HAB hab = WinQueryAnchorBlock(hwnd);

    // load language specific strings
    memset(mStringUnsupportedFmt, 0, sizeof(mStringUnsupportedFmt));
    memset(mStringSaveAs        , 0, sizeof(mStringSaveAs));
    memset(mStringOverrideFile  , 0, sizeof(mStringOverrideFile));

    WinLoadString(hab, mDllModule, mpLanguage->resid[RES_SID_GBMVIEWER_UNSUPPORTED_FMT],
                  sizeof(mStringUnsupportedFmt), mStringUnsupportedFmt);
    WinLoadString(hab, mDllModule, mpLanguage->resid[RES_SID_GBMVIEWER_SAVEAS],
                  sizeof(mStringSaveAs), mStringSaveAs);
    WinLoadString(hab, mDllModule, mpLanguage->resid[RES_SID_GBMVIEWER_OVERRIDE_FILE],
                  sizeof(mStringOverrideFile), mStringOverrideFile);

    // configure the viewer either for FULL or EMBEDDED mode
    if (fullMode)
    {
        // create the embedded frame window with menu
        ULONG flFrameFlags = FCF_MENU       | FCF_ACCELTABLE |
                             FCF_VERTSCROLL | FCF_HORZSCROLL |
                             FCF_NOBYTEALIGN;

        WinRegisterClass(WinQueryAnchorBlock(hwnd),
                         WC_GBMVIEWER,
                         (PFNWP) GlobalWindowProcedure,
                         0L,
                         4L);

        mHwndFrame = WinCreateStdWindow(
            hwnd,                                 /* Parent window handle              */
            WS_VISIBLE,                           /* Style of frame window             */
            &flFrameFlags,                        /* Pointer to control data           */
            WC_GBMVIEWER,                         /* Client window class name          */
            GBMBITMAPVIEWER_Title,                /* Title bar text                    */
            0L,                                   /* Style of client window            */
            mDllModule,                           /* Module handle for resources       */
            mpLanguage->resid[RES_RID_GBMVIEWER], /* ID of resources                   */
            &mHwndClient);                        /* Pointer to client window handle   */

        WinSetWindowULong(mHwndClient, QWL_USER, (ULONG) this);

        // load the popup menu
        mCurrentPopupMenuId = RES_RID_GBMVIEWER_FULL_POPUP;
        mHwndPopupMenu      = WinLoadMenu(mHwndClient, mDllModule, mpLanguage->resid[mCurrentPopupMenuId]);

        mHwndHScroll = WinWindowFromID(mHwndFrame, FID_HORZSCROLL);
        mHwndVScroll = WinWindowFromID(mHwndFrame, FID_VERTSCROLL);
        mHwndMenu    = WinWindowFromID(mHwndFrame, FID_MENU);

        mZoomMode = GbmBitmapViewer::ZoomMode_NONE;
    }
    else
    {
        // create the embedded frame window with menu
        ULONG flFrameFlags = FCF_NOBYTEALIGN;

        WinRegisterClass(WinQueryAnchorBlock(hwnd),
                         WC_GBMVIEWER,
                         (PFNWP) GlobalWindowProcedure,
                         0L,
                         4L);

        mHwndFrame = WinCreateStdWindow(
            hwnd,                                 /* Parent window handle              */
            WS_VISIBLE,                           /* Style of frame window             */
            &flFrameFlags,                        /* Pointer to control data           */
            WC_GBMVIEWER,                         /* Client window class name          */
            GBMBITMAPVIEWER_Title,                /* Title bar text                    */
            0L,                                   /* Style of client window            */
            mDllModule,                           /* Module handle for resources       */
            mpLanguage->resid[RES_RID_GBMVIEWER], /* ID of resources                   */
            &mHwndClient);                        /* Pointer to client window handle   */

        WinSetWindowULong(mHwndClient, QWL_USER, (ULONG) this);

        // load the popup menu
        mCurrentPopupMenuId = RES_RID_GBMVIEWER_SMALL_POPUP;
        mHwndPopupMenu      = WinLoadMenu(mHwndClient, mDllModule, mpLanguage->resid[mCurrentPopupMenuId]);

        mHwndHScroll = 0;
        mHwndVScroll = 0;
        mHwndMenu    = mHwndPopupMenu;

        mZoomMode = GbmBitmapViewer::ZoomMode_STRETCH;
    }

    // if creation of help instance fails, we can still continue without help
    createHelpInstance(hab);

    mHScrollMax  = 0;
    mHScrollPos  = 0;
    mVScrollMax  = 0;
    mVScrollPos  = 0;
    mHScrollInc  = 0;
    mVScrollInc  = 0;
    mCxClient    = 0;
    mCyClient    = 0;

    // set initial scrollbar settings
    updateScrollBars(mHwndClient);
}

// ---------------------------------------------------------

void GbmBitmapViewer::setLoadProgress(const int progress, const int progressMax)
{
    mLoadProgress    = (progress    < 0) ? 0 : progress;
    mLoadProgressMax = (progressMax < 0) ? 0 : progressMax;

    if (mLoadProgress > mLoadProgressMax)
    {
        mLoadProgress = mLoadProgressMax;
    }
    if (mHwndClient != NULLHANDLE)
    {
        WinInvalidateRect(mHwndClient, 0, TRUE);
        WinUpdateWindow(mHwndClient);
    }
}

// ---------------------------------------------------------

void GbmBitmapViewer::setScalerType(const GbmRenderer::ScalerType scalerType)
{
    mScalerType = scalerType;
    if (mHwndClient != NULLHANDLE)
    {
        WinInvalidateRect(mHwndClient, 0, TRUE);
        WinUpdateWindow(mHwndClient);
    }
}

// ---------------------------------------------------------

BOOL GbmBitmapViewer::hasBitmap() const
{
    return (mGbmDocument.getNumberOfPages() < 1) ? FALSE : TRUE;
}

// ---------------------------------------------------------

void GbmBitmapViewer::draw(HPS hps, RECTL *rect, double pageScaleX, double pageScaleY)
{
    if (! hasBitmap())
    {
        POINTL startPoint = { rect->xLeft , rect->yBottom };
        POINTL endPoint   = { rect->xRight, rect->yTop };

        if ((mLoadProgress    > 0) &&
            (mLoadProgressMax > 0) &&
            (mLoadProgress    <= mLoadProgressMax))
        {
            endPoint.y = rect->yTop * mLoadProgress / mLoadProgressMax;
            GpiSetColor(hps, CLR_DARKGRAY);
            GpiMove    (hps, &startPoint);
            GpiBox     (hps, DRO_OUTLINEFILL, &endPoint, 0L, 0L);

            startPoint.y = endPoint.y;
            endPoint.y   = rect->yTop;

            GpiSetColor(hps, CLR_PALEGRAY);
            GpiMove    (hps, &startPoint);
            GpiBox     (hps, DRO_OUTLINEFILL, &endPoint, 0L, 0L);
        }
        else
        {
            GpiSetColor(hps, CLR_PALEGRAY);
            GpiMove    (hps, &startPoint);
            GpiBox     (hps, DRO_OUTLINEFILL, &endPoint, 0L, 0L);
        }
        return;
    }

    try
    {
        // we show so far always page 0
        GbmDocumentPage & page(mGbmDocument.getPage(mCurrentPageIndex));
        GbmRenderer     & renderer(mGbmDocument.getRenderer());

        // check if render rect is larger than the actual bitmap data
        const int pageWidth  = (int)(pageScaleX * (double)page.getWidth());
        const int pageHeight = (int)(pageScaleY * (double)page.getHeight());

        LONG yOffset = rect->yTop - rect->yBottom - pageHeight;
        if (yOffset < 0)
        {
            yOffset = 0;
        }

        RECTL renderRectl = *rect;

        if (renderRectl.xRight - renderRectl.xLeft > pageWidth)
        {
            renderRectl.xRight = pageWidth + renderRectl.xLeft;

            // draw only the visible area not filled by the bitmap right beside
            POINTL unusedAreaStartPoint = { renderRectl.xRight + 1, renderRectl.yBottom };
            POINTL unusedAreaEndPoint   = { rect->xRight          , renderRectl.yTop };

            GpiSetColor(hps, CLR_DARKGRAY);
            GpiMove    (hps, &unusedAreaStartPoint);
            GpiBox     (hps, DRO_OUTLINEFILL, &unusedAreaEndPoint, 0L, 0L);
        }
        if (renderRectl.yTop - renderRectl.yBottom > pageHeight)
        {
            renderRectl.yTop = pageHeight + renderRectl.yBottom;

            // draw only the visible area not filled by the bitmap right beside
            POINTL unusedAreaStartPoint = { rect->xLeft , renderRectl.yBottom + yOffset - 1 };
            POINTL unusedAreaEndPoint   = { rect->xRight, rect->yBottom };

            GpiSetColor(hps, CLR_DARKGRAY);
            GpiMove    (hps, &unusedAreaStartPoint);
            GpiBox     (hps, DRO_OUTLINEFILL, &unusedAreaEndPoint, 0L, 0L);
        }

        // calculate the source rectangle to be extracted from the bitmap
        const int src_x      = mHScrollPos;
        const int src_y      = mVScrollPos;
        const int src_width  = (int) (renderRectl.xRight - renderRectl.xLeft);
        const int src_height = (int) (renderRectl.yTop   - renderRectl.yBottom);

        // correct render area and move the bitmap to be shown at the top rather than the bottom
        renderRectl.yBottom += yOffset - 1;
        renderRectl.yTop    += yOffset - 1;

        renderer.setRenderPage(&page);
        renderer.setScalerType(mScalerType);
        renderer.renderToHPS(     src_x,      src_y,
                              src_width, src_height,
                             pageScaleX, pageScaleY, renderRectl, hps);
    }
    catch(...)
    {
        POINTL startPoint = { rect->xLeft , rect->yBottom };
        POINTL endPoint   = { rect->xRight, rect->yTop };

        GpiSetColor(hps, CLR_PALEGRAY);
        GpiMove    (hps, &startPoint);
        GpiBox     (hps, DRO_OUTLINEFILL, &endPoint, 0L, 0L);
    }
}

//----------------------------------------------------------------------------

int GbmBitmapViewer::getNumberOfPages() const
{
    return mGbmDocument.getNumberOfPages();
}

int GbmBitmapViewer::getPageWidth()
{
    try
    {
        GbmDocumentPage & page(mGbmDocument.getPage(mCurrentPageIndex));
        return page.getWidth();
    }
    catch(GbmException & ex)
    {
    }
    return 0;
}

int GbmBitmapViewer::getPageHeight()
{
    try
    {
        GbmDocumentPage & page(mGbmDocument.getPage(mCurrentPageIndex));
        return page.getHeight();
    }
    catch(GbmException & ex)
    {
    }
    return 0;
}

int GbmBitmapViewer::getPageColorDepth()
{
    try
    {
        GbmDocumentPage & page(mGbmDocument.getPage(mCurrentPageIndex));
        return page.getColorDepth();
    }
    catch(GbmException & ex)
    {
    }
    return 0;
}

const char * GbmBitmapViewer::getLongDescription() const
{
    if (hasBitmap())
    {
        return mGbmDocument.getLongDescription();
    }
    return mStringUnsupportedFmt;
}

//----------------------------------------------------------------------------

void GbmBitmapViewer::executeOperations(HWND hwnd, long rotateBy, GbmDocumentPage::MirrorMode mirrorMode)
{
    try
    {
        // we show so far always page 0
        GbmDocumentPage & page(mGbmDocument.getPage(mCurrentPageIndex));
        GbmRenderer     & renderer(mGbmDocument.getRenderer());

        // Reset the renderer as it cannot detect these changes in all cases
        // because the size of the bitmap and the page index might not change.
        renderer.setRenderPage(NULL);

        // Set the render page rotation angle
        // -> we rotate the original page data as this is lossless and most memory conservative
        page.rotate(rotateBy);

        // Set the render page mirror mode
        // -> we mirror the original page data as this is lossless and most memory conservative
        page.mirror(mirrorMode);
    }
    catch(...)
    {
    }
    WinInvalidateRect(hwnd, 0, TRUE);
    WinUpdateWindow(hwnd);
}

//----------------------------------------------------------------------------

void GbmBitmapViewer::gotoPageIndex(HWND hwnd, int pageIndex)
{
    if (pageIndex != mCurrentPageIndex)
    {
        const int oldPageIndex = mCurrentPageIndex;

        mCurrentPageIndex = MAX(pageIndex, 0);
        mCurrentPageIndex = MIN(mCurrentPageIndex, mGbmDocument.getNumberOfPages()-1);

        if (mCurrentPageIndex != oldPageIndex)
        {
            WinInvalidateRect(hwnd, 0, TRUE);
            WinUpdateWindow(hwnd);
        }
    }
}

//----------------------------------------------------------------------------

void GbmBitmapViewer::updateScaleFactor(HWND hwnd, BOOL forceWindowRepaint, BOOL skipWindowRepaint)
{
    const double oldScaleX = mScaleX;
    const double oldScaleY = mScaleY;

    switch(mZoomMode)
    {
        case GbmBitmapViewer::ZoomMode_NONE:
            mScaleX = 1.0;
            mScaleY = mScaleX;
            break;

        case GbmBitmapViewer::ZoomMode_FITWIN:
        {
            const int pageWidth  = getPageWidth();
            const int pageHeight = getPageHeight();
            const double scaleX  = (pageWidth  > 0) ? ((double)mCxClient)/pageWidth  : 1.0;
            const double scaleY  = (pageHeight > 0) ? ((double)mCyClient)/pageHeight : 1.0;
            mScaleX = MIN(scaleX, scaleY);
            mScaleY = mScaleX;
            break;
        }

        case GbmBitmapViewer::ZoomMode_FITWIDTH:
        {
            const int pageWidth = getPageWidth();
            mScaleX = (pageWidth > 0) ? ((double)mCxClient)/pageWidth  : 1.0;
            mScaleY = mScaleX;
            break;
        }

        case GbmBitmapViewer::ZoomMode_STRETCH:
        {
            const int pageWidth  = getPageWidth();
            const int pageHeight = getPageHeight();
            mScaleX = (pageWidth  > 0) ? ((double)mCxClient)/pageWidth  : 1.0;
            mScaleY = (pageHeight > 0) ? ((double)mCyClient)/pageHeight : 1.0;
            break;
        }

        default: // ZoomMode_FREE
            break;
    }

    if ((! skipWindowRepaint) &&
        (forceWindowRepaint || (mScaleX != oldScaleX) || (mScaleY != oldScaleY)))
    {
        WinInvalidateRect(hwnd, 0, TRUE);
        WinUpdateWindow(hwnd);
    }
}

//----------------------------------------------------------------------------

void GbmBitmapViewer::updateWindowSize(HWND hwnd)
{
    /* get window size */
    RECTL rect = { 0, 0, 0, 0 };
    WinQueryWindowRect(hwnd, &rect);

    mCxClient = rect.xRight - rect.xLeft;
    mCyClient = rect.yTop   - rect.yBottom;
}

//----------------------------------------------------------------------------

void GbmBitmapViewer::updateScrollBars(HWND hwnd)
{
    updateWindowSize(hwnd);

    if (mHwndHScroll && mHwndVScroll)
    {
        if (hasBitmap())
        {
            try
            {
                GbmDocumentPage & page(mGbmDocument.getPage(mCurrentPageIndex));

                const int pageWidth  = mScaleX * page.getWidth();
                const int pageHeight = mScaleY * page.getHeight();

                mHScrollMax = MAX(0, pageWidth - mCxClient);
                mHScrollPos = MIN(mHScrollPos, mHScrollMax);

                mVScrollMax = MAX(0, pageHeight - mCyClient);
                mVScrollPos = MIN(mVScrollPos, mVScrollMax);

                SetScrollBar(mHwndHScroll, mHScrollPos, 0, mHScrollMax);
                SetScrollBar(mHwndVScroll, mVScrollPos, 0, mVScrollMax);

                const SHORT visibleH = (pageWidth  > mCxClient) ? mCxClient : pageWidth;
                const SHORT visibleV = (pageHeight > mCyClient) ? mCyClient : pageHeight;

                SetScrollThumbSize(mHwndHScroll, visibleH, pageWidth);
                SetScrollThumbSize(mHwndVScroll, visibleV, pageHeight);

                WinEnableWindow(mHwndHScroll, mHScrollMax ? TRUE : FALSE);
                WinEnableWindow(mHwndVScroll, mVScrollMax ? TRUE : FALSE);
            }
            catch(GbmException & ex)
            {
            }
        }
        else
        {
            WinEnableWindow(mHwndHScroll, FALSE);
            WinEnableWindow(mHwndVScroll, FALSE);
        }
    }
}

//----------------------------------------------------------------------------

void GbmBitmapViewer::updateMenuItems(HWND hwnd, SHORT itemId) const
{
    const BOOL hasData  = hasBitmap();
    const int  numPages = mGbmDocument.getNumberOfPages();

    // work either on the frame menu or if visible the popup menu (same menu style)
    const HWND refHwndMenu = WinIsWindowVisible(mHwndPopupMenu) ? mHwndPopupMenu : mHwndMenu;

    switch(itemId)
    {
        case MID_FILE:
            EnableMenuItem(refHwndMenu, MID_FILE_SAVE_AS, hasData);
            break;

        // -------------------------

        case MID_BITMAP:
            EnableMenuItem(refHwndMenu, MID_BITMAP_REF_HORZ , hasData);
            EnableMenuItem(refHwndMenu, MID_BITMAP_REF_VERT , hasData);
            EnableMenuItem(refHwndMenu, MID_BITMAP_TRANSPOSE, hasData);
            EnableMenuItem(refHwndMenu, MID_BITMAP_ROT_P90  , hasData);
            EnableMenuItem(refHwndMenu, MID_BITMAP_ROT_M90  , hasData);
            break;

        // -------------------------

        case MID_VIEW:
            // Page submenu
            EnableMenuItem(refHwndMenu, MID_VIEW_PAGE      , numPages > 1);
            EnableMenuItem(refHwndMenu, MID_VIEW_PAGE_FIRST, mCurrentPageIndex > 0);
            EnableMenuItem(refHwndMenu, MID_VIEW_PAGE_PREV , mCurrentPageIndex > 0);
            EnableMenuItem(refHwndMenu, MID_VIEW_PAGE_NEXT , mCurrentPageIndex < numPages-1);
            EnableMenuItem(refHwndMenu, MID_VIEW_PAGE_LAST , mCurrentPageIndex < numPages-1);

            // Zoom menu entries
            EnableMenuItem(refHwndMenu, MID_VIEW_ZOOM_DOWN    , hasData);
            EnableMenuItem(refHwndMenu, MID_VIEW_ZOOM_UP      , hasData);
            EnableMenuItem(refHwndMenu, MID_VIEW_ZOOM_NONE    , hasData);
            EnableMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIN  , hasData);
            EnableMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIDTH, hasData);

            switch(mZoomMode)
            {
                case GbmBitmapViewer::ZoomMode_NONE:
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_NONE    , TRUE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIN  , FALSE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIDTH, FALSE);
                    break;

                case GbmBitmapViewer::ZoomMode_FITWIN:
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_NONE    , FALSE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIN  , TRUE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIDTH, FALSE);
                    break;

                case GbmBitmapViewer::ZoomMode_FITWIDTH:
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_NONE    , FALSE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIN  , FALSE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIDTH, TRUE);
                    break;

                default: // ZoomMode_FREE
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_NONE    , FALSE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIN  , FALSE);
                    CheckMenuItem(refHwndMenu, MID_VIEW_ZOOM_FITWIDTH, FALSE);
                    break;
            }
            break;

        // -------------------------

        case MID_HELP:
            // Help submenu
            EnableMenuItem(refHwndMenu, MID_HELP_IMAGEINFO, hasData);
            break;

        // -------------------------

        case RES_RID_GBMVIEWER_SMALL_POPUP:
            EnableMenuItem(mHwndPopupMenu, MID_FILE_SAVE_AS    , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_REF_HORZ , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_REF_VERT , hasData);

            // Page submenu
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE      , numPages > 1);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_FIRST, mCurrentPageIndex > 0);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_PREV , mCurrentPageIndex > 0);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_NEXT , mCurrentPageIndex < numPages-1);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_LAST , mCurrentPageIndex < numPages-1);

            // Help submenu
            EnableMenuItem(mHwndPopupMenu, MID_HELP_IMAGEINFO, hasData);
            break;

        // -------------------------

        case RES_RID_GBMVIEWER_FULL_POPUP:
            EnableMenuItem(mHwndPopupMenu, MID_FILE_SAVE_AS    , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_REF_HORZ , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_REF_VERT , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_TRANSPOSE, hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_ROT_P90  , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_BITMAP_ROT_M90  , hasData);

            // Page submenu
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE      , numPages > 1);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_FIRST, mCurrentPageIndex > 0);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_PREV , mCurrentPageIndex > 0);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_NEXT , mCurrentPageIndex < numPages-1);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_PAGE_LAST , mCurrentPageIndex < numPages-1);

            // Zoom menu entries
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_DOWN    , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_UP      , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_NONE    , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIN  , hasData);
            EnableMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIDTH, hasData);

            switch(mZoomMode)
            {
                case GbmBitmapViewer::ZoomMode_NONE:
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_NONE    , TRUE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIN  , FALSE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIDTH, FALSE);
                    break;

                case GbmBitmapViewer::ZoomMode_FITWIN:
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_NONE    , FALSE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIN  , TRUE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIDTH, FALSE);
                    break;

                case GbmBitmapViewer::ZoomMode_FITWIDTH:
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_NONE    , FALSE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIN  , FALSE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIDTH, TRUE);
                    break;

                default: // ZoomMode_FREE
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_NONE    , FALSE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIN  , FALSE);
                    CheckMenuItem(mHwndPopupMenu, MID_VIEW_ZOOM_FITWIDTH, FALSE);
                    break;
            }

            // Help submenu
            EnableMenuItem(mHwndPopupMenu, MID_HELP_IMAGEINFO, hasData);
            break;

        // -------------------------

        default:
            break;
    }
}

//----------------------------------------------------------------------------

//
// Here is the window procedure for the client window.
//
MRESULT GbmBitmapViewer::instanceWindowProcedure(HWND   hwnd,
                                                 ULONG  msg,
                                                 MPARAM mp1,
                                                 MPARAM mp2)
{
    switch(msg)
    {
        case WM_SIZE:
            updateWindowSize(hwnd);
            updateScaleFactor(hwnd, FALSE, FALSE);
            updateScrollBars(hwnd);
            return 0;

        // -------------------------

        case WM_HSCROLL:
        {
            // set line increment to 200 steps of the image width
            SHORT scrollbar_line_increment = getPageWidth() / 200;
            scrollbar_line_increment = MAX(1, scrollbar_line_increment);

            switch(SHORT2FROMMP(mp2))
            {
                case SB_LINELEFT:
                    mHScrollInc = -scrollbar_line_increment;
                    break;

                case SB_LINERIGHT:
                    mHScrollInc = scrollbar_line_increment;
                    break;

                case SB_PAGELEFT:
                    mHScrollInc = MIN(-scrollbar_line_increment, -mCxClient);
                    break;

                case SB_PAGERIGHT:
                    mHScrollInc = MAX( scrollbar_line_increment, mCxClient);
                    break;

                case SB_SLIDERTRACK:
                    mHScrollInc = SHORT1FROMMP(mp2) - mHScrollPos;
                    break;

                default:
                    mHScrollInc = 0;
                    break;
            }

            mHScrollInc = MAX(-mHScrollPos, MIN(mHScrollInc, mHScrollMax - mHScrollPos));

            if (mHScrollInc != 0)
            {
                mHScrollPos += mHScrollInc;
                WinScrollWindow(hwnd, -mHScrollInc, 0, NULL, NULL, NULL, NULL, SW_INVALIDATERGN);
                SetScrollBarPos(mHwndHScroll, mHScrollPos);
                WinUpdateWindow(hwnd);
            }
            return 0;
        }

        // -------------------------

        case WM_VSCROLL:
        {
            // set line increment to 200 steps of the image width
            SHORT scrollbar_line_increment = getPageHeight() / 200;
            scrollbar_line_increment = MAX(1, scrollbar_line_increment);

            switch(SHORT2FROMMP(mp2))
            {
                case SB_LINEUP:
                    mVScrollInc = -scrollbar_line_increment;
                    break;

                case SB_LINEDOWN:
                    mVScrollInc = scrollbar_line_increment;
                    break;

                case SB_PAGEUP:
                    mVScrollInc = MIN(-scrollbar_line_increment, -mCyClient);
                    break;

                case SB_PAGEDOWN:
                    mVScrollInc = MAX( scrollbar_line_increment, mCyClient);
                    break;

                case SB_SLIDERTRACK:
                    mVScrollInc = SHORT1FROMMP(mp2) - mVScrollPos;
                    break;

                default:
                    mVScrollInc = 0;
                    break;
            }

            mVScrollInc = MAX(-mVScrollPos, MIN(mVScrollInc, mVScrollMax - mVScrollPos));

            if (mVScrollInc != 0)
            {
                mVScrollPos += mVScrollInc;
                WinScrollWindow(hwnd, 0, mVScrollInc, NULL, NULL, NULL, NULL, SW_INVALIDATERGN);
                SetScrollBarPos(mHwndVScroll, mVScrollPos);
                WinUpdateWindow(hwnd);
            }
            return 0;
        }

        // -------------------------

        case WM_CHAR:
        {
            // check if we have scrollers (full mode: yes, embedded: no)
            if (mHwndHScroll && mHwndVScroll)
            {
                const USHORT fsflags = SHORT1FROMMP(mp1);

                if (! (fsflags & KC_KEYUP))
                {
                    if (fsflags & KC_VIRTUALKEY)
                    {
                        // forward keyboard messages to the scrollers and let them do the processing
                        switch(SHORT2FROMMP(mp2))
                        {
                            case VK_LEFT:
                            case VK_RIGHT:
                                return WinSendMsg(mHwndHScroll, msg, mp1, mp2);

                            case VK_UP:
                            case VK_DOWN:
                            case VK_PAGEUP:
                            case VK_PAGEDOWN:
                                return WinSendMsg(mHwndVScroll, msg, mp1, mp2);
                        }
                    }
                }
            }
            break;
        }

        // -------------------------

        case WM_PAINT:
        {
            updateScaleFactor(hwnd, FALSE, TRUE);
            updateScrollBars(hwnd);

            RECTL invalidRect;
            HPS hps = WinBeginPaint(hwnd, NULL, &invalidRect);

            /* get window size */
            RECTL rect;
            WinQueryWindowRect(hwnd, &rect);

            // draw full visible window
            draw(hps, &rect, mScaleX, mScaleY);

            WinEndPaint(hps);
        }
        return 0;

        // -------------------------

        case WM_INITMENU:
            updateMenuItems(hwnd, SHORT1FROMMP(mp1));
            return 0;

        // -------------------------

        case WM_BUTTON2CLICK:
            // Update the menu item of the small popup window manually as it
            // has no submenus. Thus there is no WM_INITMENU send.
            updateMenuItems(hwnd, mCurrentPopupMenuId);

            WinPopupMenu(hwnd,
                         hwnd,
                         mHwndPopupMenu,
                         SHORT1FROMMP(mp1) /* x */,
                         SHORT2FROMMP(mp1) /* y */,
                         0,
                         PU_HCONSTRAIN |
                         PU_VCONSTRAIN |
                         PU_NONE       |
                         PU_KEYBOARD   | PU_MOUSEBUTTON1);
            return 0;

        // -------------------------

        case WM_COMMAND:
        {
            switch ( SHORT1FROMMP(mp1) )
            {
                // -------------------------
                // File menu
                // -------------------------

                case MID_FILE_SAVE_AS:
                {
                    if (showFileDialog_SaveAs(hwnd,
                                              mSaveAsFilename, sizeof(mSaveAsFilename),
                                              mSaveAsOptions , sizeof(mSaveAsOptions)))
                    {
                        if (CheckFileExistsAndQueryOverride(hwnd, mSaveAsFilename,
                                                                  mStringOverrideFile))
                        {
                            // Write file
                            try
                            {
                                mGbmDocument.writeDocumentPageToFile(mSaveAsFilename,
                                                                     mSaveAsOptions,
                                                                     mCurrentPageIndex);
                            }
                            catch(GbmException & ex)
                            {
                                const GbmAccessor & gbmAccessor(mGbmDocument.getGbmAccessor());

                                // extract error message
                                const char * msg = ex.getErrorMessage();
                                if (strlen(msg) == 0)
                                {
                                    const char * gbmErrorMsg = gbmAccessor.Gbm_err(ex.getErrorId());

                                    WinMessageBox(HWND_DESKTOP,
                                                  hwnd,
                                                  gbmErrorMsg,
                                                  "GBM Bitmap Viewer plugin",
                                                  0, MB_OK | MB_ERROR | MB_MOVEABLE);
                                }
                                else
                                {
                                    const char * gbmErrorMsg = gbmAccessor.Gbm_err(ex.getErrorId());

                                    // format the error string: gbm_error:\nuser_message
                                    char * pMessage = new char[strlen(gbmErrorMsg) + strlen(msg) + 3 + 1];
                                    sprintf(pMessage, "%s:\n%s", gbmErrorMsg, msg);

                                    WinMessageBox(HWND_DESKTOP,
                                                  hwnd,
                                                  pMessage,
                                                  "GBM Bitmap Viewer plugin",
                                                  0, MB_OK | MB_ERROR | MB_MOVEABLE);
                                    delete [] pMessage;
                                }
                            }
                        }
                    }
                    return 0;
                }

                // -------------------------
                // Bitmap menu
                // -------------------------

                case MID_BITMAP_REF_HORZ:
                    executeOperations(hwnd, 0, GbmDocumentPage::MirrorMode_HORIZONTAL);
                    return 0;

                case MID_BITMAP_REF_VERT:
                    executeOperations(hwnd, 0, GbmDocumentPage::MirrorMode_VERTICAL);
                    return 0;

                case MID_BITMAP_TRANSPOSE:
                    executeOperations(hwnd, 0, GbmDocumentPage::MirrorMode_TRANSPOSE);
                    return 0;

                case MID_BITMAP_ROT_P90:
                    executeOperations(hwnd, 90, GbmDocumentPage::MirrorMode_NONE);
                    return 0;

                case MID_BITMAP_ROT_M90:
                    executeOperations(hwnd, 270, GbmDocumentPage::MirrorMode_NONE);
                    return 0;

                // -------------------------
                // View menu
                // -------------------------

                case MID_VIEW_PAGE_FIRST:
                    gotoPageIndex(hwnd, 0);
                    return 0;

                case MID_VIEW_PAGE_PREV:
                    gotoPageIndex(hwnd, mCurrentPageIndex-1);
                    return 0;

                case MID_VIEW_PAGE_NEXT:
                    gotoPageIndex(hwnd, mCurrentPageIndex+1);
                    return 0;

                case MID_VIEW_PAGE_LAST:
                    gotoPageIndex(hwnd, mGbmDocument.getNumberOfPages()-1);
                    return 0;

                // -------------------------

                case MID_VIEW_ZOOM_DOWN:
                    mZoomMode = GbmBitmapViewer::ZoomMode_FREE;
                    mScaleX   = MAX(0.1, mScaleX/1.1);
                    mScaleY   = MAX(0.1, mScaleY/1.1);
                    updateScaleFactor(hwnd, TRUE, FALSE);
                    return 0;

                case MID_VIEW_ZOOM_UP:
                    mZoomMode = GbmBitmapViewer::ZoomMode_FREE;
                    mScaleX   = MIN(100, mScaleX*1.1);
                    mScaleY   = MIN(100, mScaleY*1.1);
                    updateScaleFactor(hwnd, TRUE, FALSE);
                    return 0;

                case MID_VIEW_ZOOM_NONE:
                    mZoomMode = GbmBitmapViewer::ZoomMode_NONE;
                    updateScaleFactor(hwnd, TRUE, FALSE);
                    return 0;

                case MID_VIEW_ZOOM_FITWIN:
                    mZoomMode = GbmBitmapViewer::ZoomMode_FITWIN;
                    updateScaleFactor(hwnd, TRUE, FALSE);
                    return 0;

                case MID_VIEW_ZOOM_FITWIDTH:
                    mZoomMode = GbmBitmapViewer::ZoomMode_FITWIDTH;
                    updateScaleFactor(hwnd, TRUE, FALSE);
                    return 0;

                // -------------------------
                // Help menu
                // -------------------------

                case MID_HELP_ABOUT:
                {
                    const char * info = "GBM plugin (version %.2f), using GBM.DLL %.2f\n(c) 2006-2008, Heiko Nitzsche";
                    const int infoLen = strlen(info);

                    char * pBuf = new char[infoLen + 10 + 1];
                    if (pBuf != NULL)
                    {
                        const GbmAccessor & gbmAccessor(mGbmDocument.getGbmAccessor());
                        sprintf(pBuf, info, ((float)GBM_PLUGIN_VERSION)/100.0,
                                            ((float)gbmAccessor.Gbm_version())/100.0);
                        WinMessageBox(HWND_DESKTOP,
                                      hwnd,
                                      pBuf,
                                      GBMBITMAPVIEWER_Title,
                                      0, MB_OK | MB_INFORMATION | MB_MOVEABLE);
                        delete [] pBuf;
                        pBuf = NULL;
                    }
                    return 0;
                }

                case MID_HELP_IMAGEINFO:
                {
                    WinDlgBox(HWND_DESKTOP,
                              hwnd,
                              GlobalInfoDlgWindowProcedure,
                              mDllModule,
                              mpLanguage->resid[RES_RID_GBMVIEWER_FORMATINFO],
                              this);
                    return 0;
                }
            }
            break;
        }

        default:
            break;
    }

    return WinDefWindowProc(hwnd, msg, mp1, mp2);
}

//----------------------------------------------------------------------------

//
// Here is the subclass window procedure for the GBM file dialog.
//
MRESULT GbmBitmapViewer::instanceGbmDlgWindowProcedure(HWND   hwnd,
                                                       ULONG  msg,
                                                       MPARAM mp1,
                                                       MPARAM mp2) const
{
    switch(msg)
    {
        case WM_ACTIVATE:
            if (mHwndHelp)
            {
                const HWND hwndActivated = HWNDFROMMP(mp2);
                if (hwndActivated == hwnd)
                {
                    const BOOL activated = SHORT1FROMMP(mp1); // usactive
                    if (activated)
                    {
                        WinAssociateHelpInstance(mHwndHelp, hwnd);
                    }
                    else
                    {
                        WinAssociateHelpInstance(NULLHANDLE, hwnd);
                    }
                }
            }
            break;

        /* We need to intercept the help manager message as the GBM file dialog
         * usually post help messages to the owner window (which we don't have here).
         */
        case WM_HELP:
            if (mHwndHelp)
            {
                WinSendMsg(mHwndHelp, HM_DISPLAY_HELP, MPFROMSHORT(HID_GBM_FILEDLG),
                                                       MPFROMSHORT(HM_RESOURCEID));
                return ((MRESULT) TRUE);
            }
            break;

        default:
            break;
    }
    return mpDlgAccessor->GbmDefFileDlgProc(hwnd, msg, mp1, mp2);
}

//----------------------------------------------------------------------------

/*
 * Show either GBM File Dialog if gbmdlg.dll is available or
 * use as fallback the standard OS/2 file dialog.
 */
BOOL GbmBitmapViewer::showFileDialog_SaveAs(HWND   hwnd,
                                            CHAR * filename, int filenameSize,
                                            CHAR * options , int optionsSize) const
{
    // use the GBM File Dialog to provide the user with more options if available
    GBMFILEDLG gbmfild;

    memset(&gbmfild, 0, sizeof(GBMFILEDLG));
    gbmfild.fild.cbSize   = sizeof(FILEDLG);
    gbmfild.fild.fl       = FDS_CENTER | FDS_ENABLEFILELB | FDS_SAVEAS_DIALOG;
    gbmfild.fild.pszTitle = (PSZ)mStringSaveAs;

    // set initial fields
    strncpy(gbmfild.fild.szFullFile, filename, MIN(filenameSize-1, sizeof(gbmfild.fild.szFullFile)-1));
    strncpy(gbmfild.szOptions      , options , MIN(optionsSize-1 , sizeof(gbmfild.szOptions)-1));

    BOOL errorDetected = FALSE;

    if (mpDlgAccessor != NULL)
    {
        // Do we have the help manager available?
        // Is this a bugfixed GBM file dialog that has a public dialog window procedure?
        if (mHwndHelp && mpDlgAccessor->hasPublicGbmDefFileDlgProc())
        {
            // add help button and make the dialog modeless so that we can subclass it
            gbmfild.fild.fl |= FDS_HELPBUTTON;

            // Put the class instance in the ulUser field to access it from the
            // global subclass window procedure.
            gbmfild.fild.ulUser = (ULONG) this;

            // Set our local window procedure. It will forward messages to the GBM file dialog.
            gbmfild.fild.pfnDlgProc = (PFNWP) GlobalGbmDlgWindowProcedure;
        }

        // create the GBM file dialog
        const HWND hwndDlg = mpDlgAccessor->GbmFileDlg(HWND_DESKTOP, hwnd, &gbmfild);
        if (hwndDlg)
        {
           return processGbmFileDlg( hwndDlg , &gbmfild,
                                     filename,  filenameSize,
                                     options ,  optionsSize);
        }
        else
        {
           // GBM file dialog not created
           errorDetected = TRUE;
        }
    }
    else
    {
        // no GbmDlg accessor available
        errorDetected = TRUE;
    }

    /* Fallback to standard OS/2 File Dialog */
    if (errorDetected)
    {
        // Display the dialog and get the file
        const HWND hwndDlg = WinFileDlg(HWND_DESKTOP, hwnd, &(gbmfild.fild));
        if (hwndDlg)
        {
            return processGbmFileDlg( hwndDlg , &gbmfild,
                                      filename,  filenameSize,
                                      options ,  optionsSize);
        }
        else
        {
            WinMessageBox(HWND_DESKTOP,
                          hwnd,
                          "File dialog not available.",
                          "GBM Bitmap Viewer plugin",
                          0, MB_OK | MB_ERROR | MB_MOVEABLE);
        }
    }
    return FALSE;
}

// ---------

BOOL GbmBitmapViewer::processGbmFileDlg(HWND   hwndDlg , GBMFILEDLG * gbmfild,
                                        CHAR * filename, int          filenameSize,
                                        CHAR * options , int          optionsSize) const
{
    if (gbmfild->fild.lReturn == DID_OK)
    {
        memset(filename, 0, filenameSize);
        memset(options , 0, optionsSize);

        strncpy(filename, gbmfild->fild.szFullFile,
                MIN(filenameSize-1, strlen(gbmfild->fild.szFullFile)));
        strncpy(options, gbmfild->szOptions,
                MIN(optionsSize-1, strlen(gbmfild->szOptions)));

        return TRUE;
    }
    return FALSE;
}


