open-axiom repository from github
// Copyright (C) 2011-2013, Gabriel Dos Reis.1// All rights reserved.2// Written by Gabriel Dos Reis.3//4// Redistribution and use in source and binary forms, with or without5// modification, are permitted provided that the following conditions are6// met:7//8// - Redistributions of source code must retain the above copyright9// notice, this list of conditions and the following disclaimer.10//11// - Redistributions in binary form must reproduce the above copyright12// notice, this list of conditions and the following disclaimer in13// the documentation and/or other materials provided with the14// distribution.15//16// - Neither the name of OpenAxiom. nor the names of its contributors17// may be used to endorse or promote products derived from this18// software without specific prior written permission.19//20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS21// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED22// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A23// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER24// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,25// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,26// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR27// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF28// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING29// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS30// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.3132#include <cmath>33#include <string>34#include <sstream>35#include <iostream>3637#include <QScrollBar>38#include <QApplication>39#include "conversation.h"40#include "debate.h"4142namespace OpenAxiom {43// Largest width and line spacing, in pixel, of the font metrics44// associated with `w'.45static QSize font_units(const QWidget* w) {46const QFontMetrics fm = w->fontMetrics();47const auto h = fm.lineSpacing();48if (auto w = fm.maxWidth())49return { w, h };50return { fm.width('W'), h };51}5253// Return true if the QString `s' is morally an empty string.54// QT makes a difference between a null string and an empty string.55// That distinction is largely pedantic and without difference56// for most of our practical purposes.57static bool58empty_string(const QString& s) {59return s.isNull() or s.isEmpty();60}6162// Return a resonable margin for this frame.63static int our_margin(const QFrame* f) {64return 2 + f->frameWidth();65}6667// --------------------68// -- OutputTextArea --69// --------------------70OutputTextArea::OutputTextArea(QWidget* p)71: Base(p), cur(document()) {72get_cursor().movePosition(QTextCursor::End);73setReadOnly(true); // this is a output only area.74setLineWrapMode(NoWrap); // for the time being, mess with nothing.75setFont(p->font());76setViewportMargins(0, 0, 0, 0);77setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);78// We do not want to see scroll bars. Usually disallowing vertical79// scroll bars and allocating enough horizontal space is sufficient80// to ensure that we don't see any horizontal scrollbar.81setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);82setLineWidth(1);83}8485// This overriding implementation is so that we can control the86// amount of vertical space in the read-only editor viewport allocated87// for the display of output text. In particular we do not want88// scrollbars.89QSize90OutputTextArea::sizeHint() const {91const QSize s = font_units(this);92return QSize(width(), (1 + document()->lineCount()) * s.height());93}9495void OutputTextArea::add_paragraph(const QString& s) {96if (not document()->isEmpty())97get_cursor().insertBlock();98get_cursor().insertText(s);99QSize sz = sizeHint();100sz.setWidth(parentWidget()->width() - our_margin(this));101resize(sz);102show();103updateGeometry();104}105106void OutputTextArea::add_text(const QString& s) {107setPlainText(toPlainText() + s);108QSize sz = sizeHint();109const int w = parentWidget()->width() - 2 * frameWidth();110if (w > sz.width())111sz.setWidth(w);112resize(sz);113show();114updateGeometry();115}116117OutputTextArea&118OutputTextArea::insert_block(const QString& s) {119if (not document()->isEmpty())120get_cursor().insertBlock();121get_cursor().insertText(s);122resize(sizeHint());123updateGeometry();124return *this;125}126127// --------------128// -- Question --129// --------------130Question::Question(Exchange* e) : QLineEdit(e) {131setBackgroundRole(QPalette::AlternateBase);132setFrame(true);133}134135void Question::focusInEvent(QFocusEvent* e) {136setFrame(true);137update();138QLineEdit::focusInEvent(e);139}140141void Question::enterEvent(QEvent* e) {142setFrame(true);143update();144QLineEdit::enterEvent(e);145}146147// ------------148// -- Answer --149// ------------150Answer::Answer(Exchange* e) : OutputTextArea(e) {151setFrameStyle(StyledPanel | Raised);152}153154// --------------155// -- Exchange --156// --------------157// Amount of pixel spacing between the query and reply areas.158const int spacing = 2;159160// Return a monospace font161static QFont monospace_font() {162QFont f("Monaco", 11);163f.setStyleHint(QFont::TypeWriter);164return f;165}166167// The layout within an exchange is as follows:168// -- input area (an editor) with its own decoation accounted for.169// -- an optional spacing170// -- an output area with its own decoration accounted for.171QSize Exchange::sizeHint() const {172const int m = our_margin(this);173QSize sz = question()->size() + QSize(2 * m, 2 * m);174if (not answer()->isHidden())175sz.rheight() += answer()->height() + spacing;176return sz;177}178179Server* Exchange::server() const {180return win->debate()->server();181}182183// Dress the query area with initial properties.184static void185prepare_query_widget(Conversation* conv, Exchange* e) {186Question* q = e->question();187q->setFrame(false);188q->setFont(conv->font());189const int m = our_margin(e);190q->setGeometry(m, m, conv->width() - 2 * m, q->height());191}192193// Dress the reply aread with initial properties.194// Place the reply widget right below the frame containing195// the query widget; make both of the same width, of course.196static void197prepare_reply_widget(Conversation* conv, Exchange* e) {198Answer* a = e->answer();199Question* q = e->question();200const QPoint pt = e->question()->geometry().bottomLeft();201const int m = our_margin(a);202a->setGeometry(pt.x(), pt.y() + spacing,203conv->width() - 2 * m, q->height());204a->setBackgroundRole(q->backgroundRole());205a->hide(); // nothing to show yet206}207208static void209finish_exchange_make_up(Conversation* conv, Exchange* e) {210e->setAutoFillBackground(true);211e->move(conv->bottom_left());212}213214Exchange::Exchange(Conversation* conv, int n)215: QFrame(conv), win(conv), no(n), query(this), reply(this) {216setLineWidth(1);217setFont(conv->font());218prepare_query_widget(conv, this);219prepare_reply_widget(conv, this);220finish_exchange_make_up(conv, this);221connect(question(), SIGNAL(returnPressed()),222this, SLOT(reply_to_query()));223}224225static void ensure_visibility(Debate* debate, Exchange* e) {226const int y = e->y() + e->height();227QScrollBar* vbar = debate->verticalScrollBar();228const int value = vbar->value();229int new_value = y - vbar->pageStep();230if (y < value)231vbar->setValue(std::max(new_value, 0));232else if (new_value > value)233vbar->setValue(std::min(new_value, vbar->maximum()));234e->question()->setFocus(Qt::OtherFocusReason);235}236237void238Exchange::reply_to_query() {239QString input = question()->text().trimmed();240if (empty_string(input))241return;242question()->setReadOnly(true); // Make query area read only.243question()->clearFocus();244question()->setFocusPolicy(Qt::NoFocus);245server()->input(input);246QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));247}248249void Exchange::resizeEvent(QResizeEvent* e) {250QFrame::resizeEvent(e);251const int w = width() - 2 * our_margin(this);252if (w > question()->width()) {253question()->resize(w, question()->height());254answer()->resize(w, answer()->height());255}256}257258// ------------259// -- Banner --260// ------------261Banner::Banner(Conversation* conv) : Base(conv) {262setFrameStyle(StyledPanel | Raised);263setBackgroundRole(QPalette::Base);264}265266// ------------------267// -- Conversation --268// -------------------269270// Default number of characters per question line.271const int columns = 80;272const int lines = 40;273274static QSize275minimum_preferred_size(const Conversation* conv) {276const QSize s = font_units(conv);277return QSize(columns * s.width(), lines * s.height());278}279280// Set a minimum preferred widget size, so no layout manager281// messes with it. Indicate we can make use of more space.282Conversation::Conversation(Debate* d)283: QWidget(d),284win(d),285greatings(this),286cur_ex(),287cur_out(&greatings),288rx("\\(\\d+\\)\\s->"),289tx("\\sType: ") {290setFont(monospace_font());291setBackgroundRole(QPalette::Base);292greatings.setFont(font());293setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);294}295296Conversation::~Conversation() {297for (int i = children.size() -1 ; i >= 0; --i)298delete children[i];299}300301QPoint Conversation::bottom_left() const {302if (length() == 0)303return greatings.geometry().bottomLeft();304return children.back()->geometry().bottomLeft();305}306307static QSize308round_up_height(const QSize& sz, int height) {309if (height < 1)310height = 1;311const int n = (sz.height() + height) / height;312return QSize(sz.width(), n * height);313}314315QSize Conversation::sizeHint() const {316const int n = length();317if (n == 0)318return minimum_preferred_size(this);319const int view_height = debate()->viewport()->height();320QSize sz = greatings.size();321for (int i = 0; i < n; ++i)322sz.rheight() += children[i]->height();323return round_up_height(sz, view_height);324}325326void Conversation::resizeEvent(QResizeEvent* e) {327QWidget::resizeEvent(e);328setMinimumSize(size());329const QSize sz = size();330if (e->oldSize() == sz)331return;332greatings.resize(sz.width(), greatings.height());333for (int i = 0; i < length(); ++i) {334Exchange* e = children[i];335e->resize(sz.width(), e->height());336}337}338339void Conversation::paintEvent(QPaintEvent* e) {340QWidget::paintEvent(e);341if (length() == 0)342greatings.update();343}344345Exchange*346Conversation::new_topic() {347Exchange* w = new Exchange(this, length() + 1);348w->show();349children.push_back(w);350adjustSize();351updateGeometry();352cur_out = w->answer();353return cur_ex = w;354}355356Exchange*357Conversation::next(Exchange* w) {358if (w == 0 or w->number() == length())359return new_topic();360return cur_ex = children[w->number()];361}362363static QTextCharFormat364get_type_format(OutputTextArea* area) {365auto format = area->get_cursor().charFormat();366format.setFontWeight(QFont::Bold);367format.setToolTip("domain of result");368return format;369}370371static void372display_type(OutputTextArea* area, QString& text, int n) {373area->insert_block(QString(n, ' '));374area->get_cursor().insertText(text.mid(n), get_type_format(area));375area->resize(area->sizeHint());376area->updateGeometry();377}378379void380Conversation::read_reply() {381auto data = debate()->server()->readAll();382QStringList strs = QString::fromLocal8Bit(data).split('\n');383QString prompt;384for (auto& s : strs) {385if (rx.indexIn(s) != -1) {386prompt = s;387continue;388}389auto tpos = tx.indexIn(s);390if (tpos != -1)391display_type(cur_out, s, tpos + tx.matchedLength());392else393cur_out->add_paragraph(s);394}395if (length() == 0) {396if (not empty_string(prompt))397ensure_visibility(debate(), new_topic());398}399else {400exchange()->adjustSize();401exchange()->update();402exchange()->updateGeometry();403if (empty_string(prompt))404ensure_visibility(debate(), exchange());405else {406ensure_visibility(debate(), next(exchange()));407QApplication::restoreOverrideCursor();408}409}410}411}412413414