mirror of https://github.com/OpenTTD/OpenTTD.git
Add: Basic autocompletion on tab for console commands (#12163)
This commit is contained in:
parent
24efdf6ac5
commit
23d733be95
|
@ -52,6 +52,8 @@ add_files(
|
||||||
animated_tile_func.h
|
animated_tile_func.h
|
||||||
articulated_vehicles.cpp
|
articulated_vehicles.cpp
|
||||||
articulated_vehicles.h
|
articulated_vehicles.h
|
||||||
|
autocompletion.cpp
|
||||||
|
autocompletion.h
|
||||||
autoreplace.cpp
|
autoreplace.cpp
|
||||||
autoreplace_base.h
|
autoreplace_base.h
|
||||||
autoreplace_cmd.cpp
|
autoreplace_cmd.cpp
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* This file is part of OpenTTD.
|
||||||
|
* OpenTTD 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, version 2.
|
||||||
|
* OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @file autocompletion.cpp Generic auto-completion engine. */
|
||||||
|
|
||||||
|
#include "stdafx.h"
|
||||||
|
|
||||||
|
#include "autocompletion.h"
|
||||||
|
|
||||||
|
#include "console_internal.h"
|
||||||
|
#include "town.h"
|
||||||
|
#include "network/network_base.h"
|
||||||
|
|
||||||
|
#include "safeguards.h"
|
||||||
|
|
||||||
|
bool AutoCompletion::AutoComplete()
|
||||||
|
{
|
||||||
|
// We are pressing TAB for the first time after reset.
|
||||||
|
if (this->suggestions.empty()) {
|
||||||
|
this->InitSuggestions(this->textbuf->buf);
|
||||||
|
if (this->suggestions.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->ApplySuggestion(prefix, suggestions[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are pressing TAB again on the same text.
|
||||||
|
if (this->current_suggestion_index + 1 < this->suggestions.size()) {
|
||||||
|
this->ApplySuggestion(prefix, this->suggestions[++this->current_suggestion_index]);
|
||||||
|
} else {
|
||||||
|
// We are out of options, restore original text.
|
||||||
|
this->textbuf->Assign(initial_buf);
|
||||||
|
this->Reset();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoCompletion::Reset()
|
||||||
|
{
|
||||||
|
this->prefix = "";
|
||||||
|
this->query = "";
|
||||||
|
this->initial_buf.clear();
|
||||||
|
this->suggestions.clear();
|
||||||
|
this->current_suggestion_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoCompletion::InitSuggestions(std::string_view text)
|
||||||
|
{
|
||||||
|
this->initial_buf = text;
|
||||||
|
size_t space_pos = this->initial_buf.find_last_of(' ');
|
||||||
|
this->query = this->initial_buf;
|
||||||
|
if (space_pos == std::string::npos) {
|
||||||
|
this->prefix = "";
|
||||||
|
} else {
|
||||||
|
this->prefix = this->query.substr(0, space_pos + 1);
|
||||||
|
this->query.remove_prefix(space_pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->suggestions = this->GetSuggestions(prefix, query);
|
||||||
|
this->current_suggestion_index = 0;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* This file is part of OpenTTD.
|
||||||
|
* OpenTTD 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, version 2.
|
||||||
|
* OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @file autocompletion.h Generic auto-completion engine. */
|
||||||
|
|
||||||
|
#ifndef AUTOCOMPLETION_H
|
||||||
|
#define AUTOCOMPLETION_H
|
||||||
|
|
||||||
|
#include "textbuf_type.h"
|
||||||
|
|
||||||
|
class AutoCompletion {
|
||||||
|
protected:
|
||||||
|
Textbuf *textbuf;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string initial_buf; ///< Value of text buffer when we started current suggestion session.
|
||||||
|
|
||||||
|
std::string_view prefix; ///< Prefix of the text before the last space.
|
||||||
|
std::string_view query; ///< Last token of the text. This is used to based the suggestions on.
|
||||||
|
|
||||||
|
std::vector<std::string> suggestions;
|
||||||
|
size_t current_suggestion_index;
|
||||||
|
|
||||||
|
public:
|
||||||
|
AutoCompletion(Textbuf *textbuf) : textbuf(textbuf)
|
||||||
|
{
|
||||||
|
this->Reset();
|
||||||
|
}
|
||||||
|
virtual ~AutoCompletion() = default;
|
||||||
|
|
||||||
|
// Returns true the textbuf was updated.
|
||||||
|
bool AutoComplete();
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void InitSuggestions(std::string_view text);
|
||||||
|
|
||||||
|
virtual std::vector<std::string> GetSuggestions(std::string_view prefix, std::string_view query) = 0;
|
||||||
|
virtual void ApplySuggestion(std::string_view prefix, std::string_view suggestion) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* AUTOCOMPLETION_H */
|
|
@ -10,6 +10,7 @@
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "textbuf_type.h"
|
#include "textbuf_type.h"
|
||||||
#include "window_gui.h"
|
#include "window_gui.h"
|
||||||
|
#include "autocompletion.h"
|
||||||
#include "console_gui.h"
|
#include "console_gui.h"
|
||||||
#include "console_internal.h"
|
#include "console_internal.h"
|
||||||
#include "window_func.h"
|
#include "window_func.h"
|
||||||
|
@ -68,9 +69,44 @@ static std::deque<IConsoleLine> _iconsole_buffer;
|
||||||
|
|
||||||
static bool TruncateBuffer();
|
static bool TruncateBuffer();
|
||||||
|
|
||||||
|
class ConsoleAutoCompletion final : public AutoCompletion {
|
||||||
|
public:
|
||||||
|
using AutoCompletion::AutoCompletion;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::string> GetSuggestions(std::string_view prefix, std::string_view query) override
|
||||||
|
{
|
||||||
|
prefix = StrTrimView(prefix);
|
||||||
|
std::vector<std::string> suggestions;
|
||||||
|
|
||||||
|
/* We only suggest commands or aliases, so we only do it for the first token or an argument to help command. */
|
||||||
|
if (!prefix.empty() && prefix != "help") {
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[_, command] : IConsole::Commands()) {
|
||||||
|
if (command.name.starts_with(query)) {
|
||||||
|
suggestions.push_back(command.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &[_, alias] : IConsole::Aliases()) {
|
||||||
|
if (alias.name.starts_with(query)) {
|
||||||
|
suggestions.push_back(alias.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplySuggestion(std::string_view prefix, std::string_view suggestion) override
|
||||||
|
{
|
||||||
|
this->textbuf->Assign(fmt::format("{}{} ", prefix, suggestion));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* ** main console cmd buffer ** */
|
/* ** main console cmd buffer ** */
|
||||||
static Textbuf _iconsole_cmdline(ICON_CMDLN_SIZE);
|
static Textbuf _iconsole_cmdline(ICON_CMDLN_SIZE);
|
||||||
|
static ConsoleAutoCompletion _iconsole_tab_completion(&_iconsole_cmdline);
|
||||||
static std::deque<std::string> _iconsole_history;
|
static std::deque<std::string> _iconsole_history;
|
||||||
static ptrdiff_t _iconsole_historypos;
|
static ptrdiff_t _iconsole_historypos;
|
||||||
IConsoleModes _iconsole_mode;
|
IConsoleModes _iconsole_mode;
|
||||||
|
@ -86,6 +122,7 @@ static void IConsoleClearCommand()
|
||||||
_iconsole_cmdline.pixels = 0;
|
_iconsole_cmdline.pixels = 0;
|
||||||
_iconsole_cmdline.caretpos = 0;
|
_iconsole_cmdline.caretpos = 0;
|
||||||
_iconsole_cmdline.caretxoffs = 0;
|
_iconsole_cmdline.caretxoffs = 0;
|
||||||
|
_iconsole_tab_completion.Reset();
|
||||||
SetWindowDirty(WC_CONSOLE, 0);
|
SetWindowDirty(WC_CONSOLE, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,8 +295,18 @@ struct IConsoleWindow : Window
|
||||||
IConsoleCmdExec("clear");
|
IConsoleCmdExec("clear");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
case WKC_TAB:
|
||||||
if (_iconsole_cmdline.HandleKeyPress(key, keycode) != HKPR_NOT_HANDLED) {
|
if (_iconsole_tab_completion.AutoComplete()) {
|
||||||
|
this->SetDirty();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
HandleKeyPressResult handle_result = _iconsole_cmdline.HandleKeyPress(key, keycode);
|
||||||
|
if (handle_result != HKPR_NOT_HANDLED) {
|
||||||
|
if (handle_result == HKPR_EDITING) {
|
||||||
|
_iconsole_tab_completion.Reset();
|
||||||
|
}
|
||||||
IConsoleWindow::scroll = 0;
|
IConsoleWindow::scroll = 0;
|
||||||
IConsoleResetHistoryPos();
|
IConsoleResetHistoryPos();
|
||||||
this->SetDirty();
|
this->SetDirty();
|
||||||
|
@ -267,6 +314,7 @@ struct IConsoleWindow : Window
|
||||||
return ES_NOT_HANDLED;
|
return ES_NOT_HANDLED;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ES_HANDLED;
|
return ES_HANDLED;
|
||||||
}
|
}
|
||||||
|
@ -274,13 +322,14 @@ struct IConsoleWindow : Window
|
||||||
void InsertTextString(WidgetID, const char *str, bool marked, const char *caret, const char *insert_location, const char *replacement_end) override
|
void InsertTextString(WidgetID, const char *str, bool marked, const char *caret, const char *insert_location, const char *replacement_end) override
|
||||||
{
|
{
|
||||||
if (_iconsole_cmdline.InsertString(str, marked, caret, insert_location, replacement_end)) {
|
if (_iconsole_cmdline.InsertString(str, marked, caret, insert_location, replacement_end)) {
|
||||||
|
_iconsole_tab_completion.Reset();
|
||||||
IConsoleWindow::scroll = 0;
|
IConsoleWindow::scroll = 0;
|
||||||
IConsoleResetHistoryPos();
|
IConsoleResetHistoryPos();
|
||||||
this->SetDirty();
|
this->SetDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Textbuf *GetFocusedTextbuf() const override
|
const Textbuf *GetFocusedTextbuf() const override
|
||||||
{
|
{
|
||||||
return &_iconsole_cmdline;
|
return &_iconsole_cmdline;
|
||||||
}
|
}
|
||||||
|
@ -434,6 +483,7 @@ static void IConsoleHistoryNavigate(int direction)
|
||||||
} else {
|
} else {
|
||||||
_iconsole_cmdline.Assign(_iconsole_history[_iconsole_historypos]);
|
_iconsole_cmdline.Assign(_iconsole_history[_iconsole_historypos]);
|
||||||
}
|
}
|
||||||
|
_iconsole_tab_completion.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
#include "../stdafx.h"
|
#include "../stdafx.h"
|
||||||
#include "../strings_func.h"
|
#include "../strings_func.h"
|
||||||
|
#include "../autocompletion.h"
|
||||||
#include "../blitter/factory.hpp"
|
#include "../blitter/factory.hpp"
|
||||||
#include "../console_func.h"
|
#include "../console_func.h"
|
||||||
#include "../video/video_driver.hpp"
|
#include "../video/video_driver.hpp"
|
||||||
|
@ -44,7 +45,6 @@ struct ChatMessage {
|
||||||
static std::deque<ChatMessage> _chatmsg_list; ///< The actual chat message list.
|
static std::deque<ChatMessage> _chatmsg_list; ///< The actual chat message list.
|
||||||
static bool _chatmessage_dirty = false; ///< Does the chat message need repainting?
|
static bool _chatmessage_dirty = false; ///< Does the chat message need repainting?
|
||||||
static bool _chatmessage_visible = false; ///< Is a chat message visible.
|
static bool _chatmessage_visible = false; ///< Is a chat message visible.
|
||||||
static bool _chat_tab_completion_active; ///< Whether tab completion is active.
|
|
||||||
static uint MAX_CHAT_MESSAGES = 0; ///< The limit of chat messages to show.
|
static uint MAX_CHAT_MESSAGES = 0; ///< The limit of chat messages to show.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -262,11 +262,47 @@ static void SendChat(const std::string &buf, DestType type, int dest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NetworkChatAutoCompletion final : public AutoCompletion {
|
||||||
|
public:
|
||||||
|
using AutoCompletion::AutoCompletion;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::string> GetSuggestions([[maybe_unused]] std::string_view prefix, std::string_view query) override
|
||||||
|
{
|
||||||
|
std::vector<std::string> suggestions;
|
||||||
|
for (NetworkClientInfo *ci : NetworkClientInfo::Iterate()) {
|
||||||
|
if (ci->client_name.starts_with(query)) {
|
||||||
|
suggestions.push_back(ci->client_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const Town *t : Town::Iterate()) {
|
||||||
|
/* Get the town-name via the string-system */
|
||||||
|
SetDParam(0, t->index);
|
||||||
|
std::string town_name = GetString(STR_TOWN_NAME);
|
||||||
|
if (town_name.starts_with(query)) {
|
||||||
|
suggestions.push_back(std::move(town_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplySuggestion(std::string_view prefix, std::string_view suggestion) override
|
||||||
|
{
|
||||||
|
/* Add ': ' if we are at the start of the line (pretty) */
|
||||||
|
if (prefix.empty()) {
|
||||||
|
this->textbuf->Assign(fmt::format("{}: ", suggestion));
|
||||||
|
} else {
|
||||||
|
this->textbuf->Assign(fmt::format("{}{} ", prefix, suggestion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** Window to enter the chat message in. */
|
/** Window to enter the chat message in. */
|
||||||
struct NetworkChatWindow : public Window {
|
struct NetworkChatWindow : public Window {
|
||||||
DestType dtype; ///< The type of destination.
|
DestType dtype; ///< The type of destination.
|
||||||
int dest; ///< The identifier of the destination.
|
int dest; ///< The identifier of the destination.
|
||||||
QueryString message_editbox; ///< Message editbox.
|
QueryString message_editbox; ///< Message editbox.
|
||||||
|
NetworkChatAutoCompletion chat_tab_completion; ///< Holds the state and logic of auto-completion of player names and towns on Tab press.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a chat input window.
|
* Create a chat input window.
|
||||||
|
@ -274,7 +310,8 @@ struct NetworkChatWindow : public Window {
|
||||||
* @param type The type of destination.
|
* @param type The type of destination.
|
||||||
* @param dest The actual destination index.
|
* @param dest The actual destination index.
|
||||||
*/
|
*/
|
||||||
NetworkChatWindow(WindowDesc *desc, DestType type, int dest) : Window(desc), message_editbox(NETWORK_CHAT_LENGTH)
|
NetworkChatWindow(WindowDesc *desc, DestType type, int dest)
|
||||||
|
: Window(desc), message_editbox(NETWORK_CHAT_LENGTH), chat_tab_completion(&message_editbox.text)
|
||||||
{
|
{
|
||||||
this->dtype = type;
|
this->dtype = type;
|
||||||
this->dest = dest;
|
this->dest = dest;
|
||||||
|
@ -295,7 +332,6 @@ struct NetworkChatWindow : public Window {
|
||||||
|
|
||||||
this->SetFocusedWidget(WID_NC_TEXTBOX);
|
this->SetFocusedWidget(WID_NC_TEXTBOX);
|
||||||
InvalidateWindowData(WC_NEWS_WINDOW, 0, this->height);
|
InvalidateWindowData(WC_NEWS_WINDOW, 0, this->height);
|
||||||
_chat_tab_completion_active = false;
|
|
||||||
|
|
||||||
PositionNetworkChatWindow(this);
|
PositionNetworkChatWindow(this);
|
||||||
}
|
}
|
||||||
|
@ -311,125 +347,12 @@ struct NetworkChatWindow : public Window {
|
||||||
Window::FindWindowPlacementAndResize(_toolbar_width, def_height);
|
Window::FindWindowPlacementAndResize(_toolbar_width, def_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the next item of the list of things that can be auto-completed.
|
|
||||||
* @param item The current indexed item to return. This function can, and most
|
|
||||||
* likely will, alter item, to skip empty items in the arrays.
|
|
||||||
* @return Returns the view that matched to the index.
|
|
||||||
*/
|
|
||||||
std::optional<std::string> ChatTabCompletionNextItem(uint *item)
|
|
||||||
{
|
|
||||||
/* First, try clients */
|
|
||||||
if (*item < MAX_CLIENT_SLOTS) {
|
|
||||||
/* Skip inactive clients */
|
|
||||||
for (NetworkClientInfo *ci : NetworkClientInfo::Iterate(*item)) {
|
|
||||||
*item = ci->index;
|
|
||||||
return ci->client_name;
|
|
||||||
}
|
|
||||||
*item = MAX_CLIENT_SLOTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Then, try townnames
|
|
||||||
* Not that the following assumes all town indices are adjacent, ie no
|
|
||||||
* towns have been deleted. */
|
|
||||||
if (*item < (uint)MAX_CLIENT_SLOTS + Town::GetPoolSize()) {
|
|
||||||
for (const Town *t : Town::Iterate(*item - MAX_CLIENT_SLOTS)) {
|
|
||||||
/* Get the town-name via the string-system */
|
|
||||||
SetDParam(0, t->index);
|
|
||||||
return GetString(STR_TOWN_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find what text to complete. It scans for a space from the left and marks
|
|
||||||
* the word right from that as to complete. It also writes a \0 at the
|
|
||||||
* position of the space (if any). If nothing found, buf is returned.
|
|
||||||
*/
|
|
||||||
static std::string_view ChatTabCompletionFindText(std::string_view &buf)
|
|
||||||
{
|
|
||||||
auto it = buf.find_last_of(' ');
|
|
||||||
if (it == std::string_view::npos) return buf;
|
|
||||||
|
|
||||||
std::string_view res = buf.substr(it + 1);
|
|
||||||
buf.remove_suffix(res.size() + 1);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See if we can auto-complete the current text of the user.
|
* See if we can auto-complete the current text of the user.
|
||||||
*/
|
*/
|
||||||
void ChatTabCompletion()
|
void ChatTabCompletion()
|
||||||
{
|
{
|
||||||
static std::string _chat_tab_completion_buf;
|
if (this->chat_tab_completion.AutoComplete()) {
|
||||||
|
|
||||||
Textbuf *tb = &this->message_editbox.text;
|
|
||||||
uint item = 0;
|
|
||||||
bool second_scan = false;
|
|
||||||
|
|
||||||
/* Create views, so we do not need to copy the data for now. */
|
|
||||||
std::string_view pre_buf = _chat_tab_completion_active ? std::string_view(_chat_tab_completion_buf) : std::string_view(tb->buf);
|
|
||||||
std::string_view tb_buf = ChatTabCompletionFindText(pre_buf);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Comparing pointers of the data, as both "Hi:<tab>" and "Hi: Hi:<tab>" will result in
|
|
||||||
* tb_buf and pre_buf being "Hi:", which would be equal in content but not in context.
|
|
||||||
*/
|
|
||||||
bool begin_of_line = tb_buf.data() == pre_buf.data();
|
|
||||||
|
|
||||||
std::optional<std::string> cur_item;
|
|
||||||
while ((cur_item = ChatTabCompletionNextItem(&item)).has_value()) {
|
|
||||||
std::string_view cur_name = cur_item.value();
|
|
||||||
item++;
|
|
||||||
|
|
||||||
if (_chat_tab_completion_active) {
|
|
||||||
/* We are pressing TAB again on the same name, is there another name
|
|
||||||
* that starts with this? */
|
|
||||||
if (!second_scan) {
|
|
||||||
std::string_view view;
|
|
||||||
|
|
||||||
/* If we are completing at the begin of the line, skip the ': ' we added */
|
|
||||||
if (begin_of_line) {
|
|
||||||
view = std::string_view(tb->buf, (tb->bytes - 1) - 2);
|
|
||||||
} else {
|
|
||||||
/* Else, find the place we are completing at */
|
|
||||||
size_t offset = pre_buf.size() + 1;
|
|
||||||
view = std::string_view(tb->buf + offset, (tb->bytes - 1) - offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compare if we have a match */
|
|
||||||
if (cur_name == view) second_scan = true;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Now any match we make on _chat_tab_completion_buf after this, is perfect */
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tb_buf.size() < cur_name.size() && cur_name.starts_with(tb_buf)) {
|
|
||||||
/* Save the data it was before completion */
|
|
||||||
if (!second_scan) _chat_tab_completion_buf = tb->buf;
|
|
||||||
_chat_tab_completion_active = true;
|
|
||||||
|
|
||||||
/* Change to the found name. Add ': ' if we are at the start of the line (pretty) */
|
|
||||||
if (begin_of_line) {
|
|
||||||
this->message_editbox.text.Assign(fmt::format("{}: ", cur_name));
|
|
||||||
} else {
|
|
||||||
this->message_editbox.text.Assign(fmt::format("{} {}", pre_buf, cur_name));
|
|
||||||
}
|
|
||||||
|
|
||||||
this->SetDirty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (second_scan) {
|
|
||||||
/* We walked all possibilities, and the user presses tab again.. revert to original text */
|
|
||||||
this->message_editbox.text.Assign(_chat_tab_completion_buf);
|
|
||||||
_chat_tab_completion_active = false;
|
|
||||||
|
|
||||||
this->SetDirty();
|
this->SetDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -475,7 +398,7 @@ struct NetworkChatWindow : public Window {
|
||||||
void OnEditboxChanged(WidgetID widget) override
|
void OnEditboxChanged(WidgetID widget) override
|
||||||
{
|
{
|
||||||
if (widget == WID_NC_TEXTBOX) {
|
if (widget == WID_NC_TEXTBOX) {
|
||||||
_chat_tab_completion_active = false;
|
this->chat_tab_completion.Reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -254,30 +254,6 @@ bool StrValid(const char *str, const char *last)
|
||||||
return *str == '\0';
|
return *str == '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Trim the spaces from the begin of given string in place, i.e. the string buffer
|
|
||||||
* that is passed will be modified whenever spaces exist in the given string.
|
|
||||||
* When there are spaces at the begin, the whole string is moved forward.
|
|
||||||
* @param str The string to perform the in place left trimming on.
|
|
||||||
*/
|
|
||||||
static void StrLeftTrimInPlace(std::string &str)
|
|
||||||
{
|
|
||||||
size_t pos = str.find_first_not_of(' ');
|
|
||||||
str.erase(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trim the spaces from the end of given string in place, i.e. the string buffer
|
|
||||||
* that is passed will be modified whenever spaces exist in the given string.
|
|
||||||
* When there are spaces at the end, the '\0' will be moved forward.
|
|
||||||
* @param str The string to perform the in place left trimming on.
|
|
||||||
*/
|
|
||||||
static void StrRightTrimInPlace(std::string &str)
|
|
||||||
{
|
|
||||||
size_t pos = str.find_last_not_of(' ');
|
|
||||||
if (pos != std::string::npos) str.erase(pos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trim the spaces from given string in place, i.e. the string buffer that
|
* Trim the spaces from given string in place, i.e. the string buffer that
|
||||||
* is passed will be modified whenever spaces exist in the given string.
|
* is passed will be modified whenever spaces exist in the given string.
|
||||||
|
@ -287,8 +263,17 @@ static void StrRightTrimInPlace(std::string &str)
|
||||||
*/
|
*/
|
||||||
void StrTrimInPlace(std::string &str)
|
void StrTrimInPlace(std::string &str)
|
||||||
{
|
{
|
||||||
StrLeftTrimInPlace(str);
|
str = StrTrimView(str);
|
||||||
StrRightTrimInPlace(str);
|
}
|
||||||
|
|
||||||
|
std::string_view StrTrimView(std::string_view str)
|
||||||
|
{
|
||||||
|
size_t first_pos = str.find_first_not_of(' ');
|
||||||
|
if (first_pos == std::string::npos) {
|
||||||
|
return std::string_view{};
|
||||||
|
}
|
||||||
|
size_t last_pos = str.find_last_not_of(' ');
|
||||||
|
return str.substr(first_pos, last_pos - first_pos + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,6 +29,7 @@ bool strtolower(std::string &str, std::string::size_type offs = 0);
|
||||||
|
|
||||||
[[nodiscard]] bool StrValid(const char *str, const char *last) NOACCESS(2);
|
[[nodiscard]] bool StrValid(const char *str, const char *last) NOACCESS(2);
|
||||||
void StrTrimInPlace(std::string &str);
|
void StrTrimInPlace(std::string &str);
|
||||||
|
std::string_view StrTrimView(std::string_view str);
|
||||||
|
|
||||||
[[nodiscard]] bool StrStartsWithIgnoreCase(std::string_view str, const std::string_view prefix);
|
[[nodiscard]] bool StrStartsWithIgnoreCase(std::string_view str, const std::string_view prefix);
|
||||||
[[nodiscard]] bool StrEndsWithIgnoreCase(std::string_view str, const std::string_view suffix);
|
[[nodiscard]] bool StrEndsWithIgnoreCase(std::string_view str, const std::string_view suffix);
|
||||||
|
|
|
@ -384,3 +384,27 @@ TEST_CASE("ConvertHexToBytes")
|
||||||
CHECK(bytes3[6] == 0xde);
|
CHECK(bytes3[6] == 0xde);
|
||||||
CHECK(bytes3[7] == 0xf0);
|
CHECK(bytes3[7] == 0xf0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const std::vector<std::pair<std::string, std::string>> _str_trim_testcases = {
|
||||||
|
{"a", "a"},
|
||||||
|
{" a", "a"},
|
||||||
|
{"a ", "a"},
|
||||||
|
{" a ", "a"},
|
||||||
|
{" a b c ", "a b c"},
|
||||||
|
{" ", ""}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("StrTrimInPlace")
|
||||||
|
{
|
||||||
|
for (auto [input, expected] : _str_trim_testcases) {
|
||||||
|
StrTrimInPlace(input);
|
||||||
|
CHECK(input == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("StrTrimView") {
|
||||||
|
for (const auto& [input, expected] : _str_trim_testcases) {
|
||||||
|
CHECK(StrTrimView(input) == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -789,7 +789,12 @@ void CocoaDialog(const char *title, const char *message, const char *buttonLabel
|
||||||
case QZ_LEFT: SB(_dirkeys, 0, 1, down); break;
|
case QZ_LEFT: SB(_dirkeys, 0, 1, down); break;
|
||||||
case QZ_RIGHT: SB(_dirkeys, 2, 1, down); break;
|
case QZ_RIGHT: SB(_dirkeys, 2, 1, down); break;
|
||||||
|
|
||||||
case QZ_TAB: _tab_is_down = down; break;
|
case QZ_TAB:
|
||||||
|
_tab_is_down = down;
|
||||||
|
if (down && EditBoxInGlobalFocus()) {
|
||||||
|
HandleKeypress(WKC_TAB, unicode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case QZ_RETURN:
|
case QZ_RETURN:
|
||||||
case QZ_f:
|
case QZ_f:
|
||||||
|
|
Loading…
Reference in New Issue