Add a STYLUS_BASED variant to magnets
[sgt-puzzles/ydirson.git] / qt.cxx
blob61de29e2b7e65b4f6f454c58e9a04da75db39186
1 #include <Qt/QtGui>
3 /* TODO:
4 * - custom parameters
5 * - keyboard shortcuts
6 * - STYLUS_BASED
7 */
8 #ifdef QTOPIA
9 #include <QtopiaApplication>
10 #include <QSoftMenuBar>
11 #endif
13 #include <cassert>
14 #include <sys/time.h>
16 extern "C" {
17 #include "puzzles.h"
20 #include "qt.h"
22 // stubs to allow *not* supporting printing
24 void document_add_puzzle(document *doc, const game *game, game_params *par,
25 game_state *st, game_state *st2)
27 assert(0);
30 /* ----------------------------------------------------------------------
31 * Error reporting functions used elsewhere.
34 void fatal(char *fmt, ...)
36 va_list ap;
38 fprintf(stderr, "fatal error: ");
40 va_start(ap, fmt);
41 vfprintf(stderr, fmt, ap);
42 va_end(ap);
44 fprintf(stderr, "\n");
45 exit(1);
48 /* ----------------------------------------------------------------------
49 * Qt front end to puzzles.
52 struct blitter {
53 QPixmap *pixmap;
54 int w, h, x, y;
57 static void do_start_draw(frontend *fe)
59 bool ok = fe->painter->begin(fe->pixmap);
60 fe->painter->setRenderHints(QPainter::Antialiasing, true);
61 assert(ok);
63 static void do_end_draw(frontend *fe)
65 bool ok = fe->painter->end();
66 assert(ok);
69 static void setup_backing_store(frontend *fe)
71 fe->pixmap = new QPixmap(fe->pw, fe->ph);
73 do_start_draw(fe);
74 fe->painter->eraseRect(0, 0, fe->pw, fe->ph);
75 do_end_draw(fe);
78 static int backing_store_ok(frontend *fe)
80 return (!!fe->pixmap);
83 static void teardown_backing_store(frontend *fe)
85 delete fe->pixmap;
86 fe->pixmap = NULL;
89 static void repaint_rectangle(frontend *fe, QPaintDevice *widget,
90 int x, int y, int w, int h)
92 QPainter painter(widget);
93 if (x < fe->ox) {
94 painter.eraseRect(x, y, fe->ox - x, h);
95 w -= (fe->ox - x);
96 x = fe->ox;
98 if (y < fe->oy) {
99 painter.eraseRect(x, y, w, fe->oy - y);
100 h -= (fe->oy - y);
101 y = fe->oy;
103 if (w > fe->pw) {
104 painter.eraseRect(x + fe->pw, y, w - fe->pw, h);
105 w = fe->pw;
107 if (h > fe->ph) {
108 painter.eraseRect(x, y + fe->ph, w, h - fe->ph);
109 h = fe->ph;
111 painter.drawPixmap(QPoint(x, y), *fe->pixmap,
112 QRect(x - fe->ox, y - fe->oy, w, h));
115 Canvas::Canvas(frontend* fe): QWidget(), fe(fe) { }
116 void Canvas::paintEvent (QPaintEvent *event)
118 const QRect & rect = event->rect();
119 repaint_rectangle(fe, fe->area,
120 rect.x(), rect.y(),
121 rect.width(), rect.height());
124 void Canvas::do_resize(const QSize & size)
126 int w, h;
128 if (backing_store_ok(fe))
129 teardown_backing_store(fe);
131 w = fe->w = size.width();
132 h = fe->h = size.height();
133 midend_size(fe->me, &w, &h, TRUE);
134 fe->pw = w;
135 fe->ph = h;
136 fe->ox = (fe->w - fe->pw) / 2;
137 fe->oy = (fe->h - fe->ph) / 2;
139 setup_backing_store(fe);
140 midend_force_redraw(fe->me);
142 void Canvas::resizeEvent (QResizeEvent *event)
144 do_resize(event->size());
147 void Canvas::mousePressEvent (QMouseEvent *event)
149 int button;
150 if (!backing_store_ok(fe))
151 event->ignore();
152 switch(event->button()) {
153 case Qt::LeftButton: button = LEFT_BUTTON; break;
154 case Qt::RightButton: button = RIGHT_BUTTON; break;
155 case Qt::MidButton: button = MIDDLE_BUTTON; break;
156 default: event->ignore(); return; /* don't even know what button! */
158 event->accept();
159 if (!midend_process_key(fe->me, event->x() - fe->ox,
160 event->y() - fe->oy, button))
161 qApp->quit();
163 void Canvas::mouseReleaseEvent (QMouseEvent *event)
165 int button;
166 if (!backing_store_ok(fe))
167 event->ignore();
168 switch(event->button()) {
169 case Qt::LeftButton: button = LEFT_RELEASE; break;
170 case Qt::RightButton: button = RIGHT_RELEASE; break;
171 case Qt::MidButton: button = MIDDLE_RELEASE; break;
172 default: event->ignore(); return; /* don't even know what button! */
174 event->accept();
175 if (!midend_process_key(fe->me, event->x() - fe->ox,
176 event->y() - fe->oy, button))
177 qApp->quit();
179 void Canvas::mouseMoveEvent (QMouseEvent *event)
181 int button;
182 Qt::MouseButtons buttons = event->buttons();
183 if (buttons.testFlag(Qt::MidButton))
184 button = MIDDLE_DRAG;
185 else if (buttons.testFlag(Qt::LeftButton))
186 button = LEFT_DRAG;
187 else if (buttons.testFlag(Qt::RightButton))
188 button = RIGHT_DRAG;
189 else {
190 event->ignore(); /* don't even know what button! */
191 return;
193 event->accept();
194 if (!midend_process_key(fe->me, event->x() - fe->ox,
195 event->y() - fe->oy, button))
196 qApp->quit();
200 void get_random_seed(void **randseed, int *randseedsize)
202 struct timeval *tvp = snew(struct timeval);
203 gettimeofday(tvp, NULL);
204 *randseed = (void *)tvp;
205 *randseedsize = sizeof(struct timeval);
208 void frontend_default_colour(frontend *fe, float *output)
210 // FIXME: should probably ask fe->area, but it looks unavailable
211 // at this time ?
212 // FIXME: that is *not* white !?
213 output[0] = 1.0;
214 output[1] = 1.0;
215 output[2] = 1.0;
218 static void snaffle_colours(frontend *fe)
220 const float* colours = midend_colours(fe->me, &fe->ncolours);
221 fe->colours = new QColor[fe->ncolours];
222 for (int i = 0; i < fe->ncolours; i += 1)
223 fe->colours[i].setRgbF(colours[3*i], colours[3*i + 1], colours[3*i + 2]);
226 ////
228 class Timer: public QTimer
230 frontend* fe;
231 public:
232 struct timeval last_time;
233 Timer(frontend* fe): QTimer() {
234 this->fe = fe;
236 protected:
237 void timerEvent(QTimerEvent *e) {
238 struct timeval now;
239 float elapsed;
240 gettimeofday(&now, NULL);
241 elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F +
242 (now.tv_sec - last_time.tv_sec));
243 midend_timer(fe->me, elapsed); /* may clear timer_active */
244 last_time = now;
248 void deactivate_timer(frontend *fe)
250 if (!fe)
251 return; /* can happen due to --generate */
252 fe->timer->stop();
255 void activate_timer(frontend *fe)
257 if (!fe)
258 return; /* can happen due to --generate */
259 if (!fe->timer->isActive()) {
260 fe->timer->start(20);
261 gettimeofday(&fe->timer->last_time, NULL);
265 ////
267 static void qt_start_draw(void *handle)
269 frontend *fe = (frontend *)handle;
270 fe->bbox_l = fe->w;
271 fe->bbox_r = 0;
272 fe->bbox_u = fe->h;
273 fe->bbox_d = 0;
274 do_start_draw(fe);
277 static void qt_end_draw(void *handle)
279 frontend *fe = (frontend *)handle;
280 do_end_draw(fe);
282 if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
283 fe->area->update(fe->bbox_l - 1 + fe->ox,
284 fe->bbox_u - 1 + fe->oy,
285 fe->bbox_r - fe->bbox_l + 2,
286 fe->bbox_d - fe->bbox_u + 2);
290 static void qt_clip(void *handle, int x, int y, int w, int h)
292 frontend *fe = (frontend *)handle;
293 fe->painter->setClipRect(x, y, w, h);
296 static void qt_unclip(void *handle)
298 frontend *fe = (frontend *)handle;
299 fe->painter->setClipRect(0, 0, fe->w, fe->h);
302 static void qt_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
303 int align, int colour, char *text)
305 frontend *fe = (frontend *)handle;
307 QFont font;
308 font.setPixelSize(fontsize);
309 if (fonttype == FONT_FIXED)
310 font.setFamily("Monospace");
311 fe->painter->setFont(font);
313 QFontMetrics fm = fe->painter->fontMetrics();
314 int width = fm.width(text);
315 QRect rect(0, 0, width, fontsize);
317 int flags = 0;
318 if (align & ALIGN_VCENTRE) {
319 flags |= Qt::AlignVCenter;
320 rect.moveTop(y - fontsize/2);
321 } else { // as if (align & ALIGN_VNORMAL)
322 // FIXME implemented as bottom, not baseline...
323 flags |= Qt::AlignBottom;
324 rect.moveBottom(y);
326 if (align & ALIGN_HCENTRE) {
327 flags |= Qt::AlignHCenter;
328 rect.moveLeft(x - width/2);
329 } else if (align & ALIGN_HRIGHT) {
330 flags |= Qt::AlignRight;
331 rect.moveRight(x);
332 } else { // as if (align & ALIGN_HLEFT)
333 flags |= Qt::AlignLeft;
334 rect.moveLeft(x);
337 fe->painter->setPen(fe->colours[colour]);
338 fe->painter->drawText(rect, flags, text);
341 static void qt_draw_rect(void *handle, int x, int y, int w, int h, int colour)
343 frontend *fe = (frontend *)handle;
344 fe->painter->fillRect(x, y, w, h, fe->colours[colour]);
347 static void qt_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
349 frontend *fe = (frontend *)handle;
350 fe->painter->setPen(fe->colours[colour]);
351 fe->painter->drawLine(x1, y1, x2, y2);
354 static void qt_draw_polygon(void *handle, int *coords, int npoints,
355 int fillcolour, int outlinecolour)
357 frontend *fe = (frontend *)handle;
358 const QBrush oldbrush = fe->painter->brush();
359 if (fillcolour != -1)
360 fe->painter->setBrush(fe->colours[fillcolour]);
361 QPolygon polygon;
362 for (int i=0; i < npoints; i++)
363 polygon << QPoint(coords[i*2], coords[i*2 + 1]);
364 fe->painter->setPen(fe->colours[outlinecolour]);
365 fe->painter->drawPolygon(polygon);
366 if (fillcolour != -1)
367 fe->painter->setBrush(oldbrush);
370 static void qt_draw_circle(void *handle, int cx, int cy, int radius,
371 int fillcolour, int outlinecolour)
373 frontend *fe = (frontend *)handle;
374 QPainterPath path;
375 path.addEllipse(QPoint(cx, cy), radius, radius);
376 if (fillcolour != -1)
377 fe->painter->fillPath(path, fe->colours[fillcolour]);
378 fe->painter->setPen(fe->colours[outlinecolour]);
379 fe->painter->drawPath(path);
382 static blitter *qt_blitter_new(void *handle, int w, int h)
384 blitter *bl = snew(blitter);
386 * We can't create the pixmap right now, because fe->window
387 * might not yet exist. So we just cache w and h and create it
388 * during the firs call to blitter_save.
390 bl->pixmap = NULL;
391 bl->w = w;
392 bl->h = h;
393 return bl;
396 static void qt_blitter_free(void *handle, blitter *bl)
398 if (bl->pixmap)
399 delete bl->pixmap;
400 sfree(bl);
403 static void qt_blitter_save(void *handle, blitter *bl, int x, int y)
405 frontend *fe = (frontend *)handle;
407 if (!bl->pixmap) {
408 bl->pixmap = new QPixmap;
410 bl->x = x;
411 bl->y = y;
412 *bl->pixmap = fe->pixmap->copy(bl->x, bl->y, bl->w, bl->h);
415 static void qt_blitter_load(void *handle, blitter *bl, int x, int y)
417 frontend *fe = (frontend *)handle;
418 if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
419 x = bl->x;
420 y = bl->y;
422 assert(bl->pixmap);
423 fe->painter->drawPixmap(QPoint(x, y), *bl->pixmap);
426 static void qt_draw_update(void *handle, int x, int y, int w, int h)
428 frontend *fe = (frontend *)handle;
429 if (fe->bbox_l > x ) fe->bbox_l = x ;
430 if (fe->bbox_r < x+w) fe->bbox_r = x+w;
431 if (fe->bbox_u > y ) fe->bbox_u = y ;
432 if (fe->bbox_d < y+h) fe->bbox_d = y+h;
435 const struct drawing_api qt_drawing = {
436 qt_draw_text,
437 qt_draw_rect,
438 qt_draw_line,
439 qt_draw_polygon,
440 qt_draw_circle,
441 qt_draw_update,
442 qt_clip,
443 qt_unclip,
444 qt_start_draw,
445 qt_end_draw,
446 NULL, //qt_status_bar,
447 qt_blitter_new,
448 qt_blitter_free,
449 qt_blitter_save,
450 qt_blitter_load,
451 NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
452 NULL, NULL, /* line_width, line_dotted */
453 NULL, //text_fallback
454 NULL, //draw_thick_line,
457 static void maybe_enlarge_window(frontend *fe)
459 QSize delta = fe->area->sizeHint() - fe->area->size();
460 if (delta.width() < 0) delta.rwidth() = 0;
461 if (delta.height() < 0) delta.rheight() = 0;
462 if (delta == QSize(0, 0))
463 fe->area->do_resize(fe->window->size());
464 else
465 fe->window->resize(fe->window->size() + delta);
468 static void add_type_submenu(QMenu *menu, QWidget *window, frontend *fe, int npreset)
470 QActionGroup *itemgrp = new QActionGroup(window);
471 QSignalMapper *signalMapper = new QSignalMapper(window);
472 window->connect(signalMapper, SIGNAL(mapped(QObject*)),
473 window, SLOT(preset(QObject*)));
475 for (int i = 0; i < npreset; i++) {
476 char *name;
477 game_params *params;
479 midend_fetch_preset(fe->me, i, &name, &params);
481 QAction *a = new QAction(name, window);
482 menu->addAction(a);
483 window->connect(a, SIGNAL(triggered()), signalMapper, SLOT(map()));
484 GameParams *p = new GameParams(params);
485 signalMapper->setMapping(a, p);
486 itemgrp->addAction(menu->menuAction());
490 PuzzleWindow::PuzzleWindow(QWidget *parent, Qt::WFlags fl)
491 : QMainWindow( parent, fl )
493 fe = snew(frontend);
495 if (!fe) {
496 fprintf(stderr, "failed to allocate frontend\n");
497 exit(1);
500 fe->timer = new Timer(fe);
502 fe->me = midend_new(fe, &thegame, &qt_drawing, fe);
503 midend_new_game(fe->me);
505 setWindowTitle(thegame.name);
506 QWidget *vbox = new QWidget(this);
507 QVBoxLayout *vlayout = new QVBoxLayout(vbox);
508 vlayout->setMargin(0);
510 fe->pixmap = NULL;
511 fe->painter = new QPainter;
513 #ifdef QTOPIA
514 QMenu *menu = QSoftMenuBar::menuFor(this);
515 #else
516 QMenu *menu = this->menuBar()->addMenu("&Game");
517 #endif
518 menu->addAction("New game", this, SLOT(newGame()));
519 int npreset;
520 if ((npreset = midend_num_presets(fe->me)) > 0 || thegame.can_configure) {
521 #ifdef QTOPIA
522 QMenu *submenu = menu->addMenu("Type");
523 #else
524 QMenu *submenu = this->menuBar()->addMenu("Type");
525 #endif
526 add_type_submenu(submenu, this, fe, npreset);
528 menu->addAction("Restart", this, SLOT(restart()));
529 menu->addAction("Undo", this, SLOT(undo()));
530 menu->addAction("Redo", this, SLOT(redo()));
531 menu->addAction("Solve", this, SLOT(solve()));
532 #ifdef QTOPIA
533 menu->addSeparator();
534 QMenu *submenu = menu;
535 #else
536 menu->addAction("Exit", qApp, SLOT(quit()));
537 QMenu *submenu = this->menuBar()->addMenu("Help");
538 #endif
539 submenu->addAction("About", this, SLOT(about()));
541 snaffle_colours(fe);
543 fe->area = new Canvas(fe);
544 vlayout->addWidget(fe->area);
545 setCentralWidget(vbox);
547 fe->window = this;
549 QSize Canvas::sizeHint() const {
550 int x, y;
553 * Currently I don't want to scale large
554 * puzzles to fit on the screen. This is because X does permit
555 * extremely large windows and many window managers provide a
556 * means of navigating round them, and the users I consulted
557 * before deciding said that they'd rather have enormous puzzle
558 * windows spanning multiple screen pages than have them
559 * shrunk. I could change my mind later or introduce
560 * configurability; this would be the place to do so, by
561 * replacing the initial values of x and y with the screen
562 * dimensions.
564 x = INT_MAX;
565 y = INT_MAX;
566 midend_size(fe->me, &x, &y, FALSE);
567 return QSize(x, y);
569 void PuzzleWindow::newGame() {
570 midend_process_key(fe->me, 0, 0, 'n');
572 void PuzzleWindow::preset(QObject *o) {
573 GameParams *p = qobject_cast<GameParams*>(o);
574 assert (p);
576 midend_set_params(fe->me, p->params);
577 midend_new_game(fe->me);
578 //changed_preset(fe);
579 #ifdef QTOPIA
580 fe->area->do_resize(fe->window->size());
581 #else
582 maybe_enlarge_window(fe);
583 #endif
585 void PuzzleWindow::restart() {
586 midend_restart_game(fe->me);
588 void PuzzleWindow::undo() {
589 midend_process_key(fe->me, 0, 0, 'u');
591 void PuzzleWindow::redo() {
592 midend_process_key(fe->me, 0, 0, 'r');
594 void PuzzleWindow::solve() {
595 char *msg;
597 msg = midend_solve(fe->me);
599 if (msg) {
600 QMessageBox mb;
601 mb.setText(msg);
602 mb.exec();
605 void PuzzleWindow::about() {
606 char titlebuf[256];
607 char textbuf[1024];
609 sprintf(titlebuf, "About %.200s", thegame.name);
610 sprintf(textbuf,
611 "%.200s\n\n"
612 "from Simon Tatham's Portable Puzzle Collection\n\n"
613 "%.500s", thegame.name, ver);
615 QMessageBox mb;
616 mb.setWindowTitle(titlebuf);
617 mb.setText(textbuf);
618 mb.exec();
622 #ifdef QTOPIA
623 QTOPIA_ADD_APPLICATION(QTOPIA_TARGET,PuzzleWindow)
624 QTOPIA_MAIN
625 #else
626 int main(int argc, char **argv)
628 char *pname = argv[0];
629 int doing_opts = TRUE;
630 int ac = argc;
631 char **av = argv;
632 char errbuf[500];
635 * Command line parsing in this function is rather fiddly,
636 * because GTK wants to have a go at argc/argv _first_ - and
637 * yet we can't let it, because gtk_init() will bomb out if it
638 * can't open an X display, whereas in fact we want to permit
639 * our --generate and --print modes to run without an X
640 * display.
642 * So what we do is:
643 * - we parse the command line ourselves, without modifying
644 * argc/argv
645 * - if we encounter an error which might plausibly be the
646 * result of a GTK command line (i.e. not detailed errors in
647 * particular options of ours) we store the error message
648 * and terminate parsing.
649 * - if we got enough out of the command line to know it
650 * specifies a non-X mode of operation, we either display
651 * the stored error and return failure, or if there is no
652 * stored error we do the non-X operation and return
653 * success.
654 * - otherwise, we go straight to gtk_init().
657 errbuf[0] = '\0';
658 while (--ac > 0) {
659 char *p = *++av;
660 if (doing_opts && !strcmp(p, "--version")) {
661 printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
662 thegame.name, ver);
663 return 0;
664 } else if (doing_opts && !strcmp(p, "--")) {
665 doing_opts = FALSE;
666 } else {
667 sprintf(errbuf, "%.100s: unrecognised option '%.100s'\n",
668 pname, p);
669 break;
673 if (*errbuf) {
674 fputs(errbuf, stderr);
675 return 1;
678 QApplication app(argc, argv);
679 PuzzleWindow w;
680 w.show();
682 return app.exec();
684 #endif