Qt frontend: numpad support in STYLUS_BASED games
[sgt-puzzles/ydirson.git] / qt.cxx
blob7bfe83db254fc3ee3fa908ab9b08f6b07ebf4a2b
1 #include <Qt/QtGui>
3 /* TODO:
4 * - custom parameters
5 * - keyboard shortcuts
6 */
7 #ifdef QTOPIA
8 #include <QtopiaApplication>
9 #include <QSoftMenuBar>
10 #endif
12 #include <cassert>
13 #include <sys/time.h>
15 extern "C" {
16 #include "puzzles.h"
19 #include "qt.h"
21 // stubs to allow *not* supporting printing
23 void document_add_puzzle(document *doc, const game *game, game_params *par,
24 game_state *st, game_state *st2)
26 assert(0);
29 /* ----------------------------------------------------------------------
30 * Error reporting functions used elsewhere.
33 void fatal(char *fmt, ...)
35 va_list ap;
37 fprintf(stderr, "fatal error: ");
39 va_start(ap, fmt);
40 vfprintf(stderr, fmt, ap);
41 va_end(ap);
43 fprintf(stderr, "\n");
44 exit(1);
47 /* ----------------------------------------------------------------------
48 * Qt front end to puzzles.
51 struct blitter {
52 QPixmap *pixmap;
53 int w, h, x, y;
56 static void do_start_draw(frontend *fe)
58 bool ok = fe->painter->begin(fe->pixmap);
59 fe->painter->setRenderHints(QPainter::Antialiasing, true);
60 assert(ok);
62 static void do_end_draw(frontend *fe)
64 bool ok = fe->painter->end();
65 assert(ok);
68 static void setup_backing_store(frontend *fe)
70 fe->pixmap = new QPixmap(fe->pw, fe->ph);
72 do_start_draw(fe);
73 fe->painter->eraseRect(0, 0, fe->pw, fe->ph);
74 do_end_draw(fe);
77 static int backing_store_ok(frontend *fe)
79 return (!!fe->pixmap);
82 static void teardown_backing_store(frontend *fe)
84 delete fe->pixmap;
85 fe->pixmap = NULL;
88 static void repaint_rectangle(frontend *fe, QPaintDevice *widget,
89 int x, int y, int w, int h)
91 QPainter painter(widget);
92 if (x < fe->ox) {
93 painter.eraseRect(x, y, fe->ox - x, h);
94 w -= (fe->ox - x);
95 x = fe->ox;
97 if (y < fe->oy) {
98 painter.eraseRect(x, y, w, fe->oy - y);
99 h -= (fe->oy - y);
100 y = fe->oy;
102 if (w > fe->pw) {
103 painter.eraseRect(x + fe->pw, y, w - fe->pw, h);
104 w = fe->pw;
106 if (h > fe->ph) {
107 painter.eraseRect(x, y + fe->ph, w, h - fe->ph);
108 h = fe->ph;
110 painter.drawPixmap(QPoint(x, y), *fe->pixmap,
111 QRect(x - fe->ox, y - fe->oy, w, h));
114 Canvas::Canvas(frontend* fe): QWidget(), fe(fe) { }
115 void Canvas::paintEvent (QPaintEvent *event)
117 const QRect & rect = event->rect();
118 repaint_rectangle(fe, fe->area,
119 rect.x(), rect.y(),
120 rect.width(), rect.height());
123 void Canvas::do_resize(const QSize & size)
125 int w, h;
127 if (backing_store_ok(fe))
128 teardown_backing_store(fe);
130 w = fe->w = size.width();
131 h = fe->h = size.height();
132 midend_size(fe->me, &w, &h, TRUE);
133 fe->pw = w;
134 fe->ph = h;
135 fe->ox = (fe->w - fe->pw) / 2;
136 fe->oy = (fe->h - fe->ph) / 2;
138 setup_backing_store(fe);
139 midend_force_redraw(fe->me);
141 void Canvas::resizeEvent (QResizeEvent *event)
143 do_resize(event->size());
146 void Canvas::mousePressEvent (QMouseEvent *event)
148 int button;
149 if (!backing_store_ok(fe))
150 event->ignore();
151 switch(event->button()) {
152 case Qt::LeftButton: button = LEFT_BUTTON; break;
153 case Qt::RightButton: button = RIGHT_BUTTON; break;
154 case Qt::MidButton: button = MIDDLE_BUTTON; break;
155 default: event->ignore(); return; /* don't even know what button! */
157 event->accept();
158 if (!midend_process_key(fe->me, event->x() - fe->ox,
159 event->y() - fe->oy, button))
160 qApp->quit();
162 void Canvas::mouseReleaseEvent (QMouseEvent *event)
164 int button;
165 if (!backing_store_ok(fe))
166 event->ignore();
167 switch(event->button()) {
168 case Qt::LeftButton: button = LEFT_RELEASE; break;
169 case Qt::RightButton: button = RIGHT_RELEASE; break;
170 case Qt::MidButton: button = MIDDLE_RELEASE; break;
171 default: event->ignore(); return; /* don't even know what button! */
173 event->accept();
174 if (!midend_process_key(fe->me, event->x() - fe->ox,
175 event->y() - fe->oy, button))
176 qApp->quit();
178 void Canvas::mouseMoveEvent (QMouseEvent *event)
180 int button;
181 Qt::MouseButtons buttons = event->buttons();
182 if (buttons.testFlag(Qt::MidButton))
183 button = MIDDLE_DRAG;
184 else if (buttons.testFlag(Qt::LeftButton))
185 button = LEFT_DRAG;
186 else if (buttons.testFlag(Qt::RightButton))
187 button = RIGHT_DRAG;
188 else {
189 event->ignore(); /* don't even know what button! */
190 return;
192 event->accept();
193 if (!midend_process_key(fe->me, event->x() - fe->ox,
194 event->y() - fe->oy, button))
195 qApp->quit();
199 void get_random_seed(void **randseed, int *randseedsize)
201 struct timeval *tvp = snew(struct timeval);
202 gettimeofday(tvp, NULL);
203 *randseed = (void *)tvp;
204 *randseedsize = sizeof(struct timeval);
207 void frontend_default_colour(frontend *fe, float *output)
209 // FIXME: should probably ask fe->area, but it looks unavailable
210 // at this time ?
211 // FIXME: that is *not* white !?
212 output[0] = 1.0;
213 output[1] = 1.0;
214 output[2] = 1.0;
217 static void snaffle_colours(frontend *fe)
219 const float* colours = midend_colours(fe->me, &fe->ncolours);
220 fe->colours = new QColor[fe->ncolours];
221 for (int i = 0; i < fe->ncolours; i += 1)
222 fe->colours[i].setRgbF(colours[3*i], colours[3*i + 1], colours[3*i + 2]);
225 ////
227 class Timer: public QTimer
229 frontend* fe;
230 public:
231 struct timeval last_time;
232 Timer(frontend* fe): QTimer() {
233 this->fe = fe;
235 protected:
236 void timerEvent(QTimerEvent *e) {
237 struct timeval now;
238 float elapsed;
239 gettimeofday(&now, NULL);
240 elapsed = ((now.tv_usec - last_time.tv_usec) * 0.000001F +
241 (now.tv_sec - last_time.tv_sec));
242 midend_timer(fe->me, elapsed); /* may clear timer_active */
243 last_time = now;
247 void deactivate_timer(frontend *fe)
249 if (!fe)
250 return; /* can happen due to --generate */
251 fe->timer->stop();
254 void activate_timer(frontend *fe)
256 if (!fe)
257 return; /* can happen due to --generate */
258 if (!fe->timer->isActive()) {
259 fe->timer->start(20);
260 gettimeofday(&fe->timer->last_time, NULL);
264 ////
266 static void qt_start_draw(void *handle)
268 frontend *fe = (frontend *)handle;
269 fe->bbox_l = fe->w;
270 fe->bbox_r = 0;
271 fe->bbox_u = fe->h;
272 fe->bbox_d = 0;
273 do_start_draw(fe);
276 static void qt_end_draw(void *handle)
278 frontend *fe = (frontend *)handle;
279 do_end_draw(fe);
281 if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
282 fe->area->update(fe->bbox_l - 1 + fe->ox,
283 fe->bbox_u - 1 + fe->oy,
284 fe->bbox_r - fe->bbox_l + 2,
285 fe->bbox_d - fe->bbox_u + 2);
289 static void qt_clip(void *handle, int x, int y, int w, int h)
291 frontend *fe = (frontend *)handle;
292 fe->painter->setClipRect(x, y, w, h);
295 static void qt_unclip(void *handle)
297 frontend *fe = (frontend *)handle;
298 fe->painter->setClipRect(0, 0, fe->w, fe->h);
301 static void qt_draw_text(void *handle, int x, int y, int fonttype, int fontsize,
302 int align, int colour, char *text)
304 frontend *fe = (frontend *)handle;
306 QFont font;
307 font.setPixelSize(fontsize);
308 if (fonttype == FONT_FIXED)
309 font.setFamily("Monospace");
310 fe->painter->setFont(font);
312 QFontMetrics fm = fe->painter->fontMetrics();
313 int width = fm.width(text);
314 QRect rect(0, 0, width, fontsize);
316 int flags = 0;
317 if (align & ALIGN_VCENTRE) {
318 flags |= Qt::AlignVCenter;
319 rect.moveTop(y - fontsize/2);
320 } else { // as if (align & ALIGN_VNORMAL)
321 // FIXME implemented as bottom, not baseline...
322 flags |= Qt::AlignBottom;
323 rect.moveBottom(y);
325 if (align & ALIGN_HCENTRE) {
326 flags |= Qt::AlignHCenter;
327 rect.moveLeft(x - width/2);
328 } else if (align & ALIGN_HRIGHT) {
329 flags |= Qt::AlignRight;
330 rect.moveRight(x);
331 } else { // as if (align & ALIGN_HLEFT)
332 flags |= Qt::AlignLeft;
333 rect.moveLeft(x);
336 fe->painter->setPen(fe->colours[colour]);
337 fe->painter->drawText(rect, flags, text);
340 static void qt_draw_rect(void *handle, int x, int y, int w, int h, int colour)
342 frontend *fe = (frontend *)handle;
343 fe->painter->fillRect(x, y, w, h, fe->colours[colour]);
346 static void qt_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour)
348 frontend *fe = (frontend *)handle;
349 fe->painter->setPen(fe->colours[colour]);
350 fe->painter->drawLine(x1, y1, x2, y2);
353 static void qt_draw_polygon(void *handle, int *coords, int npoints,
354 int fillcolour, int outlinecolour)
356 frontend *fe = (frontend *)handle;
357 const QBrush oldbrush = fe->painter->brush();
358 if (fillcolour != -1)
359 fe->painter->setBrush(fe->colours[fillcolour]);
360 QPolygon polygon;
361 for (int i=0; i < npoints; i++)
362 polygon << QPoint(coords[i*2], coords[i*2 + 1]);
363 fe->painter->setPen(fe->colours[outlinecolour]);
364 fe->painter->drawPolygon(polygon);
365 if (fillcolour != -1)
366 fe->painter->setBrush(oldbrush);
369 static void qt_draw_circle(void *handle, int cx, int cy, int radius,
370 int fillcolour, int outlinecolour)
372 frontend *fe = (frontend *)handle;
373 QPainterPath path;
374 path.addEllipse(QPoint(cx, cy), radius, radius);
375 if (fillcolour != -1)
376 fe->painter->fillPath(path, fe->colours[fillcolour]);
377 fe->painter->setPen(fe->colours[outlinecolour]);
378 fe->painter->drawPath(path);
381 static blitter *qt_blitter_new(void *handle, int w, int h)
383 blitter *bl = snew(blitter);
385 * We can't create the pixmap right now, because fe->window
386 * might not yet exist. So we just cache w and h and create it
387 * during the firs call to blitter_save.
389 bl->pixmap = NULL;
390 bl->w = w;
391 bl->h = h;
392 return bl;
395 static void qt_blitter_free(void *handle, blitter *bl)
397 if (bl->pixmap)
398 delete bl->pixmap;
399 sfree(bl);
402 static void qt_blitter_save(void *handle, blitter *bl, int x, int y)
404 frontend *fe = (frontend *)handle;
406 if (!bl->pixmap) {
407 bl->pixmap = new QPixmap;
409 bl->x = x;
410 bl->y = y;
411 *bl->pixmap = fe->pixmap->copy(bl->x, bl->y, bl->w, bl->h);
414 static void qt_blitter_load(void *handle, blitter *bl, int x, int y)
416 frontend *fe = (frontend *)handle;
417 if (x == BLITTER_FROMSAVED && y == BLITTER_FROMSAVED) {
418 x = bl->x;
419 y = bl->y;
421 assert(bl->pixmap);
422 fe->painter->drawPixmap(QPoint(x, y), *bl->pixmap);
425 static void qt_draw_update(void *handle, int x, int y, int w, int h)
427 frontend *fe = (frontend *)handle;
428 if (fe->bbox_l > x ) fe->bbox_l = x ;
429 if (fe->bbox_r < x+w) fe->bbox_r = x+w;
430 if (fe->bbox_u > y ) fe->bbox_u = y ;
431 if (fe->bbox_d < y+h) fe->bbox_d = y+h;
434 const struct drawing_api qt_drawing = {
435 qt_draw_text,
436 qt_draw_rect,
437 qt_draw_line,
438 qt_draw_polygon,
439 qt_draw_circle,
440 qt_draw_update,
441 qt_clip,
442 qt_unclip,
443 qt_start_draw,
444 qt_end_draw,
445 NULL, //qt_status_bar,
446 qt_blitter_new,
447 qt_blitter_free,
448 qt_blitter_save,
449 qt_blitter_load,
450 NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
451 NULL, NULL, /* line_width, line_dotted */
452 NULL, //text_fallback
453 NULL, //draw_thick_line,
456 static void maybe_enlarge_window(frontend *fe)
458 QSize delta = fe->area->sizeHint() - fe->area->size();
459 if (delta.width() < 0) delta.rwidth() = 0;
460 if (delta.height() < 0) delta.rheight() = 0;
461 if (delta == QSize(0, 0))
462 fe->area->do_resize(fe->window->size());
463 else
464 fe->window->resize(fe->window->size() + delta);
467 static void add_type_submenu(QMenu *menu, QWidget *window, frontend *fe, int npreset)
469 QActionGroup *itemgrp = new QActionGroup(window);
470 QSignalMapper *signalMapper = new QSignalMapper(window);
471 window->connect(signalMapper, SIGNAL(mapped(QObject*)),
472 window, SLOT(preset(QObject*)));
474 for (int i = 0; i < npreset; i++) {
475 char *name;
476 game_params *params;
478 midend_fetch_preset(fe->me, i, &name, &params);
480 QAction *a = new QAction(name, window);
481 menu->addAction(a);
482 window->connect(a, SIGNAL(triggered()), signalMapper, SLOT(map()));
483 GameParams *p = new GameParams(params);
484 signalMapper->setMapping(a, p);
485 itemgrp->addAction(menu->menuAction());
489 PuzzleWindow::PuzzleWindow(QWidget *parent, Qt::WFlags fl)
490 : QMainWindow( parent, fl )
492 fe = snew(frontend);
494 if (!fe) {
495 fprintf(stderr, "failed to allocate frontend\n");
496 exit(1);
499 fe->timer = new Timer(fe);
501 fe->me = midend_new(fe, &thegame, &qt_drawing, fe);
502 midend_new_game(fe->me);
504 setWindowTitle(thegame.name);
505 QWidget *vbox = new QWidget(this);
506 QVBoxLayout *vlayout = new QVBoxLayout(vbox);
507 vlayout->setMargin(0);
509 fe->pixmap = NULL;
510 fe->painter = new QPainter;
512 #ifdef QTOPIA
513 QMenu *menu = QSoftMenuBar::menuFor(this);
514 #else
515 QMenu *menu = this->menuBar()->addMenu("&Game");
516 #endif
517 menu->addAction("New game", this, SLOT(newGame()));
518 int npreset;
519 if ((npreset = midend_num_presets(fe->me)) > 0 || thegame.can_configure) {
520 #ifdef QTOPIA
521 QMenu *submenu = menu->addMenu("Type");
522 #else
523 QMenu *submenu = this->menuBar()->addMenu("Type");
524 #endif
525 add_type_submenu(submenu, this, fe, npreset);
527 menu->addAction("Restart", this, SLOT(restart()));
528 menu->addAction("Undo", this, SLOT(undo()));
529 menu->addAction("Redo", this, SLOT(redo()));
530 menu->addAction("Solve", this, SLOT(solve()));
531 #ifdef QTOPIA
532 menu->addSeparator();
533 QMenu *submenu = menu;
534 #else
535 menu->addAction("Exit", qApp, SLOT(quit()));
536 QMenu *submenu = this->menuBar()->addMenu("Help");
537 #endif
538 submenu->addAction("About", this, SLOT(about()));
540 snaffle_colours(fe);
542 #ifdef STYLUS_BASED
543 if (thegame.flags & REQUIRE_NUMPAD) {
544 QSignalMapper *signalMapper = new QSignalMapper(this);
545 this->connect(signalMapper, SIGNAL(mapped(int)),
546 this, SLOT(numpad(int)));
548 QWidget *hbox = new QWidget(this);
549 QHBoxLayout *hlayout = new QHBoxLayout(hbox);
550 hlayout->setMargin(0);
551 vlayout->addWidget(hbox);
553 char buf[2];
554 buf[1] = '\0';
555 for(buf[0]='0'; buf[0]<='9'; buf[0]++) {
556 QPushButton *button = new QPushButton(buf);
557 button->setMinimumSize(1, 0); // FIXME we could do better
558 hlayout->addWidget(button);
559 this->connect(button, SIGNAL(clicked()), signalMapper, SLOT(map()));
560 signalMapper->setMapping(button, buf[0]);
563 #endif
564 fe->area = new Canvas(fe);
565 vlayout->addWidget(fe->area);
566 setCentralWidget(vbox);
568 fe->window = this;
570 QSize Canvas::sizeHint() const {
571 int x, y;
574 * Currently I don't want to scale large
575 * puzzles to fit on the screen. This is because X does permit
576 * extremely large windows and many window managers provide a
577 * means of navigating round them, and the users I consulted
578 * before deciding said that they'd rather have enormous puzzle
579 * windows spanning multiple screen pages than have them
580 * shrunk. I could change my mind later or introduce
581 * configurability; this would be the place to do so, by
582 * replacing the initial values of x and y with the screen
583 * dimensions.
585 x = INT_MAX;
586 y = INT_MAX;
587 midend_size(fe->me, &x, &y, FALSE);
588 return QSize(x, y);
590 void PuzzleWindow::newGame() {
591 midend_process_key(fe->me, 0, 0, 'n');
593 void PuzzleWindow::preset(QObject *o) {
594 GameParams *p = qobject_cast<GameParams*>(o);
595 assert (p);
597 midend_set_params(fe->me, p->params);
598 midend_new_game(fe->me);
599 //changed_preset(fe);
600 #ifdef QTOPIA
601 fe->area->do_resize(fe->window->size());
602 #else
603 maybe_enlarge_window(fe);
604 #endif
606 void PuzzleWindow::restart() {
607 midend_restart_game(fe->me);
609 void PuzzleWindow::undo() {
610 midend_process_key(fe->me, 0, 0, 'u');
612 void PuzzleWindow::redo() {
613 midend_process_key(fe->me, 0, 0, 'r');
615 void PuzzleWindow::solve() {
616 char *msg;
618 msg = midend_solve(fe->me);
620 if (msg) {
621 QMessageBox mb;
622 mb.setText(msg);
623 mb.exec();
626 void PuzzleWindow::numpad(int n) {
627 midend_process_key(fe->me, 0, 0, (char)n);
629 void PuzzleWindow::about() {
630 char titlebuf[256];
631 char textbuf[1024];
633 sprintf(titlebuf, "About %.200s", thegame.name);
634 sprintf(textbuf,
635 "%.200s\n\n"
636 "from Simon Tatham's Portable Puzzle Collection\n\n"
637 "%.500s", thegame.name, ver);
639 QMessageBox mb;
640 mb.setWindowTitle(titlebuf);
641 mb.setText(textbuf);
642 mb.exec();
646 #ifdef QTOPIA
647 QTOPIA_ADD_APPLICATION(QTOPIA_TARGET,PuzzleWindow)
648 QTOPIA_MAIN
649 #else
650 int main(int argc, char **argv)
652 char *pname = argv[0];
653 int doing_opts = TRUE;
654 int ac = argc;
655 char **av = argv;
656 char errbuf[500];
659 * Command line parsing in this function is rather fiddly,
660 * because GTK wants to have a go at argc/argv _first_ - and
661 * yet we can't let it, because gtk_init() will bomb out if it
662 * can't open an X display, whereas in fact we want to permit
663 * our --generate and --print modes to run without an X
664 * display.
666 * So what we do is:
667 * - we parse the command line ourselves, without modifying
668 * argc/argv
669 * - if we encounter an error which might plausibly be the
670 * result of a GTK command line (i.e. not detailed errors in
671 * particular options of ours) we store the error message
672 * and terminate parsing.
673 * - if we got enough out of the command line to know it
674 * specifies a non-X mode of operation, we either display
675 * the stored error and return failure, or if there is no
676 * stored error we do the non-X operation and return
677 * success.
678 * - otherwise, we go straight to gtk_init().
681 errbuf[0] = '\0';
682 while (--ac > 0) {
683 char *p = *++av;
684 if (doing_opts && !strcmp(p, "--version")) {
685 printf("%s, from Simon Tatham's Portable Puzzle Collection\n%s\n",
686 thegame.name, ver);
687 return 0;
688 } else if (doing_opts && !strcmp(p, "--")) {
689 doing_opts = FALSE;
690 } else {
691 sprintf(errbuf, "%.100s: unrecognised option '%.100s'\n",
692 pname, p);
693 break;
697 if (*errbuf) {
698 fputs(errbuf, stderr);
699 return 1;
702 QApplication app(argc, argv);
703 PuzzleWindow w;
704 w.show();
706 return app.exec();
708 #endif