mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2024-12-28 09:58:30 +01:00
578 lines
19 KiB
C
578 lines
19 KiB
C
/* -*-mode: C; fill-column: 78; compile-command: "cd ../linux && make MEMDEBUG=TRUE"; -*- */
|
|
/*
|
|
* Copyright 1997 - 2009 by Eric House (xwords@eehouse.org). All rights
|
|
* reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
/* Re: boards that can't fit on the screen. Let's have an assumption, that
|
|
* the tray is always either below the board or overlapping its bottom. There
|
|
* is never any board visible below the tray. But it's possible to have a
|
|
* board small enough that scrolling is necessary even with the tray hidden.
|
|
*
|
|
* Currently we don't specify the board bounds. We give top,left and the size
|
|
* of cells, and the board figures out the bounds. That's probably a mistake.
|
|
* Better to give bounds, and maybe a min scale, and let it figure out how
|
|
* many cells can be visible. Could it also decide if the tray should overlap
|
|
* or be below? Some platforms have to own that decision since the tray is
|
|
* narrower than the board. So give them separate bounds-setting functions,
|
|
* and let the board code figure out if they overlap.
|
|
*
|
|
* Problem: the board size must always be a multiple of the scale. The
|
|
* platform-specific code has an easy time doing that math. The board can't:
|
|
* it'd have to take bounds, then spit them back out slightly modified. It'd
|
|
* also have to refuse to work (maybe just assert) if asked to take bounds
|
|
* before it had a min_scale.
|
|
*
|
|
* Another way of looking at it closer to the current: the board's position
|
|
* and the tray's bounds determine the board's bounds. If the board's vScale
|
|
* times the number of rows places its would-be bottom at or above the bottom
|
|
* of the tray, then it's potentially visible. If its would-be bottom is
|
|
* above the top of the tray, no scrolling is needed. But if it's below the
|
|
* tray entirely then scrolling will happen even with the tray hidden. As
|
|
* above, we assume the board never appears below the tray.
|
|
*/
|
|
|
|
#include "comtypes.h"
|
|
#include "board.h"
|
|
#include "scorebdp.h"
|
|
#include "game.h"
|
|
#include "server.h"
|
|
#include "comms.h" /* for CHANNEL_NONE */
|
|
#include "dictnry.h"
|
|
#include "draw.h"
|
|
#include "engine.h"
|
|
#include "util.h"
|
|
#include "mempool.h" /* debug only */
|
|
#include "memstream.h"
|
|
#include "strutils.h"
|
|
#include "LocalizedStrIncludes.h"
|
|
|
|
#include "boardp.h"
|
|
#include "dragdrpp.h"
|
|
#include "dbgutil.h"
|
|
|
|
#ifdef CPLUS
|
|
extern "C" {
|
|
#endif
|
|
|
|
static XP_Bool drawCell( BoardCtxt* board, XP_U16 col, XP_U16 row,
|
|
XP_Bool skipBlanks );
|
|
static void drawBoard( BoardCtxt* board );
|
|
static void scrollIfCan( BoardCtxt* board );
|
|
static XP_Bool cellFocused( const BoardCtxt* board, XP_U16 col, XP_U16 row );
|
|
static void drawTradeWindowIf( BoardCtxt* board );
|
|
|
|
|
|
#ifdef XWFEATURE_SEARCHLIMIT
|
|
static HintAtts figureHintAtts( BoardCtxt* board, XP_U16 col, XP_U16 row );
|
|
#else
|
|
# define figureHintAtts(b,c,r) HINT_BORDER_NONE
|
|
#endif
|
|
|
|
|
|
#ifdef POINTER_SUPPORT
|
|
static void drawDragTileIf( BoardCtxt* board );
|
|
#endif
|
|
|
|
#ifdef KEYBOARD_NAV
|
|
#ifdef PERIMETER_FOCUS
|
|
static void
|
|
invalOldPerimeter( BoardCtxt* board )
|
|
{
|
|
/* We need to inval the center of the row that's moving into the center
|
|
from a border (at which point it got borders drawn on it.) */
|
|
XP_S16 diff = board->yOffset - board->prevYScrollOffset;
|
|
XP_U16 firstRow, lastRow;
|
|
XP_ASSERT( diff != 0 );
|
|
if ( diff < 0 ) {
|
|
/* moving up; inval row previously on bottom */
|
|
firstRow = board->yOffset + 1;
|
|
lastRow = board->prevYScrollOffset;
|
|
} else {
|
|
XP_U16 nVisible = board->lastVisibleRow - board->yOffset + 1;
|
|
lastRow = board->prevYScrollOffset + nVisible - 1;
|
|
firstRow = lastRow - diff + 1;
|
|
}
|
|
XP_ASSERT( firstRow <= lastRow );
|
|
while ( firstRow <= lastRow ) {
|
|
board->redrawFlags[firstRow] |= ~0;
|
|
++firstRow;
|
|
}
|
|
} /* invalOldPerimeter */
|
|
#endif
|
|
#endif
|
|
|
|
/* if any of a blank's neighbors is invalid, so must the blank become (since
|
|
* they share a border and drawing the neighbor will redraw the blank's border
|
|
* too) We'll want to redraw only those blanks that are themselves already
|
|
* invalid *OR* that become invalid this way, and so we'll build a new
|
|
* BlankQueue of them and replace the old.
|
|
*
|
|
* I'm not sure what happens if two blanks are neighbors.
|
|
*/
|
|
#define INVAL_BIT_SET(b,c,r) (((b)->redrawFlags[(r)] & (1 <<(c))) != 0)
|
|
static void
|
|
invalBlanksWithNeighbors( BoardCtxt* board, BlankQueue* bqp )
|
|
{
|
|
XP_U16 i;
|
|
XP_U16 lastCol, lastRow;
|
|
BlankQueue invalBlanks;
|
|
XP_U16 nInvalBlanks = 0;
|
|
|
|
lastCol = model_numCols(board->model) - 1;
|
|
lastRow = model_numRows(board->model) - 1;
|
|
|
|
for ( i = 0; i < bqp->nBlanks; ++i ) {
|
|
XP_U16 modelCol = bqp->col[i];
|
|
XP_U16 modelRow = bqp->row[i];
|
|
XP_U16 col, row;
|
|
|
|
flipIf( board, modelCol, modelRow, &col, &row );
|
|
|
|
if ( INVAL_BIT_SET( board, col, row )
|
|
|| (col > 0 && INVAL_BIT_SET( board, col-1, row ))
|
|
|| (col < lastCol && INVAL_BIT_SET( board, col+1, row ))
|
|
|| (row > 0 && INVAL_BIT_SET( board, col, row-1 ))
|
|
|| (row < lastRow && INVAL_BIT_SET( board, col, row+1 )) ) {
|
|
|
|
invalCell( board, col, row );
|
|
|
|
invalBlanks.col[nInvalBlanks] = (XP_U8)col;
|
|
invalBlanks.row[nInvalBlanks] = (XP_U8)row;
|
|
++nInvalBlanks;
|
|
}
|
|
}
|
|
invalBlanks.nBlanks = nInvalBlanks;
|
|
XP_MEMCPY( bqp, &invalBlanks, sizeof(*bqp) );
|
|
} /* invalBlanksWithNeighbors */
|
|
|
|
|
|
#ifdef XWFEATURE_SEARCHLIMIT
|
|
static HintAtts
|
|
figureHintAtts( BoardCtxt* board, XP_U16 col, XP_U16 row )
|
|
{
|
|
HintAtts result = HINT_BORDER_NONE;
|
|
|
|
/* while lets us break to exit... */
|
|
while ( board->trayVisState == TRAY_REVEALED
|
|
&& !board->gi->hintsNotAllowed
|
|
&& board->gi->allowHintRect ) {
|
|
BdHintLimits limits;
|
|
if ( dragDropGetHintLimits( board, &limits ) ) {
|
|
/* do nothing */
|
|
} else if ( board->selInfo->hasHintRect ) {
|
|
limits = board->selInfo->limits;
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
if ( col < limits.left ) break;
|
|
if ( row < limits.top ) break;
|
|
if ( col > limits.right ) break;
|
|
if ( row > limits.bottom ) break;
|
|
|
|
if ( col == limits.left ) {
|
|
result |= HINT_BORDER_LEFT;
|
|
}
|
|
if ( col == limits.right ) {
|
|
result |= HINT_BORDER_RIGHT;
|
|
}
|
|
if ( row == limits.top) {
|
|
result |= HINT_BORDER_TOP;
|
|
}
|
|
if ( row == limits.bottom ) {
|
|
result |= HINT_BORDER_BOTTOM;
|
|
}
|
|
#ifndef XWFEATURE_SEARCHLIMIT_DOCENTERS
|
|
if ( result == HINT_BORDER_NONE ) {
|
|
result = HINT_BORDER_CENTER;
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
} /* figureHintAtts */
|
|
#endif
|
|
|
|
static XP_Bool
|
|
rectContainsRect( XP_Rect* rect1, XP_Rect* rect2 )
|
|
{
|
|
return ( rect1->top <= rect2->top
|
|
&& rect1->left <= rect2->left
|
|
&& rect1->top + rect1->height >= rect2->top + rect2->height
|
|
&& rect1->left + rect1->width >= rect2->left + rect2->width );
|
|
} /* rectContainsRect */
|
|
|
|
static void
|
|
makeMiniWindowForTrade( BoardCtxt* board )
|
|
{
|
|
const XP_UCHAR* text;
|
|
|
|
text = draw_getMiniWText( board->draw, INTRADE_MW_TEXT );
|
|
|
|
makeMiniWindowForText( board, text, MINIWINDOW_TRADING );
|
|
} /* makeMiniWindowForTrade */
|
|
|
|
static void
|
|
drawBoard( BoardCtxt* board )
|
|
{
|
|
if ( board->needsDrawing
|
|
&& draw_boardBegin( board->draw,
|
|
&board->boardBounds,
|
|
dfsFor( board, OBJ_BOARD ) ) ) {
|
|
|
|
XP_Bool allDrawn = XP_TRUE;
|
|
XP_S16 lastCol, i;
|
|
XP_S16 row;
|
|
ModelCtxt* model = board->model;
|
|
BoardArrow const* arrow = NULL;
|
|
BlankQueue bq;
|
|
|
|
scrollIfCan( board ); /* this must happen before we count blanks
|
|
since it invalidates squares */
|
|
|
|
/* This is freaking expensive!!!! PENDING FIXME Can't we start from
|
|
what's invalid rather than scanning the entire model every time
|
|
somebody dirties a single cell? */
|
|
model_listPlacedBlanks( model, board->selPlayer,
|
|
board->trayVisState == TRAY_REVEALED, &bq );
|
|
invalBlanksWithNeighbors( board, &bq );
|
|
|
|
/* figure out now, before clearing inval bits, if we'll need to draw
|
|
the arrow later */
|
|
if ( board->trayVisState == TRAY_REVEALED ) {
|
|
BoardArrow const* tmpArrow = &board->selInfo->boardArrow;
|
|
if ( tmpArrow->visible ) {
|
|
XP_U16 col = tmpArrow->col;
|
|
XP_U16 row = tmpArrow->row;
|
|
if ( INVAL_BIT_SET( board, col, row )
|
|
&& !cellOccupied( board, col, row, XP_TRUE ) ) {
|
|
arrow = tmpArrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
for ( row = board->yOffset; row <= board->lastVisibleRow; ++row ) {
|
|
XP_U16 rowFlags = board->redrawFlags[row];
|
|
if ( rowFlags != 0 ) {
|
|
XP_U16 colMask;
|
|
XP_U16 failedBits = 0;
|
|
lastCol = model_numCols( model );
|
|
for ( colMask = 1<<(lastCol-1); lastCol--; colMask >>= 1 ) {
|
|
if ( (rowFlags & colMask) != 0 ) {
|
|
if ( !drawCell( board, lastCol, row, XP_TRUE )) {
|
|
failedBits |= colMask;
|
|
allDrawn = XP_FALSE;
|
|
}
|
|
}
|
|
}
|
|
board->redrawFlags[row] = failedBits;
|
|
}
|
|
}
|
|
|
|
/* draw the blanks we skipped before */
|
|
for ( i = 0; i < bq.nBlanks; ++i ) {
|
|
if ( !drawCell( board, bq.col[i], bq.row[i], XP_FALSE ) ) {
|
|
allDrawn = XP_FALSE;
|
|
}
|
|
}
|
|
|
|
if ( !!arrow ) {
|
|
XP_U16 col = arrow->col;
|
|
XP_U16 row = arrow->row;
|
|
XP_Rect arrowRect;
|
|
if ( getCellRect( board, col, row, &arrowRect ) ) {
|
|
XWBonusType bonus;
|
|
HintAtts hintAtts;
|
|
CellFlags flags = CELL_NONE;
|
|
bonus = util_getSquareBonus( board->util, model, col, row );
|
|
hintAtts = figureHintAtts( board, col, row );
|
|
#ifdef KEYBOARD_NAV
|
|
if ( cellFocused( board, col, row ) ) {
|
|
flags |= CELL_ISCURSOR;
|
|
}
|
|
#endif
|
|
draw_drawBoardArrow( board->draw, &arrowRect, bonus,
|
|
arrow->vert, hintAtts, flags );
|
|
}
|
|
}
|
|
|
|
/* I doubt the two of these can happen at the same time */
|
|
drawTradeWindowIf( board );
|
|
#ifdef POINTER_SUPPORT
|
|
drawDragTileIf( board );
|
|
#endif
|
|
draw_objFinished( board->draw, OBJ_BOARD, &board->boardBounds,
|
|
dfsFor( board, OBJ_BOARD ) );
|
|
|
|
board->needsDrawing = !allDrawn;
|
|
}
|
|
} /* drawBoard */
|
|
|
|
|
|
static XP_Bool
|
|
drawCell( BoardCtxt* board, XP_U16 col, XP_U16 row, XP_Bool skipBlanks )
|
|
{
|
|
XP_Bool success = XP_TRUE;
|
|
XP_Rect cellRect;
|
|
Tile tile;
|
|
XP_Bool isBlank, isEmpty, recent, pending = XP_FALSE;
|
|
XWBonusType bonus;
|
|
ModelCtxt* model = board->model;
|
|
DictionaryCtxt* dict = model_getDictionary( model );
|
|
XP_U16 modelCol, modelRow;
|
|
|
|
if ( dict != NULL && getCellRect( board, col, row, &cellRect ) ) {
|
|
|
|
/* We want to invert EITHER the current pending tiles OR the most recent
|
|
* move. So if the tray is visible AND there are tiles missing from it,
|
|
* show them. Otherwise show the most recent move.
|
|
*/
|
|
XP_U16 selPlayer = board->selPlayer;
|
|
XP_U16 curCount = model_getCurrentMoveCount( model, selPlayer );
|
|
XP_Bool showPending = board->trayVisState == TRAY_REVEALED
|
|
&& curCount > 0;
|
|
|
|
flipIf( board, col, row, &modelCol, &modelRow );
|
|
|
|
/* This 'while' is only here so I can 'break' below */
|
|
while ( board->trayVisState == TRAY_HIDDEN ||
|
|
!rectContainsRect( &board->trayBounds, &cellRect ) ) {
|
|
XP_UCHAR ch[4] = {'\0'};
|
|
XP_S16 owner = -1;
|
|
XP_Bool invert = XP_FALSE;
|
|
XP_Bitmaps bitmaps;
|
|
XP_Bitmaps* bptr = NULL;
|
|
const XP_UCHAR* textP = NULL;
|
|
HintAtts hintAtts;
|
|
CellFlags flags = CELL_NONE;
|
|
XP_Bool isOrigin;
|
|
|
|
isEmpty = !model_getTile( model, modelCol, modelRow, showPending,
|
|
selPlayer, &tile, &isBlank,
|
|
&pending, &recent );
|
|
if ( dragDropIsBeingDragged( board, col, row, &isOrigin ) ) {
|
|
flags |= isOrigin? CELL_DRAGSRC : CELL_DRAGCUR;
|
|
if ( isEmpty && !isOrigin ) {
|
|
dragDropTileInfo( board, &tile, &isBlank );
|
|
pending = XP_TRUE;
|
|
recent = XP_FALSE;
|
|
isEmpty = XP_FALSE;
|
|
}
|
|
}
|
|
|
|
if ( isEmpty ) {
|
|
isBlank = XP_FALSE;
|
|
flags |= CELL_ISEMPTY;
|
|
} else if ( isBlank && skipBlanks ) {
|
|
break;
|
|
} else {
|
|
if ( board->showColors ) {
|
|
owner = (XP_S16)model_getCellOwner( model, modelCol,
|
|
modelRow );
|
|
}
|
|
|
|
invert = showPending? pending : recent;
|
|
|
|
if ( board->showCellValues ) {
|
|
Tile valTile = isBlank? dict_getBlankTile( dict ) : tile;
|
|
XP_U16 val = dict_getTileValue( dict, valTile );
|
|
XP_SNPRINTF( ch, VSIZE(ch), "%d", val );
|
|
textP = ch;
|
|
} else {
|
|
if ( dict_faceIsBitmap( dict, tile ) ) {
|
|
dict_getFaceBitmaps( dict, tile, &bitmaps );
|
|
bptr = &bitmaps;
|
|
}
|
|
textP = dict_getTileString( dict, tile );
|
|
}
|
|
}
|
|
bonus = util_getSquareBonus( board->util, model, col, row );
|
|
hintAtts = figureHintAtts( board, col, row );
|
|
|
|
if ( (col==board->star_row) && (row==board->star_row) ) {
|
|
flags |= CELL_ISSTAR;
|
|
}
|
|
if ( invert ) {
|
|
flags |= CELL_HIGHLIGHT;
|
|
}
|
|
if ( isBlank ) {
|
|
flags |= CELL_ISBLANK;
|
|
}
|
|
#ifdef KEYBOARD_NAV
|
|
if ( cellFocused( board, col, row ) ) {
|
|
flags |= CELL_ISCURSOR;
|
|
}
|
|
#endif
|
|
|
|
success = draw_drawCell( board->draw, &cellRect, textP, bptr,
|
|
tile, owner, bonus, hintAtts, flags );
|
|
break;
|
|
}
|
|
}
|
|
return success;
|
|
} /* drawCell */
|
|
|
|
#ifdef KEYBOARD_NAV
|
|
DrawFocusState
|
|
dfsFor( BoardCtxt* board, BoardObjectType obj )
|
|
{
|
|
DrawFocusState dfs;
|
|
if ( (board->focussed == obj) && !board->hideFocus ) {
|
|
if ( board->focusHasDived ) {
|
|
dfs = DFS_DIVED;
|
|
} else {
|
|
dfs = DFS_TOP;
|
|
}
|
|
} else {
|
|
dfs = DFS_NONE;
|
|
}
|
|
return dfs;
|
|
} /* dfsFor */
|
|
|
|
static XP_Bool
|
|
cellFocused( const BoardCtxt* board, XP_U16 col, XP_U16 row )
|
|
{
|
|
XP_Bool focussed = XP_FALSE;
|
|
|
|
if ( (board->focussed == OBJ_BOARD) && !board->hideFocus ) {
|
|
if ( board->focusHasDived ) {
|
|
if ( (col == board->selInfo->bdCursor.col)
|
|
&& (row == board->selInfo->bdCursor.row) ) {
|
|
focussed = XP_TRUE;
|
|
}
|
|
} else {
|
|
#ifdef PERIMETER_FOCUS
|
|
focussed = (col == 0)
|
|
|| (col == model_numCols(board->model) - 1)
|
|
|| (row == board->yOffset)
|
|
|| (row == board->lastVisibleRow);
|
|
#else
|
|
focussed = XP_TRUE;
|
|
#endif
|
|
}
|
|
}
|
|
return focussed;
|
|
} /* cellFocused */
|
|
#endif
|
|
|
|
#ifdef POINTER_SUPPORT
|
|
static void
|
|
drawDragTileIf( BoardCtxt* board )
|
|
{
|
|
if ( dragDropInProgress( board ) ) {
|
|
XP_U16 col, row;
|
|
if ( dragDropGetBoardTile( board, &col, &row ) ) {
|
|
XP_Rect rect;
|
|
Tile tile;
|
|
XP_Bool isBlank;
|
|
const XP_UCHAR* face;
|
|
XP_Bitmaps bitmaps;
|
|
XP_S16 value;
|
|
CellFlags flags;
|
|
|
|
getDragCellRect( board, col, row, &rect );
|
|
|
|
dragDropTileInfo( board, &tile, &isBlank );
|
|
|
|
face = getTileDrawInfo( board, tile, isBlank, &bitmaps,
|
|
&value );
|
|
|
|
flags = CELL_DRAGCUR;
|
|
if ( isBlank ) {
|
|
flags |= CELL_ISBLANK;
|
|
}
|
|
if ( board->hideValsInTray && !board->showCellValues ) {
|
|
flags |= CELL_VALHIDDEN;
|
|
}
|
|
draw_drawTileMidDrag( board->draw, &rect, face,
|
|
bitmaps.nBitmaps > 0 ? &bitmaps : NULL,
|
|
value, board->selPlayer, flags );
|
|
}
|
|
}
|
|
} /* drawDragTileIf */
|
|
#endif
|
|
|
|
static void
|
|
scrollIfCan( BoardCtxt* board )
|
|
{
|
|
if ( board->yOffset != board->prevYScrollOffset ) {
|
|
XP_Rect scrollR = board->boardBounds;
|
|
XP_Bool scrolled;
|
|
XP_S16 dist;
|
|
|
|
#ifdef PERIMETER_FOCUS
|
|
if ( (board->focussed == OBJ_BOARD)
|
|
&& !board->focusHasDived
|
|
&& !board->hideFocus ) {
|
|
invalOldPerimeter( board );
|
|
}
|
|
#endif
|
|
invalSelTradeWindow( board );
|
|
dist = (board->yOffset - board->prevYScrollOffset)
|
|
* board->boardVScale;
|
|
|
|
scrolled = draw_vertScrollBoard( board->draw, &scrollR, dist,
|
|
dfsFor( board, OBJ_BOARD ) );
|
|
|
|
if ( scrolled ) {
|
|
/* inval the rows that have been scrolled into view. I'm cheating
|
|
making the client figure the inval rect, but Palm's the first
|
|
client and it does it so well.... */
|
|
invalCellsUnderRect( board, &scrollR );
|
|
} else {
|
|
board_invalAll( board );
|
|
}
|
|
board->prevYScrollOffset = board->yOffset;
|
|
}
|
|
} /* scrollIfCan */
|
|
|
|
static void
|
|
drawTradeWindowIf( BoardCtxt* board )
|
|
{
|
|
if ( board->tradingMiniWindowInvalid &&
|
|
TRADE_IN_PROGRESS(board) && board->trayVisState == TRAY_REVEALED ) {
|
|
MiniWindowStuff* stuff;
|
|
|
|
makeMiniWindowForTrade( board );
|
|
|
|
stuff = &board->miniWindowStuff[MINIWINDOW_TRADING];
|
|
draw_drawMiniWindow( board->draw, stuff->text,
|
|
&stuff->rect, (void**)NULL );
|
|
|
|
board->tradingMiniWindowInvalid = XP_FALSE;
|
|
}
|
|
} /* drawTradeWindowIf */
|
|
|
|
XP_Bool
|
|
board_draw( BoardCtxt* board )
|
|
{
|
|
if ( board->boardBounds.width > 0 ) {
|
|
|
|
drawScoreBoard( board );
|
|
|
|
drawTray( board );
|
|
|
|
drawBoard( board );
|
|
}
|
|
return !board->needsDrawing;
|
|
} /* board_draw */
|
|
|
|
#ifdef CPLUS
|
|
}
|
|
#endif
|