Wt examples
3.3.0
|
00001 /* 00002 * Copyright (C) 2008 Emweb bvba, Heverlee, Belgium. 00003 * 00004 * See the LICENSE file for terms of use. 00005 */ 00006 00007 #include "SimpleChatWidget.h" 00008 #include "SimpleChatServer.h" 00009 00010 #include <Wt/WApplication> 00011 #include <Wt/WContainerWidget> 00012 #include <Wt/WEnvironment> 00013 #include <Wt/WHBoxLayout> 00014 #include <Wt/WVBoxLayout> 00015 #include <Wt/WLabel> 00016 #include <Wt/WLineEdit> 00017 #include <Wt/WText> 00018 #include <Wt/WTextArea> 00019 #include <Wt/WPushButton> 00020 #include <Wt/WCheckBox> 00021 00022 #include <iostream> 00023 00024 using namespace Wt; 00025 00026 SimpleChatWidget::SimpleChatWidget(SimpleChatServer& server, 00027 Wt::WContainerWidget *parent) 00028 : WContainerWidget(parent), 00029 server_(server), 00030 loggedIn_(false), 00031 userList_(0), 00032 messageReceived_(0) 00033 { 00034 user_ = server_.suggestGuest(); 00035 letLogin(); 00036 } 00037 00038 SimpleChatWidget::~SimpleChatWidget() 00039 { 00040 delete messageReceived_; 00041 logout(); 00042 disconnect(); 00043 } 00044 00045 void SimpleChatWidget::connect() 00046 { 00047 if (server_.connect 00048 (this, boost::bind(&SimpleChatWidget::processChatEvent, this, _1))) 00049 Wt::WApplication::instance()->enableUpdates(true); 00050 } 00051 00052 void SimpleChatWidget::disconnect() 00053 { 00054 if (server_.disconnect(this)) 00055 Wt::WApplication::instance()->enableUpdates(false); 00056 } 00057 00058 void SimpleChatWidget::letLogin() 00059 { 00060 disconnect(); 00061 00062 clear(); 00063 00064 WVBoxLayout *vLayout = new WVBoxLayout(); 00065 setLayout(vLayout); 00066 00067 WHBoxLayout *hLayout = new WHBoxLayout(); 00068 vLayout->addLayout(hLayout, 0, AlignTop | AlignLeft); 00069 00070 hLayout->addWidget(new WLabel("User name:"), 0, AlignMiddle); 00071 hLayout->addWidget(userNameEdit_ = new WLineEdit(user_), 0, AlignMiddle); 00072 userNameEdit_->setFocus(); 00073 00074 WPushButton *b = new WPushButton("Login"); 00075 hLayout->addWidget(b, 0, AlignMiddle); 00076 00077 b->clicked().connect(this, &SimpleChatWidget::login); 00078 userNameEdit_->enterPressed().connect(this, &SimpleChatWidget::login); 00079 00080 vLayout->addWidget(statusMsg_ = new WText()); 00081 statusMsg_->setTextFormat(PlainText); 00082 } 00083 00084 void SimpleChatWidget::login() 00085 { 00086 if (!loggedIn()) { 00087 WString name = userNameEdit_->text(); 00088 00089 if (!messageReceived_) 00090 messageReceived_ = new WSound("sounds/message_received.mp3"); 00091 00092 if (!startChat(name)) 00093 statusMsg_->setText("Sorry, name '" + escapeText(name) + 00094 "' is already taken."); 00095 } 00096 } 00097 00098 void SimpleChatWidget::logout() 00099 { 00100 if (loggedIn()) { 00101 loggedIn_ = false; 00102 server_.logout(user_); 00103 00104 letLogin(); 00105 } 00106 } 00107 00108 void SimpleChatWidget::createLayout(WWidget *messages, WWidget *userList, 00109 WWidget *messageEdit, 00110 WWidget *sendButton, WWidget *logoutButton) 00111 { 00112 /* 00113 * Create a vertical layout, which will hold 3 rows, 00114 * organized like this: 00115 * 00116 * WVBoxLayout 00117 * -------------------------------------------- 00118 * | nested WHBoxLayout (vertical stretch=1) | 00119 * | | | 00120 * | messages | userList | 00121 * | (horizontal stretch=1) | | 00122 * | | | 00123 * -------------------------------------------- 00124 * | message edit area | 00125 * -------------------------------------------- 00126 * | WHBoxLayout | 00127 * | send | logout | 00128 * -------------------------------------------- 00129 */ 00130 WVBoxLayout *vLayout = new WVBoxLayout(); 00131 00132 // Create a horizontal layout for the messages | userslist. 00133 WHBoxLayout *hLayout = new WHBoxLayout(); 00134 00135 // Add widget to horizontal layout with stretch = 1 00136 hLayout->addWidget(messages, 1); 00137 messages->setStyleClass("chat-msgs"); 00138 00139 // Add another widget to horizontal layout with stretch = 0 00140 hLayout->addWidget(userList); 00141 userList->setStyleClass("chat-users"); 00142 00143 hLayout->setResizable(0, true); 00144 00145 // Add nested layout to vertical layout with stretch = 1 00146 vLayout->addLayout(hLayout, 1); 00147 00148 // Add widget to vertical layout with stretch = 0 00149 vLayout->addWidget(messageEdit); 00150 messageEdit->setStyleClass("chat-noedit"); 00151 00152 // Create a horizontal layout for the buttons. 00153 hLayout = new WHBoxLayout(); 00154 00155 // Add button to horizontal layout with stretch = 0 00156 hLayout->addWidget(sendButton); 00157 00158 // Add button to horizontal layout with stretch = 0 00159 hLayout->addWidget(logoutButton); 00160 00161 // Add nested layout to vertical layout with stretch = 0 00162 vLayout->addLayout(hLayout, 0, AlignLeft); 00163 00164 setLayout(vLayout); 00165 } 00166 00167 bool SimpleChatWidget::loggedIn() const 00168 { 00169 return loggedIn_; 00170 } 00171 00172 void SimpleChatWidget::render(WFlags<RenderFlag> flags) 00173 { 00174 if (flags & RenderFull) { 00175 if (loggedIn()) { 00176 /* Handle a page refresh correctly */ 00177 messageEdit_->setText(WString::Empty); 00178 doJavaScript("setTimeout(function() { " 00179 + messages_->jsRef() + ".scrollTop += " 00180 + messages_->jsRef() + ".scrollHeight;}, 0);"); 00181 } 00182 } 00183 00184 WContainerWidget::render(flags); 00185 } 00186 00187 bool SimpleChatWidget::startChat(const WString& user) 00188 { 00189 /* 00190 * When logging in, we pass our processChatEvent method as the function that 00191 * is used to indicate a new chat event for this user. 00192 */ 00193 if (server_.login(user)) { 00194 loggedIn_ = true; 00195 connect(); 00196 00197 user_ = user; 00198 00199 clear(); 00200 userNameEdit_ = 0; 00201 00202 messages_ = new WContainerWidget(); 00203 userList_ = new WContainerWidget(); 00204 messageEdit_ = new WTextArea(); 00205 messageEdit_->setRows(2); 00206 messageEdit_->setFocus(); 00207 00208 // Display scroll bars if contents overflows 00209 messages_->setOverflow(WContainerWidget::OverflowAuto); 00210 userList_->setOverflow(WContainerWidget::OverflowAuto); 00211 00212 sendButton_ = new WPushButton("Send"); 00213 WPushButton *logoutButton = new WPushButton("Logout"); 00214 00215 createLayout(messages_, userList_, messageEdit_, sendButton_, logoutButton); 00216 00217 /* 00218 * Connect event handlers: 00219 * - click on button 00220 * - enter in text area 00221 * 00222 * We will clear the input field using a small custom client-side 00223 * JavaScript invocation. 00224 */ 00225 00226 // Create a JavaScript 'slot' (JSlot). The JavaScript slot always takes 00227 // 2 arguments: the originator of the event (in our case the 00228 // button or text area), and the JavaScript event object. 00229 clearInput_.setJavaScript 00230 ("function(o, e) { setTimeout(function() {" 00231 "" + messageEdit_->jsRef() + ".value='';" 00232 "}, 0); }"); 00233 00234 // Bind the C++ and JavaScript event handlers. 00235 sendButton_->clicked().connect(this, &SimpleChatWidget::send); 00236 messageEdit_->enterPressed().connect(this, &SimpleChatWidget::send); 00237 sendButton_->clicked().connect(clearInput_); 00238 messageEdit_->enterPressed().connect(clearInput_); 00239 sendButton_->clicked().connect(messageEdit_, &WLineEdit::setFocus); 00240 messageEdit_->enterPressed().connect(messageEdit_, &WLineEdit::setFocus); 00241 00242 // Prevent the enter from generating a new line, which is its default 00243 // action 00244 messageEdit_->enterPressed().preventDefaultAction(); 00245 00246 logoutButton->clicked().connect(this, &SimpleChatWidget::logout); 00247 00248 WText *msg = new WText 00249 ("<div><span class='chat-info'>You are joining as " 00250 + escapeText(user_) + ".</span></div>", 00251 messages_); 00252 msg->setStyleClass("chat-msg"); 00253 00254 if (!userList_->parent()) { 00255 delete userList_; 00256 userList_ = 0; 00257 } 00258 00259 if (!sendButton_->parent()) { 00260 delete sendButton_; 00261 sendButton_ = 0; 00262 } 00263 00264 if (!logoutButton->parent()) 00265 delete logoutButton; 00266 00267 updateUsers(); 00268 00269 return true; 00270 } else 00271 return false; 00272 } 00273 00274 void SimpleChatWidget::send() 00275 { 00276 if (!messageEdit_->text().empty()) 00277 server_.sendMessage(user_, messageEdit_->text()); 00278 } 00279 00280 void SimpleChatWidget::updateUsers() 00281 { 00282 if (userList_) { 00283 userList_->clear(); 00284 00285 SimpleChatServer::UserSet users = server_.users(); 00286 00287 UserMap oldUsers = users_; 00288 users_.clear(); 00289 00290 for (SimpleChatServer::UserSet::iterator i = users.begin(); 00291 i != users.end(); ++i) { 00292 WCheckBox *w = new WCheckBox(escapeText(*i), userList_); 00293 w->setInline(false); 00294 00295 UserMap::const_iterator j = oldUsers.find(*i); 00296 if (j != oldUsers.end()) 00297 w->setChecked(j->second); 00298 else 00299 w->setChecked(true); 00300 00301 users_[*i] = w->isChecked(); 00302 w->changed().connect(this, &SimpleChatWidget::updateUser); 00303 00304 if (*i == user_) 00305 w->setStyleClass("chat-self"); 00306 } 00307 } 00308 } 00309 00310 void SimpleChatWidget::newMessage() 00311 { } 00312 00313 void SimpleChatWidget::updateUser() 00314 { 00315 WCheckBox *b = dynamic_cast<WCheckBox *>(sender()); 00316 users_[b->text()] = b->isChecked(); 00317 } 00318 00319 void SimpleChatWidget::processChatEvent(const ChatEvent& event) 00320 { 00321 WApplication *app = WApplication::instance(); 00322 00323 /* 00324 * This is where the "server-push" happens. The chat server posts to this 00325 * event from other sessions, see SimpleChatServer::postChatEvent() 00326 */ 00327 00328 /* 00329 * Format and append the line to the conversation. 00330 * 00331 * This is also the step where the automatic XSS filtering will kick in: 00332 * - if another user tried to pass on some JavaScript, it is filtered away. 00333 * - if another user did not provide valid XHTML, the text is automatically 00334 * interpreted as PlainText 00335 */ 00336 00337 /* 00338 * If it is not a plain message, also update the user list. 00339 */ 00340 if (event.type() != ChatEvent::Message) { 00341 if (event.type() == ChatEvent::Rename && event.user() == user_) 00342 user_ = event.data(); 00343 00344 updateUsers(); 00345 } 00346 00347 newMessage(); 00348 00349 /* 00350 * Anything else doesn't matter if we are not logged in. 00351 */ 00352 if (!loggedIn()) { 00353 app->triggerUpdate(); 00354 return; 00355 } 00356 00357 bool display = event.type() != ChatEvent::Message 00358 || !userList_ 00359 || (users_.find(event.user()) != users_.end() && users_[event.user()]); 00360 00361 if (display) { 00362 WText *w = new WText(messages_); 00363 00364 /* 00365 * If it fails, it is because the content wasn't valid XHTML 00366 */ 00367 if (!w->setText(event.formattedHTML(user_, XHTMLText))) { 00368 w->setText(event.formattedHTML(user_, PlainText)); 00369 w->setTextFormat(XHTMLText); 00370 } 00371 00372 w->setInline(false); 00373 w->setStyleClass("chat-msg"); 00374 00375 /* 00376 * Leave no more than 100 messages in the back-log 00377 */ 00378 if (messages_->count() > 100) 00379 delete messages_->children()[0]; 00380 00381 /* 00382 * Little javascript trick to make sure we scroll along with new content 00383 */ 00384 app->doJavaScript(messages_->jsRef() + ".scrollTop += " 00385 + messages_->jsRef() + ".scrollHeight;"); 00386 00387 /* If this message belongs to another user, play a received sound */ 00388 if (event.user() != user_ && messageReceived_) 00389 messageReceived_->play(); 00390 } 00391 00392 /* 00393 * This is the server push action: we propagate the updated UI to the client, 00394 * (when the event was triggered by another user) 00395 */ 00396 app->triggerUpdate(); 00397 }