* Tasedit: branches autofind best parent

* Tasedit: better hotchanges gradient
* Tasedit: "Set Marker"/"Remove marker" in context menu
* Tasedit: checking clipboard at selection.init() and retrieving info
This commit is contained in:
ansstuff 2011-12-03 20:53:31 +00:00
parent 1b728ca591
commit 06c4fff479
11 changed files with 325 additions and 96 deletions

View File

@ -275,8 +275,8 @@ BEGIN
MENUITEM SEPARATOR
MENUITEM "&Bind Markers to Input", ID_CONFIG_BINDMARKERSTOINPUT
MENUITEM SEPARATOR
MENUITEM "&Use 1P keys for all single Recordings", ID_CONFIG_USE1PFORRECORDING
MENUITEM "&Combine consecutive Recordings", ID_CONFIG_COMBINECONSECUTIVERECORDINGS
MENUITEM "&Use 1P keys for all single Recordings", ID_CONFIG_USE1PFORRECORDING
MENUITEM "&Superimpose affects copy/paste", ID_CONFIG_SUPERIMPOSE_AFFECTS_PASTE
MENUITEM SEPARATOR
MENUITEM "Mute &Turbo", ID_CONFIG_MUTETURBO
@ -368,6 +368,8 @@ BEGIN
END
POPUP "Selected"
BEGIN
MENUITEM "Set Marker", ID_SELECTED_SETMARKER
MENUITEM "Remove Marker", ID_SELECTED_REMOVEMARKER
MENUITEM "Select mid &Markers", ID_SELECTED_SELECTMIDMARKERS
MENUITEM SEPARATOR
MENUITEM "C&lear", ID_CONTEXT_SELECTED_CLEARFRAMES

View File

@ -913,6 +913,9 @@
#define ID_EDIT_PASTEINSERT 40495
#define ID_CONFIG_SUPERIMPOSEAFFECTSCOPY 40496
#define ID_CONFIG_SUPERIMPOSE_AFFECTS_PASTE 40497
#define ID_SELECTED_SETMARKER 40498
#define ID_SELECTED_CLEARMARKER 40499
#define ID_SELECTED_REMOVEMARKER40500 40500
#define IDC_DEBUGGER_ICONTRAY 55535
#define MW_ValueLabel2 65423
#define MW_ValueLabel1 65426
@ -922,7 +925,7 @@
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 185
#define _APS_NEXT_COMMAND_VALUE 40498
#define _APS_NEXT_COMMAND_VALUE 40501
#define _APS_NEXT_CONTROL_VALUE 1269
#define _APS_NEXT_SYMED_VALUE 101
#endif

View File

@ -108,39 +108,53 @@ void RedrawTasedit()
InvalidateRect(hwndTasEdit, 0, FALSE);
}
void ShowMenu(ECONTEXTMENU which, POINT& pt)
{
HMENU sub = GetSubMenu(hrmenu,(int)which);
TrackPopupMenu(sub,0,pt.x,pt.y,TPM_RIGHTBUTTON,hwndTasEdit,0);
}
void StrayClickMenu(LPNMITEMACTIVATE info)
{
POINT pt = info->ptAction;
ClientToScreen(tasedit_list.hwndList, &pt);
ShowMenu(CONTEXTMENU_STRAY, pt);
HMENU sub = GetSubMenu(hrmenu, CONTEXTMENU_STRAY);
TrackPopupMenu(sub, 0, pt.x, pt.y, 0, hwndTasEdit, 0);
}
void RightClickMenu(LPNMITEMACTIVATE info)
{
POINT pt = info->ptAction;
ClientToScreen(tasedit_list.hwndList, &pt);
ShowMenu(CONTEXTMENU_SELECTED, pt);
}
void RightClick(LPNMITEMACTIVATE info)
{
int index = info->iItem;
int column = info->iSubItem;
//stray clicks give a context menu:
if(index == -1)
SelectionFrames* current_selection = selection.MakeStrobe();
if (current_selection->size() == 0)
{
StrayClickMenu(info);
return;
}
HMENU sub = GetSubMenu(hrmenu, CONTEXTMENU_SELECTED);
// inspect current selection and disable inappropriate menu items
SelectionFrames::iterator current_selection_begin(current_selection->begin());
SelectionFrames::iterator current_selection_end(current_selection->end());
bool set_found = false, unset_found = false;
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
{
if(markers.GetMarker(*it))
set_found = true;
else
unset_found = true;
}
if (set_found)
EnableMenuItem(sub, ID_SELECTED_REMOVEMARKER, MF_BYCOMMAND | MF_ENABLED);
else
EnableMenuItem(sub, ID_SELECTED_REMOVEMARKER, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
if (unset_found)
EnableMenuItem(sub, ID_SELECTED_SETMARKER, MF_BYCOMMAND | MF_ENABLED);
else
EnableMenuItem(sub, ID_SELECTED_SETMARKER, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
if (selection.CheckFrameSelected(index))
TrackPopupMenu(sub, 0, pt.x, pt.y, 0, hwndTasEdit, 0);
}
void RightClick(LPNMITEMACTIVATE info)
{
int index = info->iItem;
if(index == -1)
StrayClickMenu(info);
else if (selection.CheckFrameSelected(index))
RightClickMenu(info);
}
@ -382,19 +396,22 @@ void Truncate()
}
}
//the column set operation, for setting a button/Marker for a span of selected values
void ColumnSet(int column)
{
if (column == COLUMN_FRAMENUM || column == COLUMN_FRAMENUM2)
FrameColumnSet();
else
InputColumnSet(column);
}
void FrameColumnSet()
{
SelectionFrames* current_selection = selection.MakeStrobe();
if (current_selection->size() == 0) return;
SelectionFrames::iterator current_selection_begin(current_selection->begin());
SelectionFrames::iterator current_selection_end(current_selection->end());
if (column == COLUMN_FRAMENUM || column == COLUMN_FRAMENUM2)
{
// Markers column
// inspect the selected frames, if they are all set, then unset all, else set all
bool unset_found = false;
bool unset_found = false, changes_made = false;
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
{
if(!markers.GetMarker(*it))
@ -405,31 +422,48 @@ void ColumnSet(int column)
}
if (unset_found)
{
// set all
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
{
if(!markers.GetMarker(*it))
{
changes_made = true;
markers.SetMarker(*it);
tasedit_list.RedrawRow(*it);
}
}
if (changes_made)
history.RegisterChanges(MODTYPE_MARKER_SET, *current_selection_begin, *current_selection->rbegin());
} else
{
// unset all
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
{
if(markers.GetMarker(*it))
{
changes_made = true;
markers.ClearMarker(*it);
tasedit_list.RedrawRow(*it);
}
}
if (changes_made)
history.RegisterChanges(MODTYPE_MARKER_UNSET, *current_selection_begin, *current_selection->rbegin());
}
if (changes_made)
project.SetProjectChanged();
// no need to RedrawList();
} else
{
// buttons column
}
void InputColumnSet(int column)
{
int joy = (column - COLUMN_JOYPAD1_A) / NUM_JOYPAD_BUTTONS;
if (joy < 0 || joy >= NUM_JOYPADS) return;
int button = (column - COLUMN_JOYPAD1_A) % NUM_JOYPAD_BUTTONS;
SelectionFrames* current_selection = selection.MakeStrobe();
if (current_selection->size() == 0) return;
SelectionFrames::iterator current_selection_begin(current_selection->begin());
SelectionFrames::iterator current_selection_end(current_selection->end());
//inspect the selected frames, if they are all set, then unset all, else set all
bool newValue = false;
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
@ -450,7 +484,6 @@ void ColumnSet(int column)
{
greenzone.InvalidateAndCheck(history.RegisterChanges(MODTYPE_UNSET, *current_selection_begin, *current_selection->rbegin()));
}
}
}
@ -551,7 +584,7 @@ bool Paste()
{
char *pGlobal = (char*)GlobalLock((HGLOBAL)hGlobal);
// TAS recording info starts with "TAS ".
// TAS recording info starts with "TAS "
if (pGlobal[0]=='T' && pGlobal[1]=='A' && pGlobal[2]=='S')
{
// Extract number of frames
@ -567,12 +600,11 @@ bool Paste()
int joy = 0;
uint8 new_buttons = 0;
char* frame;
--pos;
while (pGlobal++ && *pGlobal!='\0')
{
// Detect skipped frames in paste
frame = pGlobal;
// Detect skipped frames in paste.
if (frame[0]=='+')
{
pos += atoi(frame+1);
@ -653,7 +685,7 @@ bool PasteInsert()
{
char *pGlobal = (char*)GlobalLock((HGLOBAL)hGlobal);
// TAS recording info starts with "TAS ".
// TAS recording info starts with "TAS "
if (pGlobal[0]=='T' && pGlobal[1]=='A' && pGlobal[2]=='S')
{
// make sure markers have the same size as movie
@ -667,14 +699,13 @@ bool PasteInsert()
pGlobal = strchr(pGlobal, '\n');
char* frame;
int joy=0;
--pos;
while (pGlobal++ && *pGlobal!='\0')
{
char *frame = pGlobal;
// Detect skipped frames in paste.
// Detect skipped frames in paste
frame = pGlobal;
if (frame[0]=='+')
{
pos += atoi(frame+1);
@ -1322,6 +1353,57 @@ BOOL CALLBACK WndprocTasEdit(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lPar
tasedit_list.FollowSelection();
break;
}
case ID_SELECTED_SETMARKER:
{
SelectionFrames* current_selection = selection.MakeStrobe();
if (current_selection->size())
{
SelectionFrames::iterator current_selection_begin(current_selection->begin());
SelectionFrames::iterator current_selection_end(current_selection->end());
bool changes_made = false;
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
{
if(!markers.GetMarker(*it))
{
changes_made = true;
markers.SetMarker(*it);
tasedit_list.RedrawRow(*it);
}
}
if (changes_made)
{
history.RegisterChanges(MODTYPE_MARKER_SET, *current_selection_begin, *current_selection->rbegin());
project.SetProjectChanged();
}
}
break;
}
case ID_SELECTED_REMOVEMARKER:
{
SelectionFrames* current_selection = selection.MakeStrobe();
if (current_selection->size())
{
SelectionFrames::iterator current_selection_begin(current_selection->begin());
SelectionFrames::iterator current_selection_end(current_selection->end());
bool changes_made = false;
for(SelectionFrames::iterator it(current_selection_begin); it != current_selection_end; it++)
{
if(markers.GetMarker(*it))
{
changes_made = true;
markers.ClearMarker(*it);
tasedit_list.RedrawRow(*it);
}
}
if (changes_made)
{
history.RegisterChanges(MODTYPE_MARKER_UNSET, *current_selection_begin, *current_selection->rbegin());
project.SetProjectChanged();
}
}
break;
}
}
break;
@ -1382,17 +1464,9 @@ void EnterTasEdit()
SetWindowPos(hwndTasEdit, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE|SWP_NOOWNERZORDER);
// init modules
FCEU_printf("1");
greenzone.init();
FCEU_printf("2");
playback.init();
// either start new movie or use current movie
if (currMovieData.savestate.size() != 0)
{
FCEUD_PrintError("This version of TAS Editor doesn't work with movies starting from savestate.");
// delete savestate, but preserve input
currMovieData.savestate.clear();
}
if (FCEUMOV_Mode(MOVIEMODE_INACTIVE))
{
FCEUI_StopMovie();
@ -1401,6 +1475,12 @@ void EnterTasEdit()
} else
{
// use current movie to create a new project
if (currMovieData.savestate.size() != 0)
{
FCEUD_PrintError("This version of TAS Editor doesn't work with movies starting from savestate.");
// delete savestate, but preserve input
currMovieData.savestate.clear();
}
FCEUI_StopMovie();
greenzone.TryDumpIncremental(lagFlag != 0);
}

View File

@ -36,6 +36,8 @@ void DeleteFrames();
void ClearFrames(SelectionFrames* current_selection = 0);
void Truncate();
void ColumnSet(int column);
void InputColumnSet(int column);
void FrameColumnSet();
bool Copy(SelectionFrames* current_selection = 0);
void Cut();
bool Paste();

View File

@ -272,17 +272,18 @@ void BOOKMARKS::set(int slot)
bookmarks_array[slot].set();
// if this screenshot is shown on screen - reinit and redraw it
// if this screenshot is currently shown - reinit and redraw it
if (screenshot_display.screenshot_currently_shown == slot)
screenshot_display.screenshot_currently_shown = ITEM_UNDER_MOUSE_NONE;
int parent;
// inherit current branch
if (slot != current_branch)
{
int parent = bookmarks_array[slot].parent_branch;
parent = bookmarks_array[slot].parent_branch;
if (parent == -1 && saved_time[0])
{
// check if this is the only child of cloud parent, if so then set cloud time to the saved_time
// check if this was the only child of cloud parent, if so then set cloud time to the saved_time
int i = 0;
for (; i < TOTAL_BOOKMARKS; ++i)
{
@ -293,7 +294,7 @@ void BOOKMARKS::set(int slot)
// didn't find another child of cloud
strcpy(cloud_time, saved_time);
}
// before disconnecting from old parent, connect all childs to the parent
// before disconnecting from old parent, connect all childs to the old parent
for (int i = 0; i < TOTAL_BOOKMARKS; ++i)
{
if (bookmarks_array[i].not_empty && bookmarks_array[i].parent_branch == slot)
@ -302,6 +303,69 @@ void BOOKMARKS::set(int slot)
bookmarks_array[slot].parent_branch = current_branch;
}
// if parent is invalid (first_change < parent.jump_frame) then find better parent
int factor;
// also if parent == cloud, then try to find better parent
parent = bookmarks_array[slot].parent_branch;
if (parent >= 0)
factor = bookmarks_array[slot].snapshot.findFirstChange(bookmarks_array[parent].snapshot);
if (parent < 0 || (factor >= 0 && factor < bookmarks_array[parent].snapshot.jump_frame))
{
// find highest frame of change
std::vector<int> DecisiveFactor(TOTAL_BOOKMARKS);
int best_branch = -1;
for (int i = TOTAL_BOOKMARKS-1; i >= 0; i--)
{
if (i != slot && i != parent && bookmarks_array[i].not_empty && bookmarks_array[slot].snapshot.size >= bookmarks_array[i].snapshot.jump_frame)
{
factor = bookmarks_array[slot].snapshot.findFirstChange(bookmarks_array[i].snapshot);
if (factor < 0)
{
// this branch is identical to this slot
DecisiveFactor[i] = 2 * bookmarks_array[i].snapshot.size;
} else if (factor >= bookmarks_array[i].snapshot.jump_frame)
{
// hey, this branch could be our new parent...
DecisiveFactor[i] = 2 * factor;
} else
DecisiveFactor[i] = 0;
} else
{
DecisiveFactor[i] = 0;
}
}
// add +1 as a bonus to current parents and grandparents (a bit of nepotism!)
while (parent >= 0)
{
if (DecisiveFactor[parent])
DecisiveFactor[parent]++;
parent = bookmarks_array[parent].parent_branch;
}
// find max
factor = 0;
for (int i = TOTAL_BOOKMARKS-1; i >= 0; i--)
{
if (DecisiveFactor[i] && DecisiveFactor[i] > factor)
{
factor = DecisiveFactor[i];
best_branch = i;
}
}
parent = bookmarks_array[slot].parent_branch;
if (parent != best_branch)
{
// before disconnecting from old parent, connect all childs to the old parent
for (int i = 0; i < TOTAL_BOOKMARKS; ++i)
{
if (bookmarks_array[i].not_empty && bookmarks_array[i].parent_branch == slot)
bookmarks_array[i].parent_branch = parent;
}
// found new parent
bookmarks_array[slot].parent_branch = best_branch;
must_recalculate_branches_tree = true;
}
}
// switch current branch to this branch
if (slot != current_branch && current_branch >= 0)
{
@ -1041,7 +1105,7 @@ void BOOKMARKS::RecalculateBranchesTree()
// also define "current_pos" GridX
if (current_branch >= 0)
{
if (Children[current_branch+1].size() < MAX_NUM_CHILDREN)
if (Children[current_branch+1].size() < MAX_NUM_CHILDREN_ON_CANVAS_HEIGHT)
{
// "current_pos" becomes a child of current_branch
GridX[TOTAL_BOOKMARKS] = GridX[current_branch] + 1;
@ -1053,8 +1117,8 @@ void BOOKMARKS::RecalculateBranchesTree()
RecursiveAddHeight(current_branch, 1);
} else
{
// special case 0: if there's too many items on one level (more than canvas can show)
// "current_pos" becomes special branch above current_branch
// special case 0: if there's too many children on one level (more than canvas can show)
// then "current_pos" becomes special branch above current_branch
GridX[TOTAL_BOOKMARKS] = GridX[current_branch];
GridY[TOTAL_BOOKMARKS] = GridY[current_branch] - 7;
}
@ -1135,6 +1199,37 @@ void BOOKMARKS::RecalculateBranchesTree()
}
}
}
// special case 4: if cloud has all 10 children, then one child will be out of canvas
if (Children[0].size() == TOTAL_BOOKMARKS)
{
// find this child and move it to be visible
for (int t = TOTAL_BOOKMARKS - 1; t >= 0; t--)
{
if (GridY[t] > MAX_GRID_Y_POS)
{
GridY[t] = MAX_GRID_Y_POS;
GridX[t] -= 2;
// also move fireball to position near this branch
if (changes_since_current_branch && current_branch == t)
{
GridY[TOTAL_BOOKMARKS] = GridY[t];
GridX[TOTAL_BOOKMARKS] = GridX[t] + 1;
}
break;
} else if (GridY[t] < -MAX_GRID_Y_POS)
{
GridY[t] = -MAX_GRID_Y_POS;
GridX[t] -= 2;
// also move fireball to position near this branch
if (changes_since_current_branch && current_branch == t)
{
GridY[TOTAL_BOOKMARKS] = GridY[t];
GridX[TOTAL_BOOKMARKS] = GridX[t] + 1;
}
break;
}
}
}
// 3. Set pixel positions of branches
int max_x = 0;

View File

@ -35,7 +35,7 @@
#define EMPTY_BRANCHES_X -6
#define EMPTY_BRANCHES_Y_BASE 8
#define EMPTY_BRANCHES_Y_FACTOR 14
#define MAX_NUM_CHILDREN 9
#define MAX_NUM_CHILDREN_ON_CANVAS_HEIGHT 9
#define MAX_CHAIN_LEN 10
#define MAX_GRID_Y_POS 8
// spritesheet

View File

@ -62,12 +62,12 @@ void SCREENSHOT_DISPLAY::init()
}
HDC win_hdc = GetWindowDC(tasedit_list.hwndList);
scr_bmp = CreateDIBSection(win_hdc, scr_bmi, DIB_RGB_COLORS, (void**)&scr_ptr, 0, 0);
// calculate coordinates (relative to the listview top-left corner)
// calculate coordinates (relative to IDC_BOOKMARKS_BOX top-left corner)
RECT temp_rect, parent_rect;
GetWindowRect(hwndTasEdit, &parent_rect);
GetWindowRect(tasedit_list.hwndHeader, &temp_rect);
scr_bmp_x = temp_rect.left - parent_rect.left;
scr_bmp_y = temp_rect.bottom - parent_rect.top;
GetWindowRect(GetDlgItem(hwndTasEdit, IDC_BOOKMARKS_BOX), &temp_rect);
scr_bmp_x = temp_rect.left - SCREENSHOT_WIDTH - SCR_BMP_DX - parent_rect.left;
scr_bmp_y = ((temp_rect.bottom + temp_rect.top - SCREENSHOT_HEIGHT) / 2) - parent_rect.top;
}
void SCREENSHOT_DISPLAY::free()
{

View File

@ -1,7 +1,8 @@
//Specification file for SCREENSHOT_DISPLAY class
#define SCR_BMP_PHASE_MAX 13
#define SCR_BMP_PHASE_MAX 12
#define SCR_BMP_PHASE_ALPHA_MAX 10
#define SCR_BMP_DX 7
#define DISPLAY_UPDATE_TICK 40 // update at 25FPS

View File

@ -26,7 +26,9 @@ LRESULT APIENTRY ListWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
WNDPROC hwndList_oldWndProc, hwndHeader_oldWndproc;
// resources
COLORREF hot_changes_colors[16] = { 0x0, 0x661212, 0x842B4E, 0x652C73, 0x48247D, 0x383596, 0x2947AE, 0x1E53C1, 0x135DD2, 0x116EDA, 0x107EE3, 0x0F8EEB, 0x209FF4, 0x3DB1FD, 0x51C2FF, 0x4DCDFF };
COLORREF hot_changes_colors[16] = { 0x0, 0x5c4c44, 0x854604, 0xab2500, 0xc20006, 0xd6006f, 0xd40091, 0xba00a4, 0x9500ba, 0x7a00cc, 0x5800d4, 0x0045e2, 0x0063ea, 0x0079f4, 0x0092fa, 0x00aaff };
//COLORREF hot_changes_colors[16] = { 0x0, 0x661212, 0x842B4E, 0x652C73, 0x48247D, 0x383596, 0x2947AE, 0x1E53C1, 0x135DD2, 0x116EDA, 0x107EE3, 0x0F8EEB, 0x209FF4, 0x3DB1FD, 0x51C2FF, 0x4DCDFF };
char list_save_id[LIST_ID_LEN] = "LIST";
TASEDIT_LIST::TASEDIT_LIST()
@ -52,7 +54,6 @@ void TASEDIT_LIST::init()
{
free();
hwndList = GetDlgItem(hwndTasEdit, IDC_LIST1);
// prepare the main listview
ListView_SetExtendedListViewStyleEx(hwndList, LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES, LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES);
// subclass the header

View File

@ -43,6 +43,9 @@ void TASEDIT_SELECTION::init(int new_size)
AddNewSelectionToHistory();
track_selection_changes = true;
reset();
if (clipboard_selection.empty())
CheckClipboard();
RedrawTextClipboard();
}
void TASEDIT_SELECTION::free()
@ -51,7 +54,6 @@ void TASEDIT_SELECTION::free()
selections_history.resize(0);
history_total_items = 0;
temp_selection.clear();
clipboard_selection.clear();
}
void TASEDIT_SELECTION::reset()
{
@ -414,6 +416,48 @@ void TASEDIT_SELECTION::ReselectClipboard()
// also keep selection within list
update();
}
// retrieves some information from clipboard to clipboard_selection
void TASEDIT_SELECTION::CheckClipboard()
{
if (OpenClipboard(hwndTasEdit))
{
// check if clipboard contains TAS Editor input data
HANDLE hGlobal = GetClipboardData(CF_TEXT);
if (hGlobal)
{
clipboard_selection.clear();
int current_pos = -1;
char *pGlobal = (char*)GlobalLock((HGLOBAL)hGlobal);
// TAS recording info starts with "TAS "
if (pGlobal[0]=='T' && pGlobal[1]=='A' && pGlobal[2]=='S')
{
// Extract number of frames
int range;
sscanf (pGlobal+3, "%d", &range);
pGlobal = strchr(pGlobal, '\n');
while (pGlobal++ && *pGlobal!='\0')
{
// Detect skipped frames in paste
char *frame = pGlobal;
if (frame[0]=='+')
{
current_pos += atoi(frame+1);
while (*frame && *frame != '\n' && *frame != '|')
++frame;
if (*frame=='|') ++frame;
} else
current_pos++;
clipboard_selection.insert(current_pos);
// skip input
pGlobal = strchr(pGlobal, '\n');
}
}
GlobalUnlock(hGlobal);
}
CloseClipboard();
}
}
// ----------------------------------------------------------
void TASEDIT_SELECTION::ClearSelection()
{

View File

@ -56,6 +56,7 @@ public:
private:
SelectionFrames& CurrentSelection();
void CheckClipboard();
bool track_selection_changes;
bool must_redraw_text;