Chat console, including a number of rebases and modifications.

Defaults modified from original: alpha=200, key=F10
This commit is contained in:
Kahrl 2011-12-03 09:01:14 +01:00 committed by Perttu Ahola
parent 0053651814
commit 967f25461b
17 changed files with 1941 additions and 146 deletions

@ -277,3 +277,21 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
Fonts
---------------
DejaVu Sans Mono:
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
Bitstream Vera Fonts Copyright:
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Arev Fonts Copyright:
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.

@ -42,6 +42,8 @@
# Go down ladder / go down in fly mode / go fast in fast mode
#keymap_special1 = KEY_KEY_E
#keymap_chat = KEY_KEY_T
#keymap_cmd = /
#keyman_console = KEY_F10
#keymap_rangeselect = KEY_KEY_R
#keymap_freemove = KEY_KEY_K
#keymap_fastmove = KEY_KEY_J
@ -104,6 +106,10 @@
#screenshot_path = .
# Amount of view bobbing (0 = no view bobbing, 1.0 = normal, 2.0 = double)
#view_bobbing_amount = 1.0
# In-game chat console background color (R,G,B)
#console_color = (0,0,0)
# In-game chat console background alpha (opaqueness, between 0 and 255)
#console_alpha = 200
#
# Server stuff

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@ -167,6 +167,7 @@ set(minetest_SRCS
camera.cpp
clouds.cpp
clientobject.cpp
chat.cpp
guiMainMenu.cpp
guiKeyChangeMenu.cpp
guiMessageMenu.cpp
@ -175,6 +176,7 @@ set(minetest_SRCS
guiPauseMenu.cpp
guiPasswordChange.cpp
guiDeathScreen.cpp
guiChatConsole.cpp
client.cpp
tile.cpp
game.cpp

768
src/chat.cpp Normal file

@ -0,0 +1,768 @@
/*
Minetest-c55
Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "chat.h"
#include "debug.h"
#include "utility.h"
#include <cassert>
#include <cctype>
#include <sstream>
ChatBuffer::ChatBuffer(u32 scrollback):
m_scrollback(scrollback),
m_unformatted(),
m_cols(0),
m_rows(0),
m_scroll(0),
m_formatted(),
m_empty_formatted_line()
{
if (m_scrollback == 0)
m_scrollback = 1;
m_empty_formatted_line.first = true;
}
ChatBuffer::~ChatBuffer()
{
}
void ChatBuffer::addLine(std::wstring name, std::wstring text)
{
ChatLine line(name, text);
m_unformatted.push_back(line);
if (m_rows > 0)
{
// m_formatted is valid and must be kept valid
bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
u32 num_added = formatChatLine(line, m_cols, m_formatted);
if (scrolled_at_bottom)
m_scroll += num_added;
}
// Limit number of lines by m_scrollback
if (m_unformatted.size() > m_scrollback)
{
deleteOldest(m_unformatted.size() - m_scrollback);
}
}
void ChatBuffer::clear()
{
m_unformatted.clear();
m_formatted.clear();
m_scroll = 0;
}
u32 ChatBuffer::getLineCount() const
{
return m_unformatted.size();
}
u32 ChatBuffer::getScrollback() const
{
return m_scrollback;
}
const ChatLine& ChatBuffer::getLine(u32 index) const
{
assert(index < getLineCount());
return m_unformatted[index];
}
void ChatBuffer::step(f32 dtime)
{
for (u32 i = 0; i < m_unformatted.size(); ++i)
{
m_unformatted[i].age += dtime;
}
}
void ChatBuffer::deleteOldest(u32 count)
{
u32 del_unformatted = 0;
u32 del_formatted = 0;
while (count > 0 && del_unformatted < m_unformatted.size())
{
++del_unformatted;
// keep m_formatted in sync
if (del_formatted < m_formatted.size())
{
assert(m_formatted[del_formatted].first);
++del_formatted;
while (del_formatted < m_formatted.size() &&
!m_formatted[del_formatted].first)
++del_formatted;
}
--count;
}
m_unformatted.erase(0, del_unformatted);
m_formatted.erase(0, del_formatted);
}
void ChatBuffer::deleteByAge(f32 maxAge)
{
u32 count = 0;
while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
++count;
deleteOldest(count);
}
u32 ChatBuffer::getColumns() const
{
return m_cols;
}
u32 ChatBuffer::getRows() const
{
return m_rows;
}
void ChatBuffer::reformat(u32 cols, u32 rows)
{
if (cols == 0 || rows == 0)
{
// Clear formatted buffer
m_cols = 0;
m_rows = 0;
m_scroll = 0;
m_formatted.clear();
}
else if (cols != m_cols || rows != m_rows)
{
// TODO: Avoid reformatting ALL lines (even inivisble ones)
// each time the console size changes.
// Find out the scroll position in *unformatted* lines
u32 restore_scroll_unformatted = 0;
u32 restore_scroll_formatted = 0;
bool at_bottom = (m_scroll == getBottomScrollPos());
if (!at_bottom)
{
for (s32 i = 0; i < m_scroll; ++i)
{
if (m_formatted[i].first)
++restore_scroll_unformatted;
}
}
// If number of columns change, reformat everything
if (cols != m_cols)
{
m_formatted.clear();
for (u32 i = 0; i < m_unformatted.size(); ++i)
{
if (i == restore_scroll_unformatted)
restore_scroll_formatted = m_formatted.size();
formatChatLine(m_unformatted[i], cols, m_formatted);
}
}
// Update the console size
m_cols = cols;
m_rows = rows;
// Restore the scroll position
if (at_bottom)
{
scrollBottom();
}
else
{
scrollAbsolute(restore_scroll_formatted);
}
}
}
const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
{
s32 index = m_scroll + (s32) row;
if (index >= 0 && index < (s32) m_formatted.size())
return m_formatted[index];
else
return m_empty_formatted_line;
}
void ChatBuffer::scroll(s32 rows)
{
scrollAbsolute(m_scroll + rows);
}
void ChatBuffer::scrollAbsolute(s32 scroll)
{
s32 top = getTopScrollPos();
s32 bottom = getBottomScrollPos();
m_scroll = scroll;
if (m_scroll < top)
m_scroll = top;
if (m_scroll > bottom)
m_scroll = bottom;
}
void ChatBuffer::scrollBottom()
{
m_scroll = getBottomScrollPos();
}
void ChatBuffer::scrollTop()
{
m_scroll = getTopScrollPos();
}
u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
core::array<ChatFormattedLine>& destination) const
{
u32 num_added = 0;
core::array<ChatFormattedFragment> next_frags;
ChatFormattedLine next_line;
ChatFormattedFragment temp_frag;
u32 out_column = 0;
u32 in_pos = 0;
u32 hanging_indentation = 0;
// Format the sender name and produce fragments
if (!line.name.empty())
{
temp_frag.text = L"<";
temp_frag.column = 0;
//temp_frag.bold = 0;
next_frags.push_back(temp_frag);
temp_frag.text = line.name;
temp_frag.column = 0;
//temp_frag.bold = 1;
next_frags.push_back(temp_frag);
temp_frag.text = L"> ";
temp_frag.column = 0;
//temp_frag.bold = 0;
next_frags.push_back(temp_frag);
}
// Choose an indentation level
if (line.name.empty())
{
// Server messages
hanging_indentation = 0;
}
else if (line.name.size() + 3 <= cols/2)
{
// Names shorter than about half the console width
hanging_indentation = line.name.size() + 3;
}
else
{
// Very long names
hanging_indentation = 2;
}
next_line.first = true;
bool text_processing = false;
// Produce fragments and layout them into lines
while (!next_frags.empty() || in_pos < line.text.size())
{
// Layout fragments into lines
while (!next_frags.empty())
{
ChatFormattedFragment& frag = next_frags[0];
if (frag.text.size() <= cols - out_column)
{
// Fragment fits into current line
frag.column = out_column;
next_line.fragments.push_back(frag);
out_column += frag.text.size();
next_frags.erase(0, 1);
}
else
{
// Fragment does not fit into current line
// So split it up
temp_frag.text = frag.text.substr(0, cols - out_column);
temp_frag.column = out_column;
//temp_frag.bold = frag.bold;
next_line.fragments.push_back(temp_frag);
frag.text = frag.text.substr(cols - out_column);
out_column = cols;
}
if (out_column == cols || text_processing)
{
// End the current line
destination.push_back(next_line);
num_added++;
next_line.fragments.clear();
next_line.first = false;
out_column = text_processing ? hanging_indentation : 0;
}
}
// Produce fragment
if (in_pos < line.text.size())
{
u32 remaining_in_input = line.text.size() - in_pos;
u32 remaining_in_output = cols - out_column;
// Determine a fragment length <= the minimum of
// remaining_in_{in,out}put. Try to end the fragment
// on a word boundary.
u32 frag_length = 1, space_pos = 0;
while (frag_length < remaining_in_input &&
frag_length < remaining_in_output)
{
if (isspace(line.text[in_pos + frag_length]))
space_pos = frag_length;
++frag_length;
}
if (space_pos != 0 && frag_length < remaining_in_input)
frag_length = space_pos + 1;
temp_frag.text = line.text.substr(in_pos, frag_length);
temp_frag.column = 0;
//temp_frag.bold = 0;
next_frags.push_back(temp_frag);
in_pos += frag_length;
text_processing = true;
}
}
// End the last line
if (num_added == 0 || !next_line.fragments.empty())
{
destination.push_back(next_line);
num_added++;
}
return num_added;
}
s32 ChatBuffer::getTopScrollPos() const
{
s32 formatted_count = (s32) m_formatted.size();
s32 rows = (s32) m_rows;
if (rows == 0)
return 0;
else if (formatted_count <= rows)
return formatted_count - rows;
else
return 0;
}
s32 ChatBuffer::getBottomScrollPos() const
{
s32 formatted_count = (s32) m_formatted.size();
s32 rows = (s32) m_rows;
if (rows == 0)
return 0;
else
return formatted_count - rows;
}
ChatPrompt::ChatPrompt(std::wstring prompt, u32 history_limit):
m_prompt(prompt),
m_line(L""),
m_history(),
m_history_index(0),
m_history_limit(history_limit),
m_cols(0),
m_view(0),
m_cursor(0),
m_nick_completion_start(0),
m_nick_completion_end(0)
{
}
ChatPrompt::~ChatPrompt()
{
}
void ChatPrompt::input(wchar_t ch)
{
m_line.insert(m_cursor, 1, ch);
m_cursor++;
clampView();
m_nick_completion_start = 0;
m_nick_completion_end = 0;
}
std::wstring ChatPrompt::submit()
{
std::wstring line = m_line;
m_line.clear();
if (!line.empty())
m_history.push_back(line);
if (m_history.size() > m_history_limit)
m_history.erase(0);
m_history_index = m_history.size();
m_view = 0;
m_cursor = 0;
m_nick_completion_start = 0;
m_nick_completion_end = 0;
return line;
}
void ChatPrompt::clear()
{
m_line.clear();
m_view = 0;
m_cursor = 0;
m_nick_completion_start = 0;
m_nick_completion_end = 0;
}
void ChatPrompt::replace(std::wstring line)
{
m_line = line;
m_view = m_cursor = line.size();
clampView();
m_nick_completion_start = 0;
m_nick_completion_end = 0;
}
void ChatPrompt::historyPrev()
{
if (m_history_index != 0)
{
--m_history_index;
replace(m_history[m_history_index]);
}
}
void ChatPrompt::historyNext()
{
if (m_history_index + 1 >= m_history.size())
{
m_history_index = m_history.size();
replace(L"");
}
else
{
++m_history_index;
replace(m_history[m_history_index]);
}
}
void ChatPrompt::nickCompletion(const core::list<std::wstring>& names, bool backwards)
{
// Two cases:
// (a) m_nick_completion_start == m_nick_completion_end == 0
// Then no previous nick completion is active.
// Get the word around the cursor and replace with any nick
// that has that word as a prefix.
// (b) else, continue a previous nick completion.
// m_nick_completion_start..m_nick_completion_end are the
// interval where the originally used prefix was. Cycle
// through the list of completions of that prefix.
u32 prefix_start = m_nick_completion_start;
u32 prefix_end = m_nick_completion_end;
bool initial = (prefix_end == 0);
if (initial)
{
// no previous nick completion is active
prefix_start = prefix_end = m_cursor;
while (prefix_start > 0 && !isspace(m_line[prefix_start-1]))
--prefix_start;
while (prefix_end < m_line.size() && !isspace(m_line[prefix_end]))
++prefix_end;
if (prefix_start == prefix_end)
return;
}
std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
// find all names that start with the selected prefix
core::array<std::wstring> completions;
for (core::list<std::wstring>::ConstIterator
i = names.begin();
i != names.end(); i++)
{
if (str_starts_with(*i, prefix, true))
{
std::wstring completion = *i;
if (prefix_start == 0)
completion += L":";
completions.push_back(completion);
}
}
if (completions.empty())
return;
// find a replacement string and the word that will be replaced
u32 word_end = prefix_end;
u32 replacement_index = 0;
if (!initial)
{
while (word_end < m_line.size() && !isspace(m_line[word_end]))
++word_end;
std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
// cycle through completions
for (u32 i = 0; i < completions.size(); ++i)
{
if (str_equal(word, completions[i], true))
{
if (backwards)
replacement_index = i + completions.size() - 1;
else
replacement_index = i + 1;
replacement_index %= completions.size();
break;
}
}
}
std::wstring replacement = completions[replacement_index] + L" ";
if (word_end < m_line.size() && isspace(word_end))
++word_end;
// replace existing word with replacement word,
// place the cursor at the end and record the completion prefix
m_line.replace(prefix_start, word_end - prefix_start, replacement);
m_cursor = prefix_start + replacement.size();
clampView();
m_nick_completion_start = prefix_start;
m_nick_completion_end = prefix_end;
}
void ChatPrompt::reformat(u32 cols)
{
if (cols <= m_prompt.size())
{
m_cols = 0;
m_view = m_cursor;
}
else
{
s32 length = m_line.size();
bool was_at_end = (m_view + m_cols >= length + 1);
m_cols = cols - m_prompt.size();
if (was_at_end)
m_view = length;
clampView();
}
}
std::wstring ChatPrompt::getVisiblePortion() const
{
return m_prompt + m_line.substr(m_view, m_cols);
}
s32 ChatPrompt::getVisibleCursorPosition() const
{
return m_cursor - m_view + m_prompt.size();
}
void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
{
s32 old_cursor = m_cursor;
s32 new_cursor = m_cursor;
s32 length = m_line.size();
s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
if (scope == CURSOROP_SCOPE_CHARACTER)
{
new_cursor += increment;
}
else if (scope == CURSOROP_SCOPE_WORD)
{
if (increment > 0)
{
// skip one word to the right
while (new_cursor < length && isspace(m_line[new_cursor]))
new_cursor++;
while (new_cursor < length && !isspace(m_line[new_cursor]))
new_cursor++;
while (new_cursor < length && isspace(m_line[new_cursor]))
new_cursor++;
}
else
{
// skip one word to the left
while (new_cursor >= 1 && isspace(m_line[new_cursor - 1]))
new_cursor--;
while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1]))
new_cursor--;
}
}
else if (scope == CURSOROP_SCOPE_LINE)
{
new_cursor += increment * length;
}
new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
if (op == CURSOROP_MOVE)
{
m_cursor = new_cursor;
}
else if (op == CURSOROP_DELETE)
{
if (new_cursor < old_cursor)
{
m_line.erase(new_cursor, old_cursor - new_cursor);
m_cursor = new_cursor;
}
else if (new_cursor > old_cursor)
{
m_line.erase(old_cursor, new_cursor - old_cursor);
m_cursor = old_cursor;
}
}
clampView();
m_nick_completion_start = 0;
m_nick_completion_end = 0;
}
void ChatPrompt::clampView()
{
s32 length = m_line.size();
if (length + 1 <= m_cols)
{
m_view = 0;
}
else
{
m_view = MYMIN(m_view, length + 1 - m_cols);
m_view = MYMIN(m_view, m_cursor);
m_view = MYMAX(m_view, m_cursor - m_cols + 1);
m_view = MYMAX(m_view, 0);
}
}
ChatBackend::ChatBackend():
m_console_buffer(500),
m_recent_buffer(6),
m_prompt(L"]", 500)
{
}
ChatBackend::~ChatBackend()
{
}
void ChatBackend::addMessage(std::wstring name, std::wstring text)
{
// Note: A message may consist of multiple lines, for example the MOTD.
WStrfnd fnd(text);
while (!fnd.atend())
{
std::wstring line = fnd.next(L"\n");
m_console_buffer.addLine(name, line);
m_recent_buffer.addLine(name, line);
}
}
void ChatBackend::addUnparsedMessage(std::wstring message)
{
// TODO: Remove the need to parse chat messages client-side, by sending
// separate name and text fields in TOCLIENT_CHAT_MESSAGE.
if (message.size() >= 2 && message[0] == L'<')
{
std::size_t closing = message.find_first_of(L'>', 1);
if (closing != std::wstring::npos &&
closing + 2 <= message.size() &&
message[closing+1] == L' ')
{
std::wstring name = message.substr(1, closing - 1);
std::wstring text = message.substr(closing + 2);
addMessage(name, text);
return;
}
}
// Unable to parse, probably a server message.
addMessage(L"", message);
}
ChatBuffer& ChatBackend::getConsoleBuffer()
{
return m_console_buffer;
}
ChatBuffer& ChatBackend::getRecentBuffer()
{
return m_recent_buffer;
}
std::wstring ChatBackend::getRecentChat()
{
std::wostringstream stream;
for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i)
{
const ChatLine& line = m_recent_buffer.getLine(i);
if (i != 0)
stream << L"\n";
if (!line.name.empty())
stream << L"<" << line.name << L"> ";
stream << line.text;
}
return stream.str();
}
ChatPrompt& ChatBackend::getPrompt()
{
return m_prompt;
}
void ChatBackend::reformat(u32 cols, u32 rows)
{
m_console_buffer.reformat(cols, rows);
// no need to reformat m_recent_buffer, its formatted lines
// are not used
m_prompt.reformat(cols);
}
void ChatBackend::clearRecentChat()
{
m_recent_buffer.clear();
}
void ChatBackend::step(float dtime)
{
m_recent_buffer.step(dtime);
m_recent_buffer.deleteByAge(60.0);
// no need to age messages in anything but m_recent_buffer
}
void ChatBackend::scroll(s32 rows)
{
m_console_buffer.scroll(rows);
}
void ChatBackend::scrollPageDown()
{
m_console_buffer.scroll(m_console_buffer.getRows());
}
void ChatBackend::scrollPageUp()
{
m_console_buffer.scroll(-m_console_buffer.getRows());
}

272
src/chat.h Normal file

@ -0,0 +1,272 @@
/*
Minetest-c55
Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef CHAT_HEADER
#define CHAT_HEADER
#include "common_irrlicht.h"
#include <string>
// Chat console related classes, only used by the client
struct ChatLine
{
// age in seconds
f32 age;
// name of sending player, or empty if sent by server
std::wstring name;
// message text
std::wstring text;
ChatLine(std::wstring a_name, std::wstring a_text):
age(0.0),
name(a_name),
text(a_text)
{
}
};
struct ChatFormattedFragment
{
// text string
std::wstring text;
// starting column
u32 column;
// formatting
//u8 bold:1;
};
struct ChatFormattedLine
{
// Array of text fragments
core::array<ChatFormattedFragment> fragments;
// true if first line of one formatted ChatLine
bool first;
};
class ChatBuffer
{
public:
ChatBuffer(u32 scrollback);
~ChatBuffer();
// Append chat line
// Removes oldest chat line if scrollback size is reached
void addLine(std::wstring name, std::wstring text);
// Remove all chat lines
void clear();
// Get number of lines currently in buffer.
u32 getLineCount() const;
// Get scrollback size, maximum number of lines in buffer.
u32 getScrollback() const;
// Get reference to i-th chat line.
const ChatLine& getLine(u32 index) const;
// Increase each chat line's age by dtime.
void step(f32 dtime);
// Delete oldest N chat lines.
void deleteOldest(u32 count);
// Delete lines older than maxAge.
void deleteByAge(f32 maxAge);
// Get number of columns, 0 if reformat has not been called yet.
u32 getColumns() const;
// Get number of rows, 0 if reformat has not been called yet.
u32 getRows() const;
// Update console size and reformat all formatted lines.
void reformat(u32 cols, u32 rows);
// Get formatted line for a given row (0 is top of screen).
// Only valid after reformat has been called at least once
const ChatFormattedLine& getFormattedLine(u32 row) const;
// Scrolling in formatted buffer (relative)
// positive rows == scroll up, negative rows == scroll down
void scroll(s32 rows);
// Scrolling in formatted buffer (absolute)
void scrollAbsolute(s32 scroll);
// Scroll to bottom of buffer (newest)
void scrollBottom();
// Scroll to top of buffer (oldest)
void scrollTop();
// Format a chat line for the given number of columns.
// Appends the formatted lines to the destination array and
// returns the number of formatted lines.
u32 formatChatLine(const ChatLine& line, u32 cols,
core::array<ChatFormattedLine>& destination) const;
protected:
s32 getTopScrollPos() const;
s32 getBottomScrollPos() const;
private:
// Scrollback size
u32 m_scrollback;
// Array of unformatted chat lines
core::array<ChatLine> m_unformatted;
// Number of character columns in console
u32 m_cols;
// Number of character rows in console
u32 m_rows;
// Scroll position (console's top line index into m_formatted)
s32 m_scroll;
// Array of formatted lines
core::array<ChatFormattedLine> m_formatted;
// Empty formatted line, for error returns
ChatFormattedLine m_empty_formatted_line;
};
class ChatPrompt
{
public:
ChatPrompt(std::wstring prompt, u32 history_limit);
~ChatPrompt();
// Input character
void input(wchar_t ch);
// Submit, clear and return current line
std::wstring submit();
// Clear the current line
void clear();
// Replace the current line with the given text
void replace(std::wstring line);
// Select previous command from history
void historyPrev();
// Select next command from history
void historyNext();
// Nick completion
void nickCompletion(const core::list<std::wstring>& names, bool backwards);
// Update console size and reformat the visible portion of the prompt
void reformat(u32 cols);
// Get visible portion of the prompt.
std::wstring getVisiblePortion() const;
// Get cursor position (relative to visible portion). -1 if invalid
s32 getVisibleCursorPosition() const;
// Cursor operations
enum CursorOp {
CURSOROP_MOVE,
CURSOROP_DELETE
};
// Cursor operation direction
enum CursorOpDir {
CURSOROP_DIR_LEFT,
CURSOROP_DIR_RIGHT
};
// Cursor operation scope
enum CursorOpScope {
CURSOROP_SCOPE_CHARACTER,
CURSOROP_SCOPE_WORD,
CURSOROP_SCOPE_LINE
};
// Cursor operation
// op specifies whether it's a move or delete operation
// dir specifies whether the operation goes left or right
// scope specifies how far the operation will reach (char/word/line)
// Examples:
// cursorOperation(CURSOROP_MOVE, CURSOROP_DIR_RIGHT, CURSOROP_SCOPE_LINE)
// moves the cursor to the end of the line.
// cursorOperation(CURSOROP_DELETE, CURSOROP_DIR_LEFT, CURSOROP_SCOPE_WORD)
// deletes the word to the left of the cursor.
void cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope);
protected:
// set m_view to ensure that 0 <= m_view <= m_cursor < m_view + m_cols
// if line can be fully shown, set m_view to zero
// else, also ensure m_view <= m_line.size() + 1 - m_cols
void clampView();
private:
// Prompt prefix
std::wstring m_prompt;
// Currently edited line
std::wstring m_line;
// History buffer
core::array<std::wstring> m_history;
// History index (0 <= m_history_index <= m_history.size())
u32 m_history_index;
// Maximum number of history entries
u32 m_history_limit;
// Number of columns excluding columns reserved for the prompt
s32 m_cols;
// Start of visible portion (index into m_line)
s32 m_view;
// Cursor (index into m_line)
s32 m_cursor;
// Last nick completion start (index into m_line)
s32 m_nick_completion_start;
// Last nick completion start (index into m_line)
s32 m_nick_completion_end;
};
class ChatBackend
{
public:
ChatBackend();
~ChatBackend();
// Add chat message
void addMessage(std::wstring name, std::wstring text);
// Parse and add unparsed chat message
void addUnparsedMessage(std::wstring line);
// Get the console buffer
ChatBuffer& getConsoleBuffer();
// Get the recent messages buffer
ChatBuffer& getRecentBuffer();
// Concatenate all recent messages
std::wstring getRecentChat();
// Get the console prompt
ChatPrompt& getPrompt();
// Reformat all buffers
void reformat(u32 cols, u32 rows);
// Clear all recent messages
void clearRecentChat();
// Age recent messages
void step(float dtime);
// Scrolling
void scroll(s32 rows);
void scrollPageDown();
void scrollPageUp();
private:
ChatBuffer m_console_buffer;
ChatBuffer m_recent_buffer;
ChatPrompt m_prompt;
};
#endif

@ -2037,6 +2037,20 @@ void Client::printDebugInfo(std::ostream &os)
<<std::endl;*/
}
core::list<std::wstring> Client::getConnectedPlayerNames()
{
core::list<Player*> players = m_env.getPlayers(true);
core::list<std::wstring> playerNames;
for(core::list<Player*>::Iterator
i = players.begin();
i != players.end(); i++)
{
Player *player = *i;
playerNames.push_back(narrow_to_wide(player->getName()));
}
return playerNames;
}
u32 Client::getDayNightRatio()
{
//JMutexAutoLock envlock(m_env_mutex); //bulk comment-out
@ -2084,6 +2098,39 @@ void Client::clearTempMod(v3s16 p)
}
}
bool Client::getChatMessage(std::wstring &message)
{
if(m_chat_queue.size() == 0)
return false;
message = m_chat_queue.pop_front();
return true;
}
void Client::typeChatMessage(const std::wstring &message)
{
// Discard empty line
if(message == L"")
return;
// Send to others
sendChatMessage(message);
// Show locally
if (message[0] == L'/')
{
m_chat_queue.push_back(
(std::wstring)L"issued command: "+message);
}
else
{
LocalPlayer *player = m_env.getLocalPlayer();
assert(player != NULL);
std::wstring name = narrow_to_wide(player->getName());
m_chat_queue.push_back(
(std::wstring)L"<"+name+L"> "+message);
}
}
void Client::addUpdateMeshTask(v3s16 p, bool ack_to_server)
{
/*infostream<<"Client::addUpdateMeshTask(): "

@ -258,6 +258,8 @@ public:
// Prints a line or two of info
void printDebugInfo(std::ostream &os);
core::list<std::wstring> getConnectedPlayerNames();
u32 getDayNightRatio();
u16 getHP();
@ -274,29 +276,8 @@ public:
}
}
bool getChatMessage(std::wstring &message)
{
if(m_chat_queue.size() == 0)
return false;
message = m_chat_queue.pop_front();
return true;
}
void addChatMessage(const std::wstring &message)
{
if (message[0] == L'/') {
m_chat_queue.push_back(
(std::wstring)L"issued command: "+message);
return;
}
//JMutexAutoLock envlock(m_env_mutex); //bulk comment-out
LocalPlayer *player = m_env.getLocalPlayer();
assert(player != NULL);
std::wstring name = narrow_to_wide(player->getName());
m_chat_queue.push_back(
(std::wstring)L"<"+name+L"> "+message);
}
bool getChatMessage(std::wstring &message);
void typeChatMessage(const std::wstring& message);
u64 getMapSeed(){ return m_map_seed; }

@ -40,6 +40,7 @@ void set_default_settings(Settings *settings)
settings->setDefault("keymap_special1", "KEY_KEY_E");
settings->setDefault("keymap_chat", "KEY_KEY_T");
settings->setDefault("keymap_cmd", "/");
settings->setDefault("keymap_console", "KEY_F10");
settings->setDefault("keymap_rangeselect", "KEY_KEY_R");
settings->setDefault("keymap_freemove", "KEY_KEY_K");
settings->setDefault("keymap_fastmove", "KEY_KEY_J");
@ -91,7 +92,8 @@ void set_default_settings(Settings *settings)
settings->setDefault("view_bobbing_amount", "1.0");
settings->setDefault("enable_3d_clouds", "false");
settings->setDefault("opaque_water", "false");
settings->setDefault("console_color", "(0,0,0)");
settings->setDefault("console_alpha", "200");
// Server stuff
// "map-dir" doesn't exist by default.
settings->setDefault("motd", "");

@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "guiTextInputMenu.h"
#include "guiDeathScreen.h"
#include "tool.h"
#include "guiChatConsole.h"
#include "config.h"
#include "clouds.h"
#include "camera.h"
@ -62,22 +63,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#define FIELD_OF_VIEW_TEST 0
// Chat data
struct ChatLine
{
ChatLine():
age(0.0)
{
}
ChatLine(const std::wstring &a_text):
age(0.0),
text(a_text)
{
}
float age;
std::wstring text;
};
/*
Text input system
*/
@ -90,14 +75,7 @@ struct TextDestChat : public TextDest
}
void gotText(std::wstring text)
{
// Discard empty line
if(text == L"")
return;
// Send to others
m_client->sendChatMessage(text);
// Show locally
m_client->addChatMessage(text);
m_client->typeChatMessage(text);
}
Client *m_client;
@ -676,7 +654,8 @@ void the_game(
std::string address,
u16 port,
std::wstring &error_message,
std::string configpath
std::string configpath,
ChatBackend &chat_backend
)
{
video::IVideoDriver* driver = device->getVideoDriver();
@ -978,8 +957,10 @@ void the_game(
core::rect<s32>(0,0,0,0),
//false, false); // Disable word wrap as of now
false, true);
//guitext_chat->setBackgroundColor(video::SColor(96,0,0,0));
core::list<ChatLine> chat_lines;
// Remove stale "recent" chat messages from previous connections
chat_backend.clearRecentChat();
// Chat backend and console
GUIChatConsole *gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(), -1, &chat_backend, &client);
// Profiler text (size is updated when text is updated)
gui::IGUIStaticText *guitext_profiler = guienv->addStaticText(
@ -1299,7 +1280,9 @@ void the_game(
*/
// Reset input if window not active or some menu is active
if(device->isWindowActive() == false || noMenuActive() == false)
if(device->isWindowActive() == false
|| noMenuActive() == false
|| guienv->hasFocus(gui_chat_console))
{
input->clear();
}
@ -1375,6 +1358,15 @@ void the_game(
&g_menumgr, dest,
L"/"))->drop();
}
else if(input->wasKeyDown(getKeySetting("keymap_console")))
{
if (!gui_chat_console->isOpenInhibited())
{
// Open up to over half of the screen
gui_chat_console->openConsole(0.6);
guienv->setFocus(gui_chat_console);
}
}
else if(input->wasKeyDown(getKeySetting("keymap_freemove")))
{
if(g_settings->getBool("free_move"))
@ -1655,23 +1647,6 @@ void the_game(
/*
Player speed control
*/
if(!noMenuActive() || !device->isWindowActive())
{
PlayerControl control(
false,
false,
false,
false,
false,
false,
false,
camera_pitch,
camera_yaw
);
client.setPlayerControl(control);
}
else
{
/*bool a_up,
bool a_down,
@ -1758,6 +1733,8 @@ void the_game(
&g_menumgr, respawner);
menu->drop();
chat_backend.addMessage(L"", L"You died.");
/* Handle visualization */
damage_flash_timer = 0;
@ -2357,68 +2334,24 @@ void the_game(
// Get new messages from error log buffer
while(!chat_log_error_buf.empty())
{
chat_lines.push_back(ChatLine(narrow_to_wide(
chat_log_error_buf.get())));
chat_backend.addMessage(L"", narrow_to_wide(
chat_log_error_buf.get()));
}
// Get new messages from client
std::wstring message;
while(client.getChatMessage(message))
{
chat_lines.push_back(ChatLine(message));
/*if(chat_lines.size() > 6)
{
core::list<ChatLine>::Iterator
i = chat_lines.begin();
chat_lines.erase(i);
}*/
chat_backend.addUnparsedMessage(message);
}
// Append them to form the whole static text and throw
// it to the gui element
std::wstring whole;
// This will correspond to the line number counted from
// top to bottom, from size-1 to 0
s16 line_number = chat_lines.size();
// Count of messages to be removed from the top
u16 to_be_removed_count = 0;
for(core::list<ChatLine>::Iterator
i = chat_lines.begin();
i != chat_lines.end(); i++)
{
// After this, line number is valid for this loop
line_number--;
// Increment age
(*i).age += dtime;
/*
This results in a maximum age of 60*6 to the
lowermost line and a maximum of 6 lines
*/
float allowed_age = (6-line_number) * 60.0;
// Remove old messages
chat_backend.step(dtime);
if((*i).age > allowed_age)
{
to_be_removed_count++;
continue;
}
whole += (*i).text + L'\n';
}
for(u16 i=0; i<to_be_removed_count; i++)
{
core::list<ChatLine>::Iterator
it = chat_lines.begin();
chat_lines.erase(it);
}
guitext_chat->setText(whole.c_str());
// Display all messages in a static text element
u32 recent_chat_count = chat_backend.getRecentBuffer().getLineCount();
std::wstring recent_chat = chat_backend.getRecentChat();
guitext_chat->setText(recent_chat.c_str());
// Update gui element size and position
/*core::rect<s32> rect(
10,
screensize.Y - guitext_chat_pad_bottom
- text_height*chat_lines.size(),
screensize.X - 10,
screensize.Y - guitext_chat_pad_bottom
);*/
s32 chat_y = 5+(text_height+5);
if(show_debug)
chat_y += (text_height+5);
@ -2428,12 +2361,11 @@ void the_game(
screensize.X - 10,
chat_y + guitext_chat->getTextHeight()
);
guitext_chat->setRelativePosition(rect);
// Don't show chat if empty or profiler or debug is enabled
guitext_chat->setVisible(chat_lines.size() != 0
&& show_chat && show_profiler == 0);
// Don't show chat if disabled or empty or profiler is enabled
guitext_chat->setVisible(show_chat && recent_chat_count != 0
&& !show_profiler);
}
/*
@ -2634,6 +2566,8 @@ void the_game(
*/
if(clouds)
clouds->drop();
if(gui_chat_console)
gui_chat_console->drop();
/*
Draw a "shutting down" screen, which will be shown while the map
@ -2648,6 +2582,9 @@ void the_game(
gui_shuttingdowntext->remove();*/
}
chat_backend.addMessage(L"", L"# Disconnected.");
chat_backend.addMessage(L"", L"");
} // Client scope (must be destructed before destructing *def and tsrc
delete tsrc;

@ -122,6 +122,8 @@ public:
virtual void clear() {};
};
class ChatBackend; /* to avoid having to include chat.h */
void the_game(
bool &kill,
bool random_input,
@ -134,7 +136,8 @@ void the_game(
std::string address,
u16 port,
std::wstring &error_message,
std::string configpath
std::string configpath,
ChatBackend &chat_backend
);
#endif

550
src/guiChatConsole.cpp Normal file

@ -0,0 +1,550 @@
/*
Minetest-c55
Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "guiChatConsole.h"
#include "chat.h"
#include "client.h"
#include "debug.h"
#include "gettime.h"
#include "keycode.h"
#include "settings.h"
#include "main.h" // for g_settings
#include "porting.h"
#include "tile.h"
#include "IGUIFont.h"
#include <string>
#include "gettext.h"
inline u32 clamp_u8(s32 value)
{
return (u32) MYMIN(MYMAX(value, 0), 255);
}
GUIChatConsole::GUIChatConsole(
gui::IGUIEnvironment* env,
gui::IGUIElement* parent,
s32 id,
ChatBackend* backend,
Client* client
):
IGUIElement(gui::EGUIET_ELEMENT, env, parent, id,
core::rect<s32>(0,0,100,100)),
m_chat_backend(backend),
m_client(client),
m_screensize(v2u32(0,0)),
m_animate_time_old(0),
m_open(false),
m_height(0),
m_desired_height(0),
m_desired_height_fraction(0.0),
m_height_speed(5.0),
m_open_inhibited(0),
m_cursor_blink(0.0),
m_cursor_blink_speed(0.0),
m_cursor_height(0.0),
m_background(NULL),
m_background_color(255, 0, 0, 0),
m_font(NULL),
m_fontsize(0, 0)
{
m_animate_time_old = getTimeMs();
// load background settings
bool console_color_set = !g_settings->get("console_color").empty();
s32 console_alpha = g_settings->getS32("console_alpha");
// load the background texture depending on settings
m_background_color.setAlpha(clamp_u8(console_alpha));
if (console_color_set)
{
v3f console_color = g_settings->getV3F("console_color");
m_background_color.setRed(clamp_u8(myround(console_color.X)));
m_background_color.setGreen(clamp_u8(myround(console_color.Y)));
m_background_color.setBlue(clamp_u8(myround(console_color.Z)));
}
else
{
m_background = env->getVideoDriver()->getTexture(getTexturePath("background_chat.jpg").c_str());
m_background_color.setRed(255);
m_background_color.setGreen(255);
m_background_color.setBlue(255);
}
// load the font
// FIXME should a custom texture_path be searched too?
std::string font_name = "fontdejavusansmono.png";
m_font = env->getFont(getTexturePath(font_name).c_str());
if (m_font == NULL)
{
dstream << "Unable to load font: " << font_name << std::endl;
}
else
{
core::dimension2d<u32> dim = m_font->getDimension(L"M");
m_fontsize = v2u32(dim.Width, dim.Height);
dstream << "Font size: " << m_fontsize.X << " " << m_fontsize.Y << std::endl;
}
m_fontsize.X = MYMAX(m_fontsize.X, 1);
m_fontsize.Y = MYMAX(m_fontsize.Y, 1);
// set default cursor options
setCursor(true, true, 2.0, 0.1);
}
GUIChatConsole::~GUIChatConsole()
{
}
void GUIChatConsole::openConsole(f32 height)
{
m_open = true;
m_desired_height_fraction = height;
m_desired_height = height * m_screensize.Y;
reformatConsole();
}
bool GUIChatConsole::isOpenInhibited() const
{
return m_open_inhibited > 0;
}
void GUIChatConsole::closeConsole()
{
m_open = false;
}
void GUIChatConsole::closeConsoleAtOnce()
{
m_open = false;
m_height = 0;
recalculateConsolePosition();
}
f32 GUIChatConsole::getDesiredHeight() const
{
return m_desired_height_fraction;
}
void GUIChatConsole::setCursor(
bool visible, bool blinking, f32 blink_speed, f32 relative_height)
{
if (visible)
{
if (blinking)
{
// leave m_cursor_blink unchanged
m_cursor_blink_speed = blink_speed;
}
else
{
m_cursor_blink = 0x8000; // on
m_cursor_blink_speed = 0.0;
}
}
else
{
m_cursor_blink = 0; // off
m_cursor_blink_speed = 0.0;
}
m_cursor_height = relative_height;
}
void GUIChatConsole::draw()
{
if(!IsVisible)
return;
video::IVideoDriver* driver = Environment->getVideoDriver();
// Check screen size
v2u32 screensize = driver->getScreenSize();
if (screensize != m_screensize)
{
// screen size has changed
// scale current console height to new window size
if (m_screensize.Y != 0)
m_height = m_height * screensize.Y / m_screensize.Y;
m_desired_height = m_desired_height_fraction * m_screensize.Y;
m_screensize = screensize;
reformatConsole();
}
// Animation
u32 now = getTimeMs();
animate(now - m_animate_time_old);
m_animate_time_old = now;
// Draw console elements if visible
if (m_height > 0)
{
drawBackground();
drawText();
drawPrompt();
}
gui::IGUIElement::draw();
}
void GUIChatConsole::reformatConsole()
{
s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better)
s32 rows = m_desired_height / m_fontsize.Y - 1; // make room for the input prompt
if (cols <= 0 || rows <= 0)
cols = rows = 0;
m_chat_backend->reformat(cols, rows);
}
void GUIChatConsole::recalculateConsolePosition()
{
core::rect<s32> rect(0, 0, m_screensize.X, m_height);
DesiredRect = rect;
recalculateAbsolutePosition(false);
}
void GUIChatConsole::animate(u32 msec)
{
// animate the console height
s32 goal = m_open ? m_desired_height : 0;
if (m_height != goal)
{
s32 max_change = msec * m_screensize.Y * (m_height_speed / 1000.0);
if (max_change == 0)
max_change = 1;
if (m_height < goal)
{
// increase height
if (m_height + max_change < goal)
m_height += max_change;
else
m_height = goal;
}
else
{
// decrease height
if (m_height > goal + max_change)
m_height -= max_change;
else
m_height = goal;
}
recalculateConsolePosition();
}
// blink the cursor
if (m_cursor_blink_speed != 0.0)
{
u32 blink_increase = 0x10000 * msec * (m_cursor_blink_speed / 1000.0);
if (blink_increase == 0)
blink_increase = 1;
m_cursor_blink = ((m_cursor_blink + blink_increase) & 0xffff);
}
// decrease open inhibit counter
if (m_open_inhibited > msec)
m_open_inhibited -= msec;
else
m_open_inhibited = 0;
}
void GUIChatConsole::drawBackground()
{
video::IVideoDriver* driver = Environment->getVideoDriver();
if (m_background != NULL)
{
core::rect<s32> sourcerect(0, -m_height, m_screensize.X, 0);
driver->draw2DImage(
m_background,
v2s32(0, 0),
sourcerect,
&AbsoluteClippingRect,
m_background_color,
false);
}
else
{
driver->draw2DRectangle(
m_background_color,
core::rect<s32>(0, 0, m_screensize.X, m_height),
&AbsoluteClippingRect);
}
}
void GUIChatConsole::drawText()
{
if (m_font == NULL)
return;
ChatBuffer& buf = m_chat_backend->getConsoleBuffer();
for (u32 row = 0; row < buf.getRows(); ++row)
{
const ChatFormattedLine& line = buf.getFormattedLine(row);
if (line.fragments.empty())
continue;
s32 line_height = m_fontsize.Y;
s32 y = row * line_height + m_height - m_desired_height;
if (y + line_height < 0)
continue;
for (u32 i = 0; i < line.fragments.size(); ++i)
{
const ChatFormattedFragment& fragment = line.fragments[i];
s32 x = (fragment.column + 1) * m_fontsize.X;
core::rect<s32> destrect(
x, y, x + m_fontsize.X * fragment.text.size(), y + m_fontsize.Y);
m_font->draw(
fragment.text.c_str(),
destrect,
video::SColor(255, 255, 255, 255),
false,
false,
&AbsoluteClippingRect);
}
}
}
void GUIChatConsole::drawPrompt()
{
if (m_font == NULL)
return;
u32 row = m_chat_backend->getConsoleBuffer().getRows();
s32 line_height = m_fontsize.Y;
s32 y = row * line_height + m_height - m_desired_height;
ChatPrompt& prompt = m_chat_backend->getPrompt();
std::wstring prompt_text = prompt.getVisiblePortion();
// FIXME Draw string at once, not character by character
// That will only work with the cursor once we have a monospace font
for (u32 i = 0; i < prompt_text.size(); ++i)
{
wchar_t ws[2] = {prompt_text[i], 0};
s32 x = (1 + i) * m_fontsize.X;
core::rect<s32> destrect(
x, y, x + m_fontsize.X, y + m_fontsize.Y);
m_font->draw(
ws,
destrect,
video::SColor(255, 255, 255, 255),
false,
false,
&AbsoluteClippingRect);
}
// Draw the cursor during on periods
if ((m_cursor_blink & 0x8000) != 0)
{
s32 cursor_pos = prompt.getVisibleCursorPosition();
if (cursor_pos >= 0)
{
video::IVideoDriver* driver = Environment->getVideoDriver();
s32 x = (1 + cursor_pos) * m_fontsize.X;
core::rect<s32> destrect(
x,
y + (1.0-m_cursor_height) * m_fontsize.Y,
x + m_fontsize.X,
y + m_fontsize.Y);
video::SColor cursor_color(255,255,255,255);
driver->draw2DRectangle(
cursor_color,
destrect,
&AbsoluteClippingRect);
}
}
}
bool GUIChatConsole::OnEvent(const SEvent& event)
{
if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
{
// Key input
if(KeyPress(event.KeyInput) == getKeySetting("keymap_console"))
{
closeConsole();
Environment->removeFocus(this);
// inhibit open so the_game doesn't reopen immediately
m_open_inhibited = 50;
return true;
}
else if(event.KeyInput.Key == KEY_ESCAPE)
{
closeConsoleAtOnce();
Environment->removeFocus(this);
// the_game will open the pause menu
return true;
}
else if(event.KeyInput.Key == KEY_PRIOR)
{
m_chat_backend->scrollPageUp();
return true;
}
else if(event.KeyInput.Key == KEY_NEXT)
{
m_chat_backend->scrollPageDown();
return true;
}
else if(event.KeyInput.Key == KEY_RETURN)
{
std::wstring text = m_chat_backend->getPrompt().submit();
m_client->typeChatMessage(text);
return true;
}
else if(event.KeyInput.Key == KEY_UP)
{
// Up pressed
// Move back in history
m_chat_backend->getPrompt().historyPrev();
return true;
}
else if(event.KeyInput.Key == KEY_DOWN)
{
// Down pressed
// Move forward in history
m_chat_backend->getPrompt().historyNext();
return true;
}
else if(event.KeyInput.Key == KEY_LEFT)
{
// Left or Ctrl-Left pressed
// move character / word to the left
ChatPrompt::CursorOpScope scope =
event.KeyInput.Control ?
ChatPrompt::CURSOROP_SCOPE_WORD :
ChatPrompt::CURSOROP_SCOPE_CHARACTER;
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_MOVE,
ChatPrompt::CURSOROP_DIR_LEFT,
scope);
return true;
}
else if(event.KeyInput.Key == KEY_RIGHT)
{
// Right or Ctrl-Right pressed
// move character / word to the right
ChatPrompt::CursorOpScope scope =
event.KeyInput.Control ?
ChatPrompt::CURSOROP_SCOPE_WORD :
ChatPrompt::CURSOROP_SCOPE_CHARACTER;
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_MOVE,
ChatPrompt::CURSOROP_DIR_RIGHT,
scope);
return true;
}
else if(event.KeyInput.Key == KEY_HOME)
{
// Home pressed
// move to beginning of line
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_MOVE,
ChatPrompt::CURSOROP_DIR_LEFT,
ChatPrompt::CURSOROP_SCOPE_LINE);
return true;
}
else if(event.KeyInput.Key == KEY_END)
{
// End pressed
// move to end of line
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_MOVE,
ChatPrompt::CURSOROP_DIR_RIGHT,
ChatPrompt::CURSOROP_SCOPE_LINE);
return true;
}
else if(event.KeyInput.Key == KEY_BACK)
{
// Backspace or Ctrl-Backspace pressed
// delete character / word to the left
ChatPrompt::CursorOpScope scope =
event.KeyInput.Control ?
ChatPrompt::CURSOROP_SCOPE_WORD :
ChatPrompt::CURSOROP_SCOPE_CHARACTER;
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_DELETE,
ChatPrompt::CURSOROP_DIR_LEFT,
scope);
return true;
}
else if(event.KeyInput.Key == KEY_DELETE)
{
// Delete or Ctrl-Delete pressed
// delete character / word to the right
ChatPrompt::CursorOpScope scope =
event.KeyInput.Control ?
ChatPrompt::CURSOROP_SCOPE_WORD :
ChatPrompt::CURSOROP_SCOPE_CHARACTER;
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_DELETE,
ChatPrompt::CURSOROP_DIR_RIGHT,
scope);
return true;
}
else if(event.KeyInput.Key == KEY_KEY_U && event.KeyInput.Control)
{
// Ctrl-U pressed
// kill line to left end
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_DELETE,
ChatPrompt::CURSOROP_DIR_LEFT,
ChatPrompt::CURSOROP_SCOPE_LINE);
return true;
}
else if(event.KeyInput.Key == KEY_KEY_K && event.KeyInput.Control)
{
// Ctrl-K pressed
// kill line to right end
m_chat_backend->getPrompt().cursorOperation(
ChatPrompt::CURSOROP_DELETE,
ChatPrompt::CURSOROP_DIR_RIGHT,
ChatPrompt::CURSOROP_SCOPE_LINE);
return true;
}
else if(event.KeyInput.Key == KEY_TAB)
{
// Tab or Shift-Tab pressed
// Nick completion
core::list<std::wstring> names = m_client->getConnectedPlayerNames();
bool backwards = event.KeyInput.Shift;
m_chat_backend->getPrompt().nickCompletion(names, backwards);
return true;
}
else if(event.KeyInput.Char != 0 && !event.KeyInput.Control)
{
m_chat_backend->getPrompt().input(event.KeyInput.Char);
return true;
}
}
else if(event.EventType == EET_MOUSE_INPUT_EVENT)
{
if(event.MouseInput.Event == EMIE_MOUSE_WHEEL)
{
s32 rows = myround(-3.0 * event.MouseInput.Wheel);
m_chat_backend->scroll(rows);
}
}
return Parent ? Parent->OnEvent(event) : false;
}

125
src/guiChatConsole.h Normal file

@ -0,0 +1,125 @@
/*
Minetest-c55
Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef GUICHATCONSOLE_HEADER
#define GUICHATCONSOLE_HEADER
#include "common_irrlicht.h"
#include "chat.h"
class Client;
class GUIChatConsole : public gui::IGUIElement
{
public:
GUIChatConsole(gui::IGUIEnvironment* env,
gui::IGUIElement* parent,
s32 id,
ChatBackend* backend,
Client* client);
virtual ~GUIChatConsole();
// Open the console (height = desired fraction of screen size)
// This doesn't open immediately but initiates an animation.
// You should call isOpenInhibited() before this.
void openConsole(f32 height);
// Check if the console should not be opened at the moment
// This is to avoid reopening the console immediately after closing
bool isOpenInhibited() const;
// Close the console, equivalent to openConsole(0).
// This doesn't close immediately but initiates an animation.
void closeConsole();
// Close the console immediately, without animation.
void closeConsoleAtOnce();
// Return the desired height (fraction of screen size)
// Zero if the console is closed or getting closed
f32 getDesiredHeight() const;
// Change how the cursor looks
void setCursor(
bool visible,
bool blinking = false,
f32 blink_speed = 1.0,
f32 relative_height = 1.0);
// Irrlicht draw method
virtual void draw();
bool canTakeFocus(gui::IGUIElement* element) { return false; }
virtual bool OnEvent(const SEvent& event);
private:
void reformatConsole();
void recalculateConsolePosition();
// These methods are called by draw
void animate(u32 msec);
void drawBackground();
void drawText();
void drawPrompt();
private:
// pointer to the chat backend
ChatBackend* m_chat_backend;
// pointer to the client
Client* m_client;
// current screen size
v2u32 m_screensize;
// used to compute how much time passed since last animate()
u32 m_animate_time_old;
// should the console be opened or closed?
bool m_open;
// current console height [pixels]
s32 m_height;
// desired height [pixels]
f32 m_desired_height;
// desired height [screen height fraction]
f32 m_desired_height_fraction;
// console open/close animation speed [screen height fraction / second]
f32 m_height_speed;
// if nonzero, opening the console is inhibited [milliseconds]
u32 m_open_inhibited;
// cursor blink frame (16-bit value)
// cursor is off during [0,32767] and on during [32768,65535]
u32 m_cursor_blink;
// cursor blink speed [on/off toggles / second]
f32 m_cursor_blink_speed;
// cursor height [line height]
f32 m_cursor_height;
// background texture
video::ITexture* m_background;
// background color (including alpha)
video::SColor m_background_color;
// font
gui::IGUIFont* m_font;
v2u32 m_fontsize;
};
#endif

@ -261,7 +261,20 @@ void GUIKeyChangeMenu::regenerateGui(v2u32 screensize)
this->cmd = Environment->addButton(rect, this, GUI_ID_KEY_CMD_BUTTON,
wgettext(key_cmd.name()));
}
offset += v2s32(0, 25);
{
core::rect < s32 > rect(0, 0, 100, 20);
rect += topleft + v2s32(offset.X, offset.Y);
Environment->addStaticText(wgettext("Console"), rect, false, true, this, -1);
//t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT);
}
{
core::rect < s32 > rect(0, 0, 100, 30);
rect += topleft + v2s32(offset.X + 105, offset.Y - 5);
this->console = Environment->addButton(rect, this, GUI_ID_KEY_CONSOLE_BUTTON,
wgettext(key_console.name()));
}
//next col
offset = v2s32(250, 40);
@ -371,6 +384,7 @@ bool GUIKeyChangeMenu::acceptInput()
g_settings->set("keymap_inventory", key_inventory.sym());
g_settings->set("keymap_chat", key_chat.sym());
g_settings->set("keymap_cmd", key_cmd.sym());
g_settings->set("keymap_console", key_console.sym());
g_settings->set("keymap_rangeselect", key_range.sym());
g_settings->set("keymap_freemove", key_fly.sym());
g_settings->set("keymap_fastmove", key_fast.sym());
@ -391,6 +405,7 @@ void GUIKeyChangeMenu::init_keys()
key_inventory = getKeySetting("keymap_inventory");
key_chat = getKeySetting("keymap_chat");
key_cmd = getKeySetting("keymap_cmd");
key_console = getKeySetting("keymap_console");
key_range = getKeySetting("keymap_rangeselect");
key_fly = getKeySetting("keymap_freemove");
key_fast = getKeySetting("keymap_fastmove");
@ -437,6 +452,9 @@ bool GUIKeyChangeMenu::resetMenu()
case GUI_ID_KEY_CMD_BUTTON:
this->cmd->setText(wgettext(key_cmd.name()));
break;
case GUI_ID_KEY_CONSOLE_BUTTON:
this->console->setText(wgettext(key_console.name()));
break;
case GUI_ID_KEY_RANGE_BUTTON:
this->range->setText(wgettext(key_range.name()));
break;
@ -516,6 +534,11 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event)
this->cmd->setText(wgettext(kp.name()));
this->key_cmd = kp;
}
else if (activeKey == GUI_ID_KEY_CONSOLE_BUTTON)
{
this->console->setText(wgettext(kp.name()));
this->key_console = kp;
}
else if (activeKey == GUI_ID_KEY_RANGE_BUTTON)
{
this->range->setText(wgettext(kp.name()));
@ -630,6 +653,11 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event)
activeKey = event.GUIEvent.Caller->getID();
this->cmd->setText(wgettext("press Key"));
break;
case GUI_ID_KEY_CONSOLE_BUTTON:
resetMenu();
activeKey = event.GUIEvent.Caller->getID();
this->console->setText(wgettext("press Key"));
break;
case GUI_ID_KEY_SNEAK_BUTTON:
resetMenu();
activeKey = event.GUIEvent.Caller->getID();

@ -44,6 +44,7 @@ enum
GUI_ID_KEY_JUMP_BUTTON,
GUI_ID_KEY_CHAT_BUTTON,
GUI_ID_KEY_CMD_BUTTON,
GUI_ID_KEY_CONSOLE_BUTTON,
GUI_ID_KEY_SNEAK_BUTTON,
GUI_ID_KEY_DROP_BUTTON,
GUI_ID_KEY_INVENTORY_BUTTON,
@ -91,6 +92,7 @@ private:
gui::IGUIButton *dump;
gui::IGUIButton *chat;
gui::IGUIButton *cmd;
gui::IGUIButton *console;
s32 activeKey;
KeyPress key_forward;
@ -107,6 +109,7 @@ private:
KeyPress key_range;
KeyPress key_chat;
KeyPress key_cmd;
KeyPress key_console;
KeyPress key_dump;
};

@ -403,6 +403,7 @@ Doing currently:
#include "game.h"
#include "keycode.h"
#include "tile.h"
#include "chat.h"
#include "defaultsettings.h"
#include "gettext.h"
#include "settings.h"
@ -940,21 +941,21 @@ void drawMenuBackground(video::IVideoDriver* driver)
driver->getTexture(getTexturePath("menubg.png").c_str());
if(bgtexture)
{
s32 texturesize = 128;
s32 tiled_y = screensize.Height / texturesize + 1;
s32 tiled_x = screensize.Width / texturesize + 1;
s32 scaledsize = 128;
for(s32 y=0; y<tiled_y; y++)
for(s32 x=0; x<tiled_x; x++)
{
core::rect<s32> rect(0,0,texturesize,texturesize);
rect += v2s32(x*texturesize, y*texturesize);
driver->draw2DImage(bgtexture, rect,
core::rect<s32>(core::position2d<s32>(0,0),
core::dimension2di(bgtexture->getSize())),
// The important difference between destsize and screensize is
// that destsize is rounded to whole scaled pixels.
// These formulas use component-wise multiplication and division of v2u32.
v2u32 texturesize = bgtexture->getSize();
v2u32 sourcesize = texturesize * screensize / scaledsize + v2u32(1,1);
v2u32 destsize = scaledsize * sourcesize / texturesize;
// Default texture wrapping mode in Irrlicht is ETC_REPEAT.
driver->draw2DImage(bgtexture,
core::rect<s32>(0, 0, destsize.X, destsize.Y),
core::rect<s32>(0, 0, sourcesize.X, sourcesize.Y),
NULL, NULL, true);
}
}
video::ITexture *logotexture =
driver->getTexture(getTexturePath("menulogo.png").c_str());
@ -1479,6 +1480,8 @@ int main(int argc, char *argv[])
GUI stuff
*/
ChatBackend chat_backend;
/*
If an error occurs, this is set to something and the
menu-game loop is restarted. It is then displayed before
@ -1663,7 +1666,8 @@ int main(int argc, char *argv[])
address,
port,
error_message,
configpath
configpath,
chat_backend
);
} //try

@ -694,6 +694,46 @@ private:
u32 *m_result;
};
// Tests if two strings are equal, optionally case insensitive
inline bool str_equal(const std::wstring& s1, const std::wstring& s2,
bool case_insensitive = false)
{
if(case_insensitive)
{
if(s1.size() != s2.size())
return false;
for(size_t i = 0; i < s1.size(); ++i)
if(tolower(s1[i]) != tolower(s2[i]))
return false;
return true;
}
else
{
return s1 == s2;
}
}
// Tests if the second string is a prefix of the first, optionally case insensitive
inline bool str_starts_with(const std::wstring& str, const std::wstring& prefix,
bool case_insensitive = false)
{
if(str.size() < prefix.size())
return false;
if(case_insensitive)
{
for(size_t i = 0; i < prefix.size(); ++i)
if(tolower(str[i]) != tolower(prefix[i]))
return false;
}
else
{
for(size_t i = 0; i < prefix.size(); ++i)
if(str[i] != prefix[i])
return false;
}
return true;
}
// Calculates the borders of a "d-radius" cube
inline void getFacePositions(core::list<v3s16> &list, u16 d)
{
@ -1565,6 +1605,15 @@ inline std::string wrap_rows(const std::string &from, u32 rowlen)
#define MYMIN(a,b) ((a)<(b)?(a):(b))
#define MYMAX(a,b) ((a)>(b)?(a):(b))
/*
Returns nearest 32-bit integer for given floating point number.
<cmath> and <math.h> in VC++ don't provide round().
*/
inline s32 myround(f32 f)
{
return floor(f + 0.5);
}
/*
Returns integer position of node in given floating point position
*/