• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Return-To-The-Roots / s25client / 29586693417

17 Jul 2026 02:07PM UTC coverage: 50.623% (+0.2%) from 50.465%
29586693417

Pull #1951

github

web-flow
Merge 997c45d00 into 4b3146470
Pull Request #1951: Addon presets

164 of 243 new or added lines in 9 files covered. (67.49%)

6 existing lines in 4 files now uncovered.

23468 of 46358 relevant lines covered (50.62%)

47686.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.92
/libs/s25main/controls/ctrlEdit.cpp
1
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "ctrlEdit.h"
6
#include "CollisionDetection.h"
7
#include "RTTR_Assert.h"
8
#include "ctrlTextDeepening.h"
9
#include "driver/MouseCoords.h"
10
#include "drivers/VideoDriverWrapper.h"
11
#include "helpers/containerUtils.h"
12
#include "ogl/FontStyle.h"
13
#include "ogl/glFont.h"
14
#include "s25util/StringConversion.h"
15
#include "s25util/fileFuncs.h"
16
#include <s25util/utf8.h>
17
#include <boost/algorithm/string/trim.hpp>
18
#include <boost/nowide/detail/utf.hpp>
19
#include <numeric>
20

21
ctrlEdit::ctrlEdit(Window* parent, unsigned id, const DrawPoint& pos, const Extent& size, TextureColor tc,
44✔
22
                   const glFont* font, unsigned short maxlength, bool password, bool disabled, bool notify)
44✔
23
    : Window(parent, id, pos, size), maxLength_(maxlength), isPassword_(password), isDisabled_(disabled),
24
      notify_(notify)
44✔
25
{
26
    txtCtrl = static_cast<ctrlTextDeepening*>(
44✔
27
      AddTextDeepening(0, DrawPoint(0, 0), size, tc, "", font, COLOR_YELLOW, FontStyle::LEFT | FontStyle::VCENTER));
44✔
28
    UpdateInternalText();
44✔
29
}
44✔
30

31
/**
32
 *  setzt den Text.
33
 *
34
 *  @param[in] text Der Text.
35
 */
36
void ctrlEdit::SetText(const std::string& text)
109✔
37
{
38
    text_ = s25util::utf8to32(text);
109✔
39
    if(editType_ == EditType::Number)
109✔
40
        helpers::erase_if(text_, [](char32_t c) { return c < '0' || c > '9'; });
×
41
    if(editType_ == EditType::Filename)
109✔
42
        helpers::erase_if(text_, [](char32_t c) { return !isValidFileNameChar(c); });
592✔
43
    if(maxLength_ > 0 && text_.size() > maxLength_)
109✔
44
        text_.resize(maxLength_);
1✔
45

46
    viewStart_ = 0;
109✔
47
    cursorPos_ = text_.length();
109✔
48
    UpdateInternalText();
109✔
49
    Notify();
109✔
50
}
109✔
51

52
void ctrlEdit::SetText(const unsigned text)
1✔
53
{
54
    SetText(s25util::toStringClassic(text));
1✔
55
}
1✔
56

57
std::string ctrlEdit::GetText() const
92✔
58
{
59
    return s25util::utf32to8(text_);
92✔
60
}
61

62
GetFileNameResult ctrlEdit::GetFileName(const std::string& ext) const
40✔
63
{
64
    RTTR_Assert(editType_ == EditType::Filename);
40✔
65

66
    std::string name = GetText();
80✔
67
    boost::algorithm::trim_if(name, [](char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; });
115✔
68
    if(name.empty())
40✔
69
        return {FileNameStatus::Empty, {}};
6✔
70
    if(!ext.empty())
34✔
71
        name += ext;
33✔
72
    if(!isValidFileName(name))
34✔
73
        return {FileNameStatus::Invalid, {}};
1✔
74
    return {FileNameStatus::Valid, std::move(name)};
33✔
75
}
76

77
void ctrlEdit::SetFocus(bool focus)
42✔
78
{
79
    if(focus_ != focus)
42✔
80
    {
81
        focus_ = focus;
10✔
82
        txtCtrl->SetTextColor(focus_ ? 0xFFFFA000 : COLOR_YELLOW);
10✔
83
        if(focus && GetParent())
10✔
84
        {
85
            for(auto* edit : GetParent()->GetCtrls<ctrlEdit>())
12✔
86
            {
87
                if(edit != this)
8✔
88
                    edit->SetFocus(false);
4✔
89
            }
90
        }
91
    }
92
}
42✔
93

94
static void removeFirstCharFromString(std::string& str)
692✔
95
{
96
    auto it = str.begin();
692✔
97
    boost::nowide::detail::utf::utf_traits<char>::decode_valid(it);
692✔
98
    str.erase(str.begin(), it);
692✔
99
}
692✔
100

101
void ctrlEdit::UpdateInternalText()
454✔
102
{
103
    if(text_.empty())
454✔
104
    {
105
        viewStart_ = 0;
84✔
106
        cursorPos_ = 0;
84✔
107
        cursorOffsetX_ = 0;
84✔
108
        txtCtrl->SetText("");
84✔
109
    } else
110
    {
111
        std::u32string dtext = (isPassword_) ? std::u32string(text_.size(), '*') : text_;
740✔
112
        viewStart_ = std::min<unsigned>(viewStart_, dtext.length() - 1);
370✔
113
        const auto* font = txtCtrl->GetFont();
370✔
114
        const unsigned max_width = GetSize().x - ctrlTextDeepening::borderSize.x * 2;
370✔
115
        unsigned max;
116
        std::string curText = s25util::utf32to8(dtext.substr(viewStart_));
740✔
117
        font->getWidth(curText, max_width, &max);
370✔
118
        // Add chars at front as long as full text is shown
119
        while(viewStart_ > 0 && curText.length() <= max)
484✔
120
        {
121
            --viewStart_;
114✔
122
            curText = s25util::utf32to8(dtext.substr(viewStart_));
114✔
123
            font->getWidth(curText, max_width, &max);
114✔
124
        }
125
        // Remove chars from front until remaining string can be fully shown
126
        while(curText.length() > max)
1,062✔
127
        {
128
            ++viewStart_;
692✔
129
            removeFirstCharFromString(curText);
692✔
130
            font->getWidth(curText, max_width, &max);
692✔
131
        }
132

133
        // Show (up to) 5 chars before the cursor
134
        if(viewStart_ + 5 > cursorPos_)
370✔
135
        {
136
            viewStart_ = std::max(0, static_cast<int>(cursorPos_) - 5);
137✔
137
            curText = s25util::utf32to8(dtext.substr(viewStart_));
137✔
138
        }
139
        if(cursorPos_ > viewStart_)
370✔
140
            cursorOffsetX_ = font->getWidth(s25util::utf32to8(dtext.substr(viewStart_, cursorPos_ - viewStart_)));
364✔
141
        else
142
            cursorOffsetX_ = 0;
6✔
143
        txtCtrl->SetText(curText);
370✔
144
    }
145
}
454✔
146

147
inline void ctrlEdit::CursorLeft()
148✔
148
{
149
    if(cursorPos_ == 0)
148✔
150
        return;
5✔
151
    --cursorPos_;
143✔
152
    UpdateInternalText();
143✔
153
}
154

155
inline void ctrlEdit::CursorRight()
163✔
156
{
157
    if(cursorPos_ == text_.length())
163✔
158
        return;
5✔
159
    ++cursorPos_;
158✔
160
    UpdateInternalText();
158✔
161
}
162

163
/**
164
 *  zeichnet das Fenster.
165
 *
166
 *  @todo muss alles überarbeitet werden
167
 */
168
void ctrlEdit::Draw_()
1✔
169
{
170
    Window::Draw_();
1✔
171
    // Alle 500ms Cursor für 500ms anzeigen
172
    if(focus_ && !isDisabled_ && VIDEODRIVER.GetTickCount() % 1000 < 500)
1✔
173
    {
174
        const auto fontHeight = txtCtrl->GetFont()->getHeight();
1✔
175
        DrawPoint cursorDrawPos = GetDrawPos();
1✔
176
        cursorDrawPos.x += cursorOffsetX_ + ctrlTextDeepening::borderSize.x;
1✔
177
        cursorDrawPos.y += (GetSize().y - (fontHeight + 2)) / 2;
1✔
178

179
        DrawRectangle(Rect(cursorDrawPos, Extent(1, fontHeight + 2)), 0xFFFFA000);
1✔
180
    }
181
}
1✔
182

183
/**
184
 *  fügt ein Zeichen zum Text hinzu
185
 *
186
 *  @param[in] text Das Zeichen
187
 */
188
void ctrlEdit::AddChar(char32_t c)
56✔
189
{
190
    if(editType_ == EditType::Number && (c < '0' || c > '9'))
56✔
NEW
191
        return;
×
192
    if(editType_ == EditType::Filename && !isValidFileNameChar(c))
56✔
193
        return;
9✔
194

195
    if(maxLength_ > 0 && text_.size() >= maxLength_)
47✔
196
        return;
2✔
197

198
    text_.insert(cursorPos_, 1, c);
45✔
199
    CursorRight();
45✔
200
    Notify();
45✔
201
}
202

203
/**
204
 *  entfernt ein Zeichen
205
 */
206
void ctrlEdit::RemoveChar()
35✔
207
{
208
    if(cursorPos_ > 0 && !text_.empty())
35✔
209
    {
210
        text_.erase(cursorPos_ - 1, 1);
30✔
211

212
        // View verschieben
213
        while(!text_.empty() && text_.length() <= viewStart_)
30✔
214
            --viewStart_;
×
215

216
        CursorLeft();
30✔
217
        Notify();
30✔
218
    }
219
}
35✔
220

221
/**
222
 *  benachrichtigt das Parent ("OnChange")
223
 */
224
void ctrlEdit::Notify()
184✔
225
{
226
    if(!notify_ || !GetParent())
184✔
227
        return;
180✔
228

229
    GetParent()->Msg_EditChange(GetID());
4✔
230
}
231

232
void ctrlEdit::Resize(const Extent& newSize)
×
233
{
234
    Window::Resize(newSize);
×
235
    UpdateInternalText();
×
236
}
×
237

238
/**
239
 *  Maustaste-gedrückt Callback
240
 */
241
bool ctrlEdit::Msg_LeftDown(const MouseCoords& mc)
34✔
242
{
243
    SetFocus(IsPointInRect(mc.pos, GetDrawRect()));
34✔
244
    return false; // "Unhandled" so other edits can handle this too and set their focus accordingly
34✔
245
}
246

247
/**
248
 *  Taste-gedrückt Callback
249
 */
250
bool ctrlEdit::Msg_KeyDown(const KeyEvent& ke)
328✔
251
{
252
    auto isDelimiter = [](char32_t c) {
×
253
        for(const auto cur : U" \t\n-+=")
×
254
        {
255
            if(cur == c)
×
256
                return true;
×
257
        }
258
        return false;
×
259
    };
260

261
    // hat das Steuerelement den Fokus?
262
    if(!focus_)
328✔
263
        return false;
×
264

265
    switch(ke.kt)
328✔
266
    {
267
        default: return false;
1✔
268

269
        case KeyType::Left: // Cursor nach Links
118✔
270
            // Blockweise nach links, falls Strg gedrückt
271
            if(ke.ctrl)
118✔
272
            {
273
                // Erst über alle Trennzeichen hinweg
274
                while(cursorPos_ > 0 && isDelimiter(text_[cursorPos_ - 1]))
×
275
                {
276
                    --cursorPos_;
×
277
                }
278

279
                // Und dann über alles, was kein Trenner ist
280
                while(cursorPos_ > 0 && !isDelimiter(text_[cursorPos_ - 1]))
×
281
                {
282
                    --cursorPos_;
×
283
                }
284
                UpdateInternalText();
×
285
            } else
286
                CursorLeft(); // just one step
118✔
287
            break;
118✔
288

289
        case KeyType::Right: // Cursor nach Rechts
118✔
290
            // Blockweise nach rechts, falls Strg gedrückt
291
            if(ke.ctrl)
118✔
292
            {
293
                // Erst über alle Trennzeichen hinweg
294
                while(cursorPos_ + 1 < text_.length() && isDelimiter(text_[cursorPos_ + 1]))
×
295
                {
296
                    ++cursorPos_;
×
297
                }
298
                // Und dann über alles, was kein Trenner ist
299
                while(cursorPos_ + 1 < text_.length() && !isDelimiter(text_[cursorPos_ + 1]))
×
300
                {
301
                    ++cursorPos_;
×
302
                }
303
                UpdateInternalText();
×
304
            } else
305
                CursorRight(); // just one step
118✔
306
            break;
118✔
307

308
        case KeyType::Char: // Zeichen eingegeben
56✔
309
            if(!isDisabled_ && txtCtrl->GetFont()->CharExist(ke.c))
56✔
310
                AddChar(ke.c);
56✔
311
            break;
56✔
312

313
        case KeyType::Backspace: // Backspace gedrückt
35✔
314
            if(!isDisabled_)
35✔
315
                RemoveChar();
35✔
316
            break;
35✔
317

318
        case KeyType::Delete: // Entfernen gedrückt
×
319
            if(!isDisabled_ && cursorPos_ < text_.length())
×
320
            {
321
                CursorRight();
×
322
                RemoveChar();
×
323
            }
324
            break;
×
325

326
        case KeyType::Return: // Enter gedrückt
×
327
            if(!isDisabled_ && GetParent())
×
328
                GetParent()->Msg_EditEnter(GetID());
×
329
            break;
×
330

331
        case KeyType::Home: // Pos1 gedrückt
×
332
            if(cursorPos_ > 0)
×
333
            {
334
                cursorPos_ = 0;
×
335
                UpdateInternalText();
×
336
            }
337
            break;
×
338

339
        case KeyType::End: // Ende gedrückt
×
340
            if(cursorPos_ < text_.length())
×
341
            {
342
                cursorPos_ = text_.length();
×
343
                UpdateInternalText();
×
344
            }
345
            break;
×
346
    }
347

348
    return true;
327✔
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc