X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/70d26c3f4ffb24d50457d405c9595fd23f9e5b7c..82dfea7723b354423e96221d4854878874cfce58:/demos/life/life.cpp diff --git a/demos/life/life.cpp b/demos/life/life.cpp index e14ab1b841..0043a399cc 100644 --- a/demos/life/life.cpp +++ b/demos/life/life.cpp @@ -1,6 +1,6 @@ ///////////////////////////////////////////////////////////////////////////// // Name: life.cpp -// Purpose: The game of life, created by J. H. Conway +// Purpose: The game of Life, created by J. H. Conway // Author: Guillermo Rodriguez Garcia, // Modified by: // Created: Jan/2000 @@ -10,285 +10,58 @@ ///////////////////////////////////////////////////////////////////////////// // ========================================================================== -// declarations +// headers, declarations, constants // ========================================================================== -// minimum and maximum table size, in each dimension -#define LIFE_MIN 20 -#define LIFE_MAX 200 - -// some shortcuts -#define ADD_TOOL(a, b, c, d) \ - toolBar->AddTool(a, b, wxNullBitmap, FALSE, -1, -1, (wxObject *)0, c, d) - -#define GET_FRAME() \ - ((LifeFrame *) wxGetApp().GetTopWindow()) - -// -------------------------------------------------------------------------- -// headers -// -------------------------------------------------------------------------- - #ifdef __GNUG__ - #pragma implementation "life.cpp" - #pragma interface "life.cpp" + #pragma implementation "life.h" #endif -// for compilers that support precompilation, includes "wx/wx.h". +// For compilers that support precompilation, includes "wx/wx.h". #include "wx/wxprec.h" #ifdef __BORLANDC__ #pragma hdrstop #endif -// for all others, include the necessary headers #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/statline.h" -#include "wx/spinctrl.h" +#include "wx/wfstream.h" +#include "wx/filedlg.h" + +#include "life.h" +#include "game.h" +#include "dialogs.h" +#include "reader.h" // -------------------------------------------------------------------------- // resources // -------------------------------------------------------------------------- -#if defined(__WXGTK__) || defined(__WXMOTIF__) - // the application icon +#if defined(__WXGTK__) || defined(__WXMOTIF__) || defined(__WXMAC__) + // application icon #include "mondrian.xpm" // bitmap buttons for the toolbar #include "bitmaps/reset.xpm" + #include "bitmaps/open.xpm" #include "bitmaps/play.xpm" #include "bitmaps/stop.xpm" + #include "bitmaps/zoomin.xpm" + #include "bitmaps/zoomout.xpm" + #include "bitmaps/info.xpm" + + // navigator + #include "bitmaps/north.xpm" + #include "bitmaps/south.xpm" + #include "bitmaps/east.xpm" + #include "bitmaps/west.xpm" + #include "bitmaps/center.xpm" #endif -// -------------------------------------------------------------------------- -// classes -// -------------------------------------------------------------------------- - -class Life; -class LifeShape; -class LifeCanvas; -class LifeTimer; -class LifeFrame; -class LifeApp; -class LifeNewGameDialog; -class LifeSamplesDialog; - -// -------------------------------------------------------------------------- -// non-GUI classes -// -------------------------------------------------------------------------- - -// Life -class Life -{ -public: - // ctors and dtors - Life(int width, int height); - ~Life(); - void Create(int width, int height); - void Destroy(); - - // accessors - inline int GetWidth() const { return m_width; }; - inline int GetHeight() const { return m_height; }; - inline bool IsAlive(int x, int y) const; - inline bool HasChanged(int x, int y) const; - - // flags - void SetBorderWrap(bool on); - - // game logic - void Clear(); - void SetCell(int x, int y, bool alive = TRUE); - void SetShape(LifeShape &shape); - bool NextTic(); - -private: - enum CellFlags { - CELL_DEAD = 0x0000, // is dead - CELL_ALIVE = 0x0001, // is alive - CELL_MARK = 0x0002, // will change / has changed - }; - typedef int Cell; - - int GetNeighbors(int x, int y) const; - inline void SetCell(int x, int y, Cell status); - - int m_width; - int m_height; - Cell *m_cells; - bool m_wrap; -}; - -// LifeShape -class LifeShape -{ -public: - LifeShape::LifeShape(wxString name, - wxString desc, - int width, int height, char *data, - int fieldWidth = 20, int fieldHeight = 20, - bool wrap = TRUE) - { - m_name = name; - m_desc = desc; - m_width = width; - m_height = height; - m_data = data; - m_fieldWidth = fieldWidth; - m_fieldHeight = fieldHeight; - m_wrap = wrap; - } - - wxString m_name; - wxString m_desc; - int m_width; - int m_height; - char *m_data; - int m_fieldWidth; - int m_fieldHeight; - bool m_wrap; -}; - -// -------------------------------------------------------------------------- -// GUI classes -// -------------------------------------------------------------------------- - -// Life canvas -class LifeCanvas : public wxScrolledWindow -{ -public: - // ctor and dtor - LifeCanvas(wxWindow* parent, Life* life, bool interactive = TRUE); - ~LifeCanvas(); - - // member functions - void Reset(); - void DrawEverything(bool force = FALSE); - void DrawCell(int i, int j); - void DrawCell(int i, int j, wxDC &dc); - inline int CellToCoord(int i) const { return (i * m_cellsize); }; - inline int CoordToCell(int x) const { return ((x >= 0)? (x / m_cellsize) : -1); }; - - // event handlers - void OnPaint(wxPaintEvent& event); - void OnMouse(wxMouseEvent& event); - void OnSize(wxSizeEvent& event); - -private: - // any class wishing to process wxWindows events must use this macro - DECLARE_EVENT_TABLE() - - enum MouseStatus { - MOUSE_NOACTION, - MOUSE_DRAWING, - MOUSE_ERASING - }; - - Life *m_life; - wxBitmap *m_bmp; - int m_height; - int m_width; - int m_cellsize; - wxCoord m_xoffset; - wxCoord m_yoffset; - MouseStatus m_status; - bool m_interactive; -}; - -// Life timer -class LifeTimer : public wxTimer -{ -public: - void Notify(); -}; - -// Life main frame -class LifeFrame : public wxFrame -{ -public: - // ctor and dtor - LifeFrame(); - ~LifeFrame(); - - // member functions - void UpdateInfoText(); - - // event handlers - void OnMenu(wxCommandEvent& event); - void OnNewGame(wxCommandEvent& event); - void OnSamples(wxCommandEvent& event); - void OnStart(); - void OnStop(); - void OnTimer(); - void OnSlider(wxScrollEvent& event); - -private: - // any class wishing to process wxWindows events must use this macro - DECLARE_EVENT_TABLE() - - Life *m_life; - LifeTimer *m_timer; - LifeCanvas *m_canvas; - wxStaticText *m_text; - bool m_running; - long m_interval; - long m_tics; -}; - -// Life new game dialog -class LifeNewGameDialog : public wxDialog -{ -public: - // ctor - LifeNewGameDialog(wxWindow *parent, int *w, int *h); - - // event handlers - void OnOK(wxCommandEvent& event); - -private: - // any class wishing to process wxWindows events must use this macro - DECLARE_EVENT_TABLE(); - - int *m_w; - int *m_h; - wxSpinCtrl *m_spinctrlw; - wxSpinCtrl *m_spinctrlh; -}; - -// Life sample configurations dialog -class LifeSamplesDialog : public wxDialog -{ -public: - // ctor and dtor - LifeSamplesDialog(wxWindow *parent); - ~LifeSamplesDialog(); - - // members - int GetValue(); - - // event handlers - void OnListBox(wxCommandEvent &event); - -private: - // any class wishing to process wxWindows events must use this macro - DECLARE_EVENT_TABLE(); - - int m_value; - wxListBox *m_list; - wxTextCtrl *m_text; - LifeCanvas *m_canvas; - Life *m_life; -}; - -// Life app -class LifeApp : public wxApp -{ -public: - virtual bool OnInit(); -}; - // -------------------------------------------------------------------------- // constants // -------------------------------------------------------------------------- @@ -296,73 +69,102 @@ public: // IDs for the controls and the menu commands enum { - // menu items and toolbar buttons - ID_NEWGAME = 1001, + // timer + ID_TIMER = 1001, + + // file menu + ID_NEW, + ID_OPEN, ID_SAMPLES, ID_ABOUT, ID_EXIT, - ID_CLEAR, + + // view menu + ID_SHOWNAV, + ID_ORIGIN, + ID_CENTER, + ID_NORTH, + ID_SOUTH, + ID_EAST, + ID_WEST, + ID_ZOOMIN, + ID_ZOOMOUT, + ID_INFO, + + // game menu ID_START, ID_STEP, ID_STOP, - ID_WRAP, + ID_TOPSPEED, // speed selection slider ID_SLIDER, - - // listbox in samples dialog - ID_LISTBOX }; - -// built-in sample games -#include "samples.inc" - // -------------------------------------------------------------------------- // event tables and other macros for wxWindows // -------------------------------------------------------------------------- // Event tables BEGIN_EVENT_TABLE(LifeFrame, wxFrame) - EVT_MENU (ID_NEWGAME, LifeFrame::OnNewGame) - EVT_MENU (ID_SAMPLES, LifeFrame::OnSamples) - EVT_MENU (ID_ABOUT, LifeFrame::OnMenu) - EVT_MENU (ID_EXIT, LifeFrame::OnMenu) - EVT_MENU (ID_CLEAR, LifeFrame::OnMenu) - EVT_MENU (ID_START, LifeFrame::OnMenu) - EVT_MENU (ID_STEP, LifeFrame::OnMenu) - EVT_MENU (ID_STOP, LifeFrame::OnMenu) - EVT_MENU (ID_WRAP, LifeFrame::OnMenu) - EVT_COMMAND_SCROLL (ID_SLIDER, LifeFrame::OnSlider) -END_EVENT_TABLE() - -BEGIN_EVENT_TABLE(LifeCanvas, wxScrolledWindow) - EVT_PAINT ( LifeCanvas::OnPaint) - EVT_SIZE ( LifeCanvas::OnSize) - EVT_MOUSE_EVENTS ( LifeCanvas::OnMouse) + EVT_MENU (ID_NEW, LifeFrame::OnMenu) + EVT_MENU (ID_OPEN, LifeFrame::OnOpen) + EVT_MENU (ID_SAMPLES, LifeFrame::OnSamples) + EVT_MENU (ID_ABOUT, LifeFrame::OnMenu) + EVT_MENU (ID_EXIT, LifeFrame::OnMenu) + EVT_MENU (ID_SHOWNAV, LifeFrame::OnMenu) + EVT_MENU (ID_ORIGIN, LifeFrame::OnNavigate) + EVT_BUTTON (ID_CENTER, LifeFrame::OnNavigate) + EVT_BUTTON (ID_NORTH, LifeFrame::OnNavigate) + EVT_BUTTON (ID_SOUTH, LifeFrame::OnNavigate) + EVT_BUTTON (ID_EAST, LifeFrame::OnNavigate) + EVT_BUTTON (ID_WEST, LifeFrame::OnNavigate) + EVT_MENU (ID_ZOOMIN, LifeFrame::OnZoom) + EVT_MENU (ID_ZOOMOUT, LifeFrame::OnZoom) + EVT_MENU (ID_INFO, LifeFrame::OnMenu) + EVT_MENU (ID_START, LifeFrame::OnMenu) + EVT_MENU (ID_STEP, LifeFrame::OnMenu) + EVT_MENU (ID_STOP, LifeFrame::OnMenu) + EVT_MENU (ID_TOPSPEED, LifeFrame::OnMenu) + EVT_COMMAND_SCROLL (ID_SLIDER, LifeFrame::OnSlider) + EVT_TIMER (ID_TIMER, LifeFrame::OnTimer) + EVT_CLOSE ( LifeFrame::OnClose) END_EVENT_TABLE() -BEGIN_EVENT_TABLE(LifeNewGameDialog, wxDialog) - EVT_BUTTON (wxID_OK, LifeNewGameDialog::OnOK) +BEGIN_EVENT_TABLE(LifeNavigator, wxMiniFrame) + EVT_CLOSE ( LifeNavigator::OnClose) END_EVENT_TABLE() -BEGIN_EVENT_TABLE(LifeSamplesDialog, wxDialog) - EVT_LISTBOX (ID_LISTBOX, LifeSamplesDialog::OnListBox) +BEGIN_EVENT_TABLE(LifeCanvas, wxWindow) + EVT_PAINT ( LifeCanvas::OnPaint) + EVT_SCROLLWIN ( LifeCanvas::OnScroll) + EVT_SIZE ( LifeCanvas::OnSize) + EVT_MOTION ( LifeCanvas::OnMouse) + EVT_LEFT_DOWN ( LifeCanvas::OnMouse) + EVT_LEFT_UP ( LifeCanvas::OnMouse) + EVT_LEFT_DCLICK ( LifeCanvas::OnMouse) + EVT_ERASE_BACKGROUND( LifeCanvas::OnEraseBackground) END_EVENT_TABLE() // Create a new application object IMPLEMENT_APP(LifeApp) + // ========================================================================== // implementation // ========================================================================== +// some shortcuts +#define ADD_TOOL(id, bmp, tooltip, help) \ + toolBar->AddTool(id, bmp, wxNullBitmap, FALSE, -1, -1, (wxObject *)0, tooltip, help) + + // -------------------------------------------------------------------------- // LifeApp // -------------------------------------------------------------------------- -// `Main program' equivalent: the program execution "starts" here +// 'Main program' equivalent: the program execution "starts" here bool LifeApp::OnInit() { // create the main application window @@ -372,6 +174,11 @@ bool LifeApp::OnInit() frame->Show(TRUE); SetTopWindow(frame); + // just for Motif +#ifdef __WXMOTIF__ + frame->UpdateInfoText(); +#endif + // enter the main message loop and run the app return TRUE; } @@ -381,178 +188,277 @@ bool LifeApp::OnInit() // -------------------------------------------------------------------------- // frame constructor -LifeFrame::LifeFrame() : wxFrame((wxFrame *)0, -1, _("Life!"), wxPoint(50, 50)) +LifeFrame::LifeFrame() : wxFrame((wxFrame *)0, -1, _("Life!"), wxPoint(200, 200)) { // frame icon SetIcon(wxICON(mondrian)); // menu bar wxMenu *menuFile = new wxMenu("", wxMENU_TEAROFF); + wxMenu *menuView = new wxMenu("", wxMENU_TEAROFF); wxMenu *menuGame = new wxMenu("", wxMENU_TEAROFF); - menuFile->Append(ID_NEWGAME, _("New game..."), _("Start a new game")); - menuFile->Append(ID_SAMPLES, _("Sample game..."), _("Select a sample configuration")); + menuFile->Append(ID_NEW, _("&New"), _("Start a new game")); + menuFile->Append(ID_OPEN, _("&Open..."), _("Open an existing Life pattern")); + menuFile->Append(ID_SAMPLES, _("&Sample game..."), _("Select a sample configuration")); menuFile->AppendSeparator(); menuFile->Append(ID_ABOUT, _("&About...\tCtrl-A"), _("Show about dialog")); menuFile->AppendSeparator(); menuFile->Append(ID_EXIT, _("E&xit\tAlt-X"), _("Quit this program")); - menuGame->Append(ID_CLEAR, _("&Clear\tCtrl-C"), _("Clear game field")); + menuView->Append(ID_SHOWNAV, _("Navigation &toolbox"), _("Show or hide toolbox"), TRUE); + menuView->Check (ID_SHOWNAV, TRUE); + menuView->AppendSeparator(); + menuView->Append(ID_ORIGIN, _("&Absolute origin"), _("Go to (0, 0)")); + menuView->Append(ID_CENTER, _("&Center of mass"), _("Find center of mass")); + menuView->Append(ID_NORTH, _("&North"), _("Find northernmost cell")); + menuView->Append(ID_SOUTH, _("&South"), _("Find southernmost cell")); + menuView->Append(ID_EAST, _("&East"), _("Find easternmost cell")); + menuView->Append(ID_WEST, _("&West"), _("Find westernmost cell")); + menuView->AppendSeparator(); + menuView->Append(ID_ZOOMIN, _("Zoom &in\tCtrl-I"), _("Zoom in")); + menuView->Append(ID_ZOOMOUT, _("Zoom &out\tCtrl-O"), _("Zoom out")); + menuView->Append(ID_INFO, _("&Description...\tCtrl-D"), _("View pattern description")); + menuGame->Append(ID_START, _("&Start\tCtrl-S"), _("Start")); menuGame->Append(ID_STEP, _("&Next\tCtrl-N"), _("Single step")); menuGame->Append(ID_STOP, _("S&top\tCtrl-T"), _("Stop")); menuGame->Enable(ID_STOP, FALSE); menuGame->AppendSeparator(); - menuGame->Append(ID_WRAP, _("&Wraparound\tCtrl-W"), _("Wrap around borders"), TRUE); - menuGame->Check (ID_WRAP, TRUE); - + menuGame->Append(ID_TOPSPEED, _("T&op speed!"), _("Go as fast as possible")); wxMenuBar *menuBar = new wxMenuBar(); menuBar->Append(menuFile, _("&File")); + menuBar->Append(menuView, _("&View")); menuBar->Append(menuGame, _("&Game")); SetMenuBar(menuBar); // tool bar - wxBitmap tbBitmaps[3]; + wxBitmap tbBitmaps[7]; tbBitmaps[0] = wxBITMAP(reset); - tbBitmaps[1] = wxBITMAP(play); - tbBitmaps[2] = wxBITMAP(stop); + tbBitmaps[1] = wxBITMAP(open); + tbBitmaps[2] = wxBITMAP(zoomin); + tbBitmaps[3] = wxBITMAP(zoomout); + tbBitmaps[4] = wxBITMAP(info); + tbBitmaps[5] = wxBITMAP(play); + tbBitmaps[6] = wxBITMAP(stop); wxToolBar *toolBar = CreateToolBar(); toolBar->SetMargins(5, 5); toolBar->SetToolBitmapSize(wxSize(16, 16)); - ADD_TOOL(ID_CLEAR, tbBitmaps[0], _("Clear"), _("Clear game board")); - ADD_TOOL(ID_START, tbBitmaps[1], _("Start"), _("Start")); - ADD_TOOL(ID_STOP , tbBitmaps[2], _("Stop"), _("Stop")); - toolBar->EnableTool(ID_STOP, FALSE); + + ADD_TOOL(ID_NEW, tbBitmaps[0], _("New"), _("Start a new game")); + ADD_TOOL(ID_OPEN, tbBitmaps[1], _("Open"), _("Open an existing Life pattern")); + toolBar->AddSeparator(); + ADD_TOOL(ID_ZOOMIN, tbBitmaps[2], _("Zoom in"), _("Zoom in")); + ADD_TOOL(ID_ZOOMOUT, tbBitmaps[3], _("Zoom out"), _("Zoom out")); + ADD_TOOL(ID_INFO, tbBitmaps[4], _("Description"), _("Show description")); + toolBar->AddSeparator(); + ADD_TOOL(ID_START, tbBitmaps[5], _("Start"), _("Start")); + ADD_TOOL(ID_STOP, tbBitmaps[6], _("Stop"), _("Stop")); + toolBar->Realize(); + toolBar->EnableTool(ID_STOP, FALSE); // must be after Realize() ! // status bar CreateStatusBar(2); SetStatusText(_("Welcome to Life!")); - // panel - wxPanel *panel = new wxPanel(this, -1); - - // game - m_life = new Life(20, 20); - m_canvas = new LifeCanvas(panel, m_life); - m_timer = new LifeTimer(); + // game and timer + m_life = new Life(); + m_timer = new wxTimer(this, ID_TIMER); + m_running = FALSE; + m_topspeed = FALSE; m_interval = 500; m_tics = 0; - m_text = new wxStaticText(panel, -1, ""); - UpdateInfoText(); - // slider - wxSlider *slider = new wxSlider(panel, ID_SLIDER, 5, 1, 10, - wxDefaultPosition, wxSize(200, -1), wxSL_HORIZONTAL | wxSL_AUTOTICKS); + // We use two different panels to reduce flicker in wxGTK, because + // some widgets (like wxStaticText) don't have their own X11 window, + // and thus updating the text would result in a refresh of the canvas + // if they belong to the same parent. + + wxPanel *panel1 = new wxPanel(this, -1); + wxPanel *panel2 = new wxPanel(this, -1); + + // canvas + m_canvas = new LifeCanvas(panel1, m_life); + + // info panel + m_text = new wxStaticText(panel2, -1, + wxEmptyString, + wxDefaultPosition, + wxDefaultSize, + wxALIGN_CENTER | wxST_NO_AUTORESIZE); + + wxSlider *slider = new wxSlider(panel2, ID_SLIDER, + 5, 1, 10, + wxDefaultPosition, + wxSize(200, -1), + wxSL_HORIZONTAL | wxSL_AUTOTICKS); + + UpdateInfoText(); // component layout - wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(new wxStaticLine(panel, -1), 0, wxGROW | wxCENTRE); - sizer->Add(m_canvas, 1, wxGROW | wxCENTRE | wxALL, 5); - sizer->Add(new wxStaticLine(panel, -1), 0, wxGROW | wxCENTRE); - sizer->Add(m_text, 0, wxCENTRE | wxTOP, 5); - sizer->Add(slider, 0, wxCENTRE | wxALL, 5); - panel->SetSizer(sizer); - panel->SetAutoLayout(TRUE); - sizer->Fit(this); - sizer->SetSizeHints(this); + wxBoxSizer *sizer1 = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *sizer2 = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *sizer3 = new wxBoxSizer(wxVERTICAL); + + sizer1->Add( new wxStaticLine(panel1, -1), 0, wxGROW ); + sizer1->Add( m_canvas, 1, wxGROW | wxALL, 2 ); + sizer1->Add( new wxStaticLine(panel1, -1), 0, wxGROW ); + panel1->SetSizer( sizer1 ); + panel1->SetAutoLayout( TRUE ); + sizer1->Fit( panel1 ); + + sizer2->Add( m_text, 0, wxGROW | wxTOP, 4 ); + sizer2->Add( slider, 0, wxCENTRE | wxALL, 4 ); + + panel2->SetSizer( sizer2 ); + panel2->SetAutoLayout( TRUE ); + sizer2->Fit( panel2 ); + + sizer3->Add( panel1, 1, wxGROW ); + sizer3->Add( panel2, 0, wxGROW ); + SetSizer( sizer3 ); + SetAutoLayout( TRUE ); + sizer3->Fit( this ); + + // set minimum frame size + sizer3->SetSizeHints( this ); + + // navigator frame + m_navigator = new LifeNavigator(this); } LifeFrame::~LifeFrame() { delete m_timer; - delete m_life; } void LifeFrame::UpdateInfoText() { wxString msg; - msg.Printf(_("Generation: %u, Interval: %u ms"), m_tics, m_interval); + msg.Printf(_(" Generation: %u (T: %u ms), Population: %u "), + m_tics, + m_topspeed? 0 : m_interval, + m_life->GetNumCells()); m_text->SetLabel(msg); } -// event handlers +// Enable or disable tools and menu entries according to the current +// state. See also wxEVT_UPDATE_UI events for a slightly different +// way to do this. +void LifeFrame::UpdateUI() +{ + // start / stop + GetToolBar()->EnableTool(ID_START, !m_running); + GetToolBar()->EnableTool(ID_STOP, m_running); + GetMenuBar()->GetMenu(2)->Enable(ID_START, !m_running); + GetMenuBar()->GetMenu(2)->Enable(ID_STEP, !m_running); + GetMenuBar()->GetMenu(2)->Enable(ID_STOP, m_running); + + // zooming + int cellsize = m_canvas->GetCellSize(); + GetToolBar()->EnableTool(ID_ZOOMIN, cellsize < 32); + GetToolBar()->EnableTool(ID_ZOOMOUT, cellsize > 1); + GetMenuBar()->GetMenu(1)->Enable(ID_ZOOMIN, cellsize < 32); + GetMenuBar()->GetMenu(1)->Enable(ID_ZOOMOUT, cellsize > 1); +} + +// Event handlers ----------------------------------------------------------- + +// OnMenu handles all events which don't have their own event handler void LifeFrame::OnMenu(wxCommandEvent& event) { switch (event.GetId()) { - case ID_START : OnStart(); break; - case ID_STEP : OnTimer(); break; - case ID_STOP : OnStop(); break; - case ID_WRAP : - { - bool checked = GetMenuBar()->GetMenu(1)->IsChecked(ID_WRAP); - m_life->SetBorderWrap(checked); - break; - } - case ID_CLEAR : + case ID_NEW: { + // stop if it was running OnStop(); m_life->Clear(); - m_canvas->DrawEverything(TRUE); - m_canvas->Refresh(FALSE); + m_canvas->Recenter(0, 0); m_tics = 0; UpdateInfoText(); break; } - case ID_ABOUT : + case ID_ABOUT: { - wxMessageBox( - _("This is the about dialog of the Life! sample.\n" - "(c) 2000 Guillermo Rodriguez Garcia"), - _("About Life!"), - wxOK | wxICON_INFORMATION, - this); + LifeAboutDialog dialog(this); + dialog.ShowModal(); break; } - case ID_EXIT : + case ID_EXIT: { // TRUE is to force the frame to close Close(TRUE); break; } + case ID_SHOWNAV : + { + bool checked = GetMenuBar()->GetMenu(1)->IsChecked(ID_SHOWNAV); + m_navigator->Show(checked); + break; + } + case ID_INFO: + { + wxString desc = m_life->GetDescription(); + + if ( desc.IsEmpty() ) + desc = _("Not available"); + + // should we make the description editable here? + wxMessageBox(desc, _("Description"), wxOK | wxICON_INFORMATION); + + break; + } + case ID_START : OnStart(); break; + case ID_STEP : OnStep(); break; + case ID_STOP : OnStop(); break; + case ID_TOPSPEED: + { + m_running = TRUE; + m_topspeed = TRUE; + UpdateUI(); + while (m_running && m_topspeed) + { + OnStep(); + wxYield(); + } + break; + } } } -void LifeFrame::OnNewGame(wxCommandEvent& WXUNUSED(event)) +void LifeFrame::OnOpen(wxCommandEvent& WXUNUSED(event)) { - int w = m_life->GetWidth(); - int h = m_life->GetHeight(); - - // stop if it was running - OnStop(); - - // dialog box - LifeNewGameDialog dialog(this, &w, &h); - - // new game? - if (dialog.ShowModal() == wxID_OK) + wxFileDialog filedlg(this, + _("Choose a file to open"), + _(""), + _(""), + _("Life patterns (*.lif)|*.lif|All files (*.*)|*.*"), + wxOPEN | wxFILE_MUST_EXIST); + + if (filedlg.ShowModal() == wxID_OK) { - // check dimensions - if (w >= LIFE_MIN && w <= LIFE_MAX && - h >= LIFE_MIN && h <= LIFE_MAX) + wxFileInputStream stream(filedlg.GetPath()); + LifeReader reader(stream); + + // the reader handles errors itself, no need to do anything here + if (reader.IsOk()) { - // resize game field - m_life->Destroy(); - m_life->Create(w, h); - - // tell the canvas - m_canvas->Reset(); - m_canvas->Refresh(); + // stop if running and put the pattern + OnStop(); + m_life->Clear(); + m_life->SetPattern(reader.GetPattern()); + + // recenter canvas + m_canvas->Recenter(0, 0); m_tics = 0; UpdateInfoText(); } - else - { - wxString msg; - msg.Printf(_("Both dimensions must be within %u and %u.\n"), - LIFE_MIN, LIFE_MAX); - wxMessageBox(msg, _("Error!"), wxOK | wxICON_EXCLAMATION, this); - } } } @@ -564,60 +470,89 @@ void LifeFrame::OnSamples(wxCommandEvent& WXUNUSED(event)) // dialog box LifeSamplesDialog dialog(this); - // new game? if (dialog.ShowModal() == wxID_OK) { - int result = dialog.GetValue(); + const LifePattern pattern = dialog.GetPattern(); - if (result == -1) - return; + // put the pattern + m_life->Clear(); + m_life->SetPattern(pattern); - int gw = g_shapes[result].m_fieldWidth; - int gh = g_shapes[result].m_fieldHeight; - int wrap = g_shapes[result].m_wrap; + // recenter canvas + m_canvas->Recenter(0, 0); + m_tics = 0; + UpdateInfoText(); + } +} - // set wraparound (don't ask the user) - m_life->SetBorderWrap(wrap); - GetMenuBar()->GetMenu(1)->Check(ID_WRAP, wrap); +void LifeFrame::OnZoom(wxCommandEvent& event) +{ + int cellsize = m_canvas->GetCellSize(); - // need to resize the game field? - if (gw > m_life->GetWidth() || gh > m_life->GetHeight()) - { - wxString s; - s.Printf(_("Your game field is too small for this configuration.\n" - "It is recommended to resize it to %u x %u. Proceed?\n"), - gw, gh); + if ((event.GetId() == ID_ZOOMIN) && cellsize < 32) + { + m_canvas->SetCellSize(cellsize * 2); + UpdateUI(); + } + else if ((event.GetId() == ID_ZOOMOUT) && cellsize > 1) + { + m_canvas->SetCellSize(cellsize / 2); + UpdateUI(); + } +} - if (wxMessageBox(s, _("Question"), wxYES_NO | wxICON_QUESTION, this) == wxYES) - { - m_life->Destroy(); - m_life->Create(gw, gh); - } - } +void LifeFrame::OnNavigate(wxCommandEvent& event) +{ + LifeCell c; - // put the shape - m_life->SetShape(g_shapes[result]); + switch (event.GetId()) + { + case ID_NORTH: c = m_life->FindNorth(); break; + case ID_SOUTH: c = m_life->FindSouth(); break; + case ID_WEST: c = m_life->FindWest(); break; + case ID_EAST: c = m_life->FindEast(); break; + case ID_CENTER: c = m_life->FindCenter(); break; + case ID_ORIGIN: c.i = c.j = 0; break; + } - // tell the canvas about the change - m_canvas->Reset(); - m_canvas->Refresh(); - m_tics = 0; - UpdateInfoText(); + m_canvas->Recenter(c.i, c.j); +} + +void LifeFrame::OnSlider(wxScrollEvent& event) +{ + m_interval = event.GetPosition() * 100; + + if (m_running) + { + OnStop(); + OnStart(); } + + UpdateInfoText(); +} + +void LifeFrame::OnTimer(wxTimerEvent& WXUNUSED(event)) +{ + OnStep(); +} + +void LifeFrame::OnClose(wxCloseEvent& WXUNUSED(event)) +{ + // Stop if it was running; this is absolutely needed because + // the frame won't be actually destroyed until there are no + // more pending events, and this in turn won't ever happen + // if the timer is running faster than the window can redraw. + OnStop(); + Destroy(); } void LifeFrame::OnStart() { if (!m_running) { - GetToolBar()->EnableTool(ID_START, FALSE); - GetToolBar()->EnableTool(ID_STOP, TRUE); - GetMenuBar()->GetMenu(1)->Enable(ID_START, FALSE); - GetMenuBar()->GetMenu(1)->Enable(ID_STEP, FALSE); - GetMenuBar()->GetMenu(1)->Enable(ID_STOP, TRUE); - m_timer->Start(m_interval); m_running = TRUE; + UpdateUI(); } } @@ -625,51 +560,108 @@ void LifeFrame::OnStop() { if (m_running) { - GetToolBar()->EnableTool(ID_START, TRUE); - GetToolBar()->EnableTool(ID_STOP, FALSE); - GetMenuBar()->GetMenu(1)->Enable(ID_START, TRUE); - GetMenuBar()->GetMenu(1)->Enable(ID_STEP, TRUE); - GetMenuBar()->GetMenu(1)->Enable(ID_STOP, FALSE); - m_timer->Stop(); m_running = FALSE; + m_topspeed = FALSE; + UpdateUI(); } } -void LifeFrame::OnTimer() +void LifeFrame::OnStep() { if (m_life->NextTic()) m_tics++; else OnStop(); + m_canvas->DrawChanged(); UpdateInfoText(); - m_canvas->DrawEverything(); - m_canvas->Refresh(FALSE); } -void LifeFrame::OnSlider(wxScrollEvent& event) + +// -------------------------------------------------------------------------- +// LifeNavigator miniframe +// -------------------------------------------------------------------------- + +LifeNavigator::LifeNavigator(wxWindow *parent) + : wxMiniFrame(parent, -1, + _("Navigation"), + wxDefaultPosition, + wxDefaultSize, + wxCAPTION | wxSIMPLE_BORDER) { - m_interval = event.GetPosition() * 100; + wxPanel *panel = new wxPanel(this, -1); + wxBoxSizer *sizer1 = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *sizer2 = new wxBoxSizer(wxHORIZONTAL); + + // create bitmaps and masks for the buttons + wxBitmap + bmpn = wxBITMAP(north), + bmpw = wxBITMAP(west), + bmpc = wxBITMAP(center), + bmpe = wxBITMAP(east), + bmps = wxBITMAP(south); + +#if !defined(__WXGTK__) && !defined(__WXMOTIF__) + bmpn.SetMask(new wxMask(bmpn, *wxLIGHT_GREY)); + bmpw.SetMask(new wxMask(bmpw, *wxLIGHT_GREY)); + bmpc.SetMask(new wxMask(bmpc, *wxLIGHT_GREY)); + bmpe.SetMask(new wxMask(bmpe, *wxLIGHT_GREY)); + bmps.SetMask(new wxMask(bmps, *wxLIGHT_GREY)); +#endif - // restart timer if running, to set the new interval - if (m_running) - { - m_timer->Stop(); - m_timer->Start(m_interval); - } + // create the buttons and attach tooltips to them + wxBitmapButton + *bn = new wxBitmapButton(panel, ID_NORTH, bmpn), + *bw = new wxBitmapButton(panel, ID_WEST , bmpw), + *bc = new wxBitmapButton(panel, ID_CENTER, bmpc), + *be = new wxBitmapButton(panel, ID_EAST , bmpe), + *bs = new wxBitmapButton(panel, ID_SOUTH, bmps); + +#if wxUSE_TOOLTIPS + bn->SetToolTip(_("Find northernmost cell")); + bw->SetToolTip(_("Find westernmost cell")); + bc->SetToolTip(_("Find center of mass")); + be->SetToolTip(_("Find easternmost cell")); + bs->SetToolTip(_("Find southernmost cell")); +#endif - UpdateInfoText(); -} + // add buttons to sizers + sizer2->Add( bw, 0, wxCENTRE | wxWEST, 4 ); + sizer2->Add( bc, 0, wxCENTRE); + sizer2->Add( be, 0, wxCENTRE | wxEAST, 4 ); + sizer1->Add( bn, 0, wxCENTRE | wxNORTH, 4 ); + sizer1->Add( sizer2 ); + sizer1->Add( bs, 0, wxCENTRE | wxSOUTH, 4 ); -// -------------------------------------------------------------------------- -// LifeTimer -// -------------------------------------------------------------------------- + // set the miniframe size + panel->SetSizer(sizer1); + panel->SetAutoLayout(TRUE); + sizer1->Fit(this); + sizer1->SetSizeHints(this); + + // move it to a sensible position + wxRect parentRect = parent->GetRect(); + wxSize childSize = GetSize(); + int x = parentRect.GetX() + + parentRect.GetWidth(); + int y = parentRect.GetY() + + (parentRect.GetHeight() - childSize.GetHeight()) / 4; + Move(x, y); + + // done + Show(TRUE); +} -void LifeTimer::Notify() +void LifeNavigator::OnClose(wxCloseEvent& event) { - GET_FRAME()->OnTimer(); -}; + // avoid if we can + if (event.CanVeto()) + event.Veto(); + else + Destroy(); +} + // -------------------------------------------------------------------------- // LifeCanvas @@ -677,506 +669,442 @@ void LifeTimer::Notify() // canvas constructor LifeCanvas::LifeCanvas(wxWindow *parent, Life *life, bool interactive) - : wxScrolledWindow(parent, -1, wxPoint(0, 0), wxSize(100, 100)) + : wxWindow(parent, -1, wxPoint(0, 0), wxSize(100, 100), + wxSUNKEN_BORDER) { m_life = life; m_interactive = interactive; m_cellsize = 8; - m_bmp = NULL; - Reset(); + m_status = MOUSE_NOACTION; + m_viewportX = 0; + m_viewportY = 0; + m_viewportH = 0; + m_viewportW = 0; + + if (m_interactive) + SetCursor(*wxCROSS_CURSOR); + + // reduce flicker if wxEVT_ERASE_BACKGROUND is not available + SetBackgroundColour(*wxWHITE); } LifeCanvas::~LifeCanvas() { - delete m_bmp; + delete m_life; } -void LifeCanvas::Reset() +// recenter at the given position +void LifeCanvas::Recenter(wxInt32 i, wxInt32 j) { - if (m_bmp) - delete m_bmp; - - m_status = MOUSE_NOACTION; - m_width = CellToCoord(m_life->GetWidth()) + 1; - m_height = CellToCoord(m_life->GetHeight()) + 1; - m_bmp = new wxBitmap(m_width, m_height); - wxCoord w = GetClientSize().GetX(); - wxCoord h = GetClientSize().GetY(); - m_xoffset = (w > m_width)? ((w - m_width) / 2) : 0; - m_yoffset = (h > m_height)? ((h - m_height) / 2) : 0; + m_viewportX = i - m_viewportW / 2; + m_viewportY = j - m_viewportH / 2; // redraw everything - DrawEverything(TRUE); - SetScrollbars(10, 10, (m_width + 9) / 10, (m_height + 9) / 10); + Refresh(FALSE); } -void LifeCanvas::DrawEverything(bool force) +// set the cell size and refresh display +void LifeCanvas::SetCellSize(int cellsize) { - wxMemoryDC dc; - - dc.SelectObject(*m_bmp); - dc.BeginDrawing(); - - // draw cells - for (int j = 0; j < m_life->GetHeight(); j++) - for (int i = 0; i < m_life->GetWidth(); i++) - if (force || m_life->HasChanged(i, j)) - DrawCell(i, j, dc); - - // bounding rectangle (always drawn - better than clipping region) - dc.SetPen(*wxBLACK_PEN); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(0, 0, m_width, m_height); - - dc.EndDrawing(); - dc.SelectObject(wxNullBitmap); + m_cellsize = cellsize; + + // find current center + wxInt32 cx = m_viewportX + m_viewportW / 2; + wxInt32 cy = m_viewportY + m_viewportH / 2; + + // get current canvas size and adjust viewport accordingly + int w, h; + GetClientSize(&w, &h); + m_viewportW = (w + m_cellsize - 1) / m_cellsize; + m_viewportH = (h + m_cellsize - 1) / m_cellsize; + + // recenter + m_viewportX = cx - m_viewportW / 2; + m_viewportY = cy - m_viewportH / 2; + + // adjust scrollbars + if (m_interactive) + { + SetScrollbar(wxHORIZONTAL, m_viewportW, m_viewportW, 3 * m_viewportW); + SetScrollbar(wxVERTICAL, m_viewportH, m_viewportH, 3 * m_viewportH); + m_thumbX = m_viewportW; + m_thumbY = m_viewportH; + } + + Refresh(FALSE); } -void LifeCanvas::DrawCell(int i, int j) +// draw a cell +void LifeCanvas::DrawCell(wxInt32 i, wxInt32 j, bool alive) { - wxMemoryDC dc; + wxClientDC dc(this); - dc.SelectObject(*m_bmp); - dc.BeginDrawing(); + dc.SetPen(alive? *wxBLACK_PEN : *wxWHITE_PEN); + dc.SetBrush(alive? *wxBLACK_BRUSH : *wxWHITE_BRUSH); - dc.SetClippingRegion(1, 1, m_width - 2, m_height - 2); + dc.BeginDrawing(); DrawCell(i, j, dc); - dc.EndDrawing(); - dc.SelectObject(wxNullBitmap); } -void LifeCanvas::DrawCell(int i, int j, wxDC &dc) +void LifeCanvas::DrawCell(wxInt32 i, wxInt32 j, wxDC &dc) { - if (m_life->IsAlive(i, j)) - { - dc.SetPen(*wxBLACK_PEN); - dc.SetBrush(*wxBLACK_BRUSH); - dc.DrawRectangle(CellToCoord(i), - CellToCoord(j), - m_cellsize, - m_cellsize); - } - else + wxCoord x = CellToX(i); + wxCoord y = CellToY(j); + + // if cellsize is 1 or 2, there will be no grid + switch (m_cellsize) { - dc.SetPen(*wxLIGHT_GREY_PEN); - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(CellToCoord(i), - CellToCoord(j), - m_cellsize, - m_cellsize); - dc.SetPen(*wxWHITE_PEN); - dc.SetBrush(*wxWHITE_BRUSH); - dc.DrawRectangle(CellToCoord(i) + 1, - CellToCoord(j) + 1, - m_cellsize - 1, - m_cellsize - 1); + case 1: + dc.DrawPoint(x, y); + break; + case 2: + dc.DrawRectangle(x, y, 2, 2); + break; + default: + dc.DrawRectangle(x + 1, y + 1, m_cellsize - 1, m_cellsize - 1); } } -// event handlers -void LifeCanvas::OnPaint(wxPaintEvent& event) +// draw all changed cells +void LifeCanvas::DrawChanged() { - wxPaintDC dc(this); - wxMemoryDC memdc; + wxClientDC dc(this); - wxRegionIterator upd(GetUpdateRegion()); - wxCoord x, y, w, h, xx, yy; + size_t ncells; + LifeCell *cells; + bool done = FALSE; + m_life->BeginFind(m_viewportX, + m_viewportY, + m_viewportX + m_viewportW, + m_viewportY + m_viewportH, + TRUE); + dc.BeginDrawing(); - memdc.SelectObject(*m_bmp); - while(upd) + if (m_cellsize == 1) { - x = upd.GetX(); - y = upd.GetY(); - w = upd.GetW(); - h = upd.GetH(); - CalcUnscrolledPosition(x, y, &xx, &yy); - - dc.Blit(x, y, w, h, &memdc, xx - m_xoffset, yy - m_yoffset); - upd++; - } - - memdc.SelectObject(wxNullBitmap); - dc.EndDrawing(); -} - -void LifeCanvas::OnMouse(wxMouseEvent& event) -{ - if (!m_interactive) - return; - - int x, y, xx, yy, i, j; - - // which cell are we pointing at? - x = event.GetX(); - y = event.GetY(); - CalcUnscrolledPosition(x, y, &xx, &yy); - i = CoordToCell( xx - m_xoffset ); - j = CoordToCell( yy - m_yoffset ); - - // adjust x, y to point to the upper left corner of the cell - CalcScrolledPosition( CellToCoord(i) + m_xoffset, - CellToCoord(j) + m_yoffset, - &x, &y ); - - // set cursor shape and statusbar text - if (i < 0 || i >= m_life->GetWidth() || - j < 0 || j >= m_life->GetHeight()) - { - GET_FRAME()->SetStatusText(wxEmptyString, 1); - SetCursor(*wxSTANDARD_CURSOR); + dc.SetPen(*wxBLACK_PEN); } else { - wxString msg; - msg.Printf(_("Cell: (%u, %u)"), i, j); - GET_FRAME()->SetStatusText(msg, 1); - SetCursor(*wxCROSS_CURSOR); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(*wxBLACK_BRUSH); } + dc.SetLogicalFunction(wxINVERT); - // button pressed? - if (!event.LeftIsDown()) + while (!done) { - m_status = MOUSE_NOACTION; - } - else if (i >= 0 && i < m_life->GetWidth() && - j >= 0 && j < m_life->GetHeight()) - { - bool alive = m_life->IsAlive(i, j); - - // if just pressed, update status - if (m_status == MOUSE_NOACTION) - m_status = (alive? MOUSE_ERASING : MOUSE_DRAWING); + done = m_life->FindMore(&cells, &ncells); - // toggle cell and refresh if needed - if (((m_status == MOUSE_ERASING) && alive) || - ((m_status == MOUSE_DRAWING) && !alive)) - { - wxRect rect(x, y, m_cellsize + 1, m_cellsize + 1); - m_life->SetCell(i, j, !alive); - DrawCell(i, j); - Refresh(FALSE, &rect); - } + for (size_t m = 0; m < ncells; m++) + DrawCell(cells[m].i, cells[m].j, dc); } + dc.EndDrawing(); } -void LifeCanvas::OnSize(wxSizeEvent& event) -{ - wxCoord w = event.GetSize().GetX(); - wxCoord h = event.GetSize().GetY(); - m_xoffset = (w > m_width)? ((w - m_width) / 2) : 0; - m_yoffset = (h > m_height)? ((h - m_height) / 2) : 0; - - // allow default processing - event.Skip(); -} - -// -------------------------------------------------------------------------- -// LifeNewGameDialog -// -------------------------------------------------------------------------- - -LifeNewGameDialog::LifeNewGameDialog(wxWindow *parent, int *w, int *h) - : wxDialog(parent, -1, - _("New game"), - wxDefaultPosition, - wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxDIALOG_MODAL) +// event handlers +void LifeCanvas::OnPaint(wxPaintEvent& event) { - m_w = w; - m_h = h; - - // spin ctrls - m_spinctrlw = new wxSpinCtrl( this, -1 ); - m_spinctrlw->SetValue(*m_w); - m_spinctrlw->SetRange(LIFE_MIN, LIFE_MAX); + wxPaintDC dc(this); + wxRect rect = GetUpdateRegion().GetBox(); + wxCoord x, y, w, h; + wxInt32 i0, j0, i1, j1; - m_spinctrlh = new wxSpinCtrl( this, -1 ); - m_spinctrlh->SetValue(*m_h); - m_spinctrlh->SetRange(LIFE_MIN, LIFE_MAX); + // find damaged area + x = rect.GetX(); + y = rect.GetY(); + w = rect.GetWidth(); + h = rect.GetHeight(); - // component layout - wxBoxSizer *inputsizer1 = new wxBoxSizer( wxHORIZONTAL ); - inputsizer1->Add( new wxStaticText(this, -1, _("Width")), 1, wxCENTRE | wxLEFT, 20); - inputsizer1->Add( m_spinctrlw, 2, wxCENTRE | wxLEFT | wxRIGHT, 20 ); - - wxBoxSizer *inputsizer2 = new wxBoxSizer( wxHORIZONTAL ); - inputsizer2->Add( new wxStaticText(this, -1, _("Height")), 1, wxCENTRE | wxLEFT, 20); - inputsizer2->Add( m_spinctrlh, 2, wxCENTRE | wxLEFT | wxRIGHT, 20 ); - - wxBoxSizer *topsizer = new wxBoxSizer( wxVERTICAL ); - topsizer->Add( CreateTextSizer(_("Enter board dimensions")), 0, wxALL, 10 ); - topsizer->Add( new wxStaticLine(this, -1), 0, wxGROW | wxLEFT | wxRIGHT | wxBOTTOM, 10); - topsizer->Add( inputsizer1, 1, wxGROW | wxLEFT | wxRIGHT, 5 ); - topsizer->Add( inputsizer2, 1, wxGROW | wxLEFT | wxRIGHT, 5 ); - topsizer->Add( new wxStaticLine(this, -1), 0, wxGROW | wxLEFT | wxRIGHT | wxTOP, 10); - topsizer->Add( CreateButtonSizer(wxOK | wxCANCEL), 0, wxCENTRE | wxALL, 10); - - // activate - SetSizer(topsizer); - SetAutoLayout(TRUE); - topsizer->SetSizeHints(this); - topsizer->Fit(this); - Centre(wxBOTH); -} + i0 = XToCell(x); + j0 = YToCell(y); + i1 = XToCell(x + w - 1); + j1 = YToCell(y + h - 1); -void LifeNewGameDialog::OnOK(wxCommandEvent& WXUNUSED(event)) -{ - *m_w = m_spinctrlw->GetValue(); - *m_h = m_spinctrlh->GetValue(); + size_t ncells; + LifeCell *cells; + bool done = FALSE; - EndModal(wxID_OK); -} + m_life->BeginFind(i0, j0, i1, j1, FALSE); + done = m_life->FindMore(&cells, &ncells); -// -------------------------------------------------------------------------- -// LifeSamplesDialog -// -------------------------------------------------------------------------- + // erase all damaged cells and draw the grid + dc.BeginDrawing(); + dc.SetBrush(*wxWHITE_BRUSH); -LifeSamplesDialog::LifeSamplesDialog(wxWindow *parent) - : wxDialog(parent, -1, - _("Sample games"), - wxDefaultPosition, - wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxDIALOG_MODAL) -{ - m_value = 0; - - // create and populate the list of available samples - m_list = new wxListBox( this, ID_LISTBOX, - wxDefaultPosition, - wxDefaultSize, - 0, NULL, - wxLB_SINGLE | wxLB_NEEDED_SB | wxLB_HSCROLL ); - - for (unsigned i = 0; i < (sizeof(g_shapes) / sizeof(LifeShape)); i++) - m_list->Append(g_shapes[i].m_name); - - // descriptions - wxStaticBox *statbox = new wxStaticBox( this, -1, _("Description")); - m_life = new Life( 16, 16 ); - m_life->SetShape(g_shapes[0]); - m_canvas = new LifeCanvas( this, m_life, FALSE ); - m_text = new wxTextCtrl( this, -1, - g_shapes[0].m_desc, - wxDefaultPosition, - wxSize(300, 60), - wxTE_MULTILINE | wxTE_READONLY); - - // layout components - wxStaticBoxSizer *sizer1 = new wxStaticBoxSizer( statbox, wxVERTICAL ); - sizer1->Add( m_canvas, 2, wxGROW | wxCENTRE | wxALL, 5); - sizer1->Add( m_text, 1, wxGROW | wxCENTRE | wxALL, 5 ); - - wxBoxSizer *sizer2 = new wxBoxSizer( wxHORIZONTAL ); - sizer2->Add( m_list, 0, wxGROW | wxCENTRE | wxALL, 5 ); - sizer2->Add( sizer1, 1, wxGROW | wxCENTRE | wxALL, 5 ); - - wxBoxSizer *sizer3 = new wxBoxSizer( wxVERTICAL ); - sizer3->Add( CreateTextSizer(_("Select one configuration")), 0, wxALL, 10 ); - sizer3->Add( new wxStaticLine(this, -1), 0, wxGROW | wxLEFT | wxRIGHT, 10 ); - sizer3->Add( sizer2, 1, wxGROW | wxCENTRE | wxALL, 5 ); - sizer3->Add( new wxStaticLine(this, -1), 0, wxGROW | wxLEFT | wxRIGHT, 10 ); - sizer3->Add( CreateButtonSizer(wxOK | wxCANCEL), 0, wxCENTRE | wxALL, 10 ); - - // activate - SetSizer(sizer3); - SetAutoLayout(TRUE); - sizer3->SetSizeHints(this); - sizer3->Fit(this); - Centre(wxBOTH); -} + if (m_cellsize <= 2) + { + // no grid + dc.SetPen(*wxWHITE_PEN); + dc.DrawRectangle(x, y, w, h); + } + else + { + x = CellToX(i0); + y = CellToY(j0); + w = CellToX(i1 + 1) - x + 1; + h = CellToY(j1 + 1) - y + 1; -LifeSamplesDialog::~LifeSamplesDialog() -{ - m_canvas->Destroy(); - delete m_life; -} + dc.SetPen(*wxLIGHT_GREY_PEN); + for (wxInt32 yy = y; yy <= (y + h - m_cellsize); yy += m_cellsize) + dc.DrawRectangle(x, yy, w, m_cellsize + 1); + for (wxInt32 xx = x; xx <= (x + w - m_cellsize); xx += m_cellsize) + dc.DrawLine(xx, y, xx, y + h); + } -int LifeSamplesDialog::GetValue() -{ - return m_value; -} + // draw all alive cells + dc.SetPen(*wxBLACK_PEN); + dc.SetBrush(*wxBLACK_BRUSH); -void LifeSamplesDialog::OnListBox(wxCommandEvent& event) -{ - if (event.GetSelection() != -1) + while (!done) { - m_value = m_list->GetSelection(); - m_text->SetValue(g_shapes[ event.GetSelection() ].m_desc); - m_life->SetShape(g_shapes[ event.GetSelection() ]); + for (size_t m = 0; m < ncells; m++) + DrawCell(cells[m].i, cells[m].j, dc); - m_canvas->DrawEverything(TRUE); // force redraw everything - m_canvas->Refresh(FALSE); // do not erase background + done = m_life->FindMore(&cells, &ncells); } -} -// -------------------------------------------------------------------------- -// Life -// -------------------------------------------------------------------------- + // last set + for (size_t m = 0; m < ncells; m++) + DrawCell(cells[m].i, cells[m].j, dc); -Life::Life(int width, int height) -{ - m_wrap = TRUE; - m_cells = NULL; - Create(width, height); + dc.EndDrawing(); } -Life::~Life() +void LifeCanvas::OnMouse(wxMouseEvent& event) { - Destroy(); -} + if (!m_interactive) + return; -void Life::Create(int width, int height) -{ - wxASSERT(width > 0 && height > 0); + // which cell are we pointing at? + wxInt32 i = XToCell( event.GetX() ); + wxInt32 j = YToCell( event.GetY() ); - m_width = width; - m_height = height; - m_cells = new Cell[m_width * m_height]; - Clear(); -} + // set statusbar text + wxString msg; + msg.Printf(_("Cell: (%d, %d)"), i, j); + ((LifeFrame *) wxGetApp().GetTopWindow())->SetStatusText(msg, 1); -void Life::Destroy() -{ - delete[] m_cells; -} + // NOTE that wxMouseEvent::LeftDown() and wxMouseEvent::LeftIsDown() + // have different semantics. The first one is used to signal that the + // button was just pressed (i.e., in "button down" events); the second + // one just describes the current status of the button, independently + // of the mouse event type. LeftIsDown is typically used in "mouse + // move" events, to test if the button is _still_ pressed. -void Life::Clear() -{ - for (int i = 0; i < m_width * m_height; i++) - m_cells[i] = CELL_DEAD; -} + // is the button down? + if (!event.LeftIsDown()) + { + m_status = MOUSE_NOACTION; + return; + } -bool Life::IsAlive(int x, int y) const -{ - wxASSERT(x >= 0 && y >= 0 && x < m_width && y < m_height); + // was it pressed just now? + if (event.LeftDown()) + { + // yes: start a new action and toggle this cell + m_status = (m_life->IsAlive(i, j)? MOUSE_ERASING : MOUSE_DRAWING); + + m_mi = i; + m_mj = j; + m_life->SetCell(i, j, m_status == MOUSE_DRAWING); + DrawCell(i, j, m_status == MOUSE_DRAWING); + } + else if ((m_mi != i) || (m_mj != j)) + { + // no: continue ongoing action + bool alive = (m_status == MOUSE_DRAWING); + + // prepare DC and pen + brush to optimize drawing + wxClientDC dc(this); + dc.SetPen(alive? *wxBLACK_PEN : *wxWHITE_PEN); + dc.SetBrush(alive? *wxBLACK_BRUSH : *wxWHITE_BRUSH); + dc.BeginDrawing(); + + // draw a line of cells using Bresenham's algorithm + wxInt32 d, ii, jj, di, ai, si, dj, aj, sj; + di = i - m_mi; + ai = abs(di) << 1; + si = (di < 0)? -1 : 1; + dj = j - m_mj; + aj = abs(dj) << 1; + sj = (dj < 0)? -1 : 1; + + ii = m_mi; + jj = m_mj; + + if (ai > aj) + { + // iterate over i + d = aj - (ai >> 1); + + while (ii != i) + { + m_life->SetCell(ii, jj, alive); + DrawCell(ii, jj, dc); + if (d >= 0) + { + jj += sj; + d -= ai; + } + ii += si; + d += aj; + } + } + else + { + // iterate over j + d = ai - (aj >> 1); - return (m_cells[y * m_width + x] & CELL_ALIVE); -} + while (jj != j) + { + m_life->SetCell(ii, jj, alive); + DrawCell(ii, jj, dc); + if (d >= 0) + { + ii += si; + d -= aj; + } + jj += sj; + d += ai; + } + } -bool Life::HasChanged(int x, int y) const -{ - wxASSERT(x >= 0 && y >= 0 && x < m_width && y < m_height); + // last cell + m_life->SetCell(ii, jj, alive); + DrawCell(ii, jj, dc); + m_mi = ii; + m_mj = jj; - return (m_cells[y * m_width + x] & CELL_MARK) != 0; -} + dc.EndDrawing(); + } -void Life::SetBorderWrap(bool on) -{ - m_wrap = on; + ((LifeFrame *) wxGetApp().GetTopWindow())->UpdateInfoText(); } -void Life::SetCell(int x, int y, bool alive) +void LifeCanvas::OnSize(wxSizeEvent& event) { - wxASSERT(x >= 0 && y >= 0 && x < m_width && y < m_height); + // find center + wxInt32 cx = m_viewportX + m_viewportW / 2; + wxInt32 cy = m_viewportY + m_viewportH / 2; - m_cells[y * m_width + x] = (alive? CELL_ALIVE : CELL_DEAD); -} + // get new size + wxCoord w = event.GetSize().GetX(); + wxCoord h = event.GetSize().GetY(); + m_viewportW = (w + m_cellsize - 1) / m_cellsize; + m_viewportH = (h + m_cellsize - 1) / m_cellsize; -void Life::SetShape(LifeShape& shape) -{ - wxASSERT((m_width >= shape.m_width) && (m_height >= shape.m_height)); + // recenter + m_viewportX = cx - m_viewportW / 2; + m_viewportY = cy - m_viewportH / 2; - int x0 = (m_width - shape.m_width) / 2; - int y0 = (m_height - shape.m_height) / 2; - char *p = shape.m_data; + // scrollbars + if (m_interactive) + { + SetScrollbar(wxHORIZONTAL, m_viewportW, m_viewportW, 3 * m_viewportW); + SetScrollbar(wxVERTICAL, m_viewportH, m_viewportH, 3 * m_viewportH); + m_thumbX = m_viewportW; + m_thumbY = m_viewportH; + } - Clear(); - for (int j = y0; j < y0 + shape.m_height; j++) - for (int i = x0; i < x0 + shape.m_width; i++) - SetCell(i, j, *(p++) == '*'); + // allow default processing + event.Skip(); } -bool Life::NextTic() +void LifeCanvas::OnScroll(wxScrollWinEvent& event) { - long changed = 0; - int i, j; - - /* 1st pass. Find and mark deaths and births for this generation. - * - * Rules: - * An organism with <= 1 neighbors will die due to isolation. - * An organism with >= 4 neighbors will die due to starvation. - * New organisms are born in cells with exactly 3 neighbors. - */ - for (j = 0; j < m_height; j++) - for (i = 0; i < m_width; i++) + WXTYPE type = event.GetEventType(); + int pos = event.GetPosition(); + int orient = event.GetOrientation(); + + // calculate scroll increment + int scrollinc = 0; + if (type == wxEVT_SCROLLWIN_TOP) + { + if (orient == wxHORIZONTAL) + scrollinc = -m_viewportW; + else + scrollinc = -m_viewportH; + } + else + if (type == wxEVT_SCROLLWIN_BOTTOM) + { + if (orient == wxHORIZONTAL) + scrollinc = m_viewportW; + else + scrollinc = m_viewportH; + } + else + if (type == wxEVT_SCROLLWIN_LINEUP) + { + scrollinc = -1; + } + else + if (type == wxEVT_SCROLLWIN_LINEDOWN) + { + scrollinc = +1; + } + else + if (type == wxEVT_SCROLLWIN_PAGEUP) + { + scrollinc = -10; + } + else + if (type == wxEVT_SCROLLWIN_PAGEDOWN) + { + scrollinc = -10; + } + else + if (type == wxEVT_SCROLLWIN_THUMBTRACK) + { + if (orient == wxHORIZONTAL) { - int neighbors = GetNeighbors(i, j); - bool alive = IsAlive(i, j); - - /* Set CELL_MARK if this cell must change, clear it - * otherwise. We cannot toggle the CELL_ALIVE bit yet - * because all deaths and births are simultaneous (it - * would affect neighbouring cells). - */ - if ((!alive && neighbors == 3) || - (alive && (neighbors <= 1 || neighbors >= 4))) - m_cells[j * m_width + i] |= CELL_MARK; - else - m_cells[j * m_width + i] &= ~CELL_MARK; + scrollinc = pos - m_thumbX; + m_thumbX = pos; } - - /* 2nd pass. Stabilize. - */ - for (j = 0; j < m_height; j++) - for (i = 0; i < m_width; i++) + else { - /* Toggle CELL_ALIVE for those cells marked in the - * previous pass. Do not clear the CELL_MARK bit yet; - * it is useful to know which cells have changed and - * thus must be updated in the screen. - */ - if (m_cells[j * m_width + i] & CELL_MARK) - { - m_cells[j * m_width + i] ^= CELL_ALIVE; - changed++; - } + scrollinc = pos - m_thumbY; + m_thumbY = pos; } + } + else + if (type == wxEVT_SCROLLWIN_THUMBRELEASE) + { + m_thumbX = m_viewportW; + m_thumbY = m_viewportH; + } - return (changed != 0); -} - -int Life::GetNeighbors(int x, int y) const -{ - wxASSERT(x >= 0 && y >= 0 && x < m_width && y < m_height); - - int neighbors = 0; - - int i0 = (x)? (x - 1) : 0; - int j0 = (y)? (y - 1) : 0; - int i1 = (x < (m_width - 1))? (x + 1) : (m_width - 1); - int j1 = (y < (m_height - 1))? (y + 1) : (m_height - 1); +#if defined(__WXGTK__) || defined(__WXMOTIF__) + // wxGTK and wxMotif update the thumb automatically (wxMSW doesn't); + // so reset it back as we always want it to be in the same position. + if (type != wxEVT_SCROLLWIN_THUMBTRACK) + { + SetScrollbar(wxHORIZONTAL, m_viewportW, m_viewportW, 3 * m_viewportW); + SetScrollbar(wxVERTICAL, m_viewportH, m_viewportH, 3 * m_viewportH); + } +#endif - if (m_wrap && ( !x || !y || x == (m_width - 1) || y == (m_height - 1))) + if (scrollinc == 0) return; + + // scroll the window and adjust the viewport + if (orient == wxHORIZONTAL) { - // this is an outer cell and wraparound is on - for (int j = y - 1; j <= y + 1; j++) - for (int i = x - 1; i <= x + 1; i++) - if (IsAlive( ((i < 0)? (i + m_width ) : (i % m_width)), - ((j < 0)? (j + m_height) : (j % m_height)) )) - neighbors++; + m_viewportX += scrollinc; + ScrollWindow( -m_cellsize * scrollinc, 0, (const wxRect *) NULL); } else { - // this is an inner cell, or wraparound is off - for (int j = j0; j <= j1; j++) - for (int i = i0; i <= i1; i++) - if (IsAlive(i, j)) - neighbors++; + m_viewportY += scrollinc; + ScrollWindow( 0, -m_cellsize * scrollinc, (const wxRect *) NULL); } - - // do not count ourselves - if (IsAlive(x, y)) neighbors--; - - return neighbors; } -void Life::SetCell(int x, int y, Cell status) +void LifeCanvas::OnEraseBackground(wxEraseEvent& WXUNUSED(event)) { - wxASSERT(x >= 0 && y >= 0 && x < m_width && y < m_height); - - m_cells[y * m_width + x] = status; + // do nothing. I just don't want the background to be erased, you know. } +