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

Return-To-The-Roots / s25client / 20618416952

31 Dec 2025 11:51AM UTC coverage: 50.569% (+0.06%) from 50.506%
20618416952

Pull #1850

github

web-flow
Merge c015ce8be into 109e7720c
Pull Request #1850: Refactor handling of mouse messages

57 of 103 new or added lines in 4 files covered. (55.34%)

14 existing lines in 4 files now uncovered.

22569 of 44630 relevant lines covered (50.57%)

35759.33 hits per line

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

0.0
/libs/s25main/desktops/dskOptions.cpp
1
// Copyright (C) 2005 - 2025 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "dskOptions.h"
6
#include "GlobalGameSettings.h"
7
#include "GlobalVars.h"
8
#include "Loader.h"
9
#include "MusicPlayer.h"
10
#include "Settings.h"
11
#include "WindowManager.h"
12
#include "controls/ctrlComboBox.h"
13
#include "controls/ctrlEdit.h"
14
#include "controls/ctrlGroup.h"
15
#include "controls/ctrlImageButton.h"
16
#include "controls/ctrlOptionGroup.h"
17
#include "controls/ctrlProgress.h"
18
#include "driver/VideoDriver.h"
19
#include "drivers/AudioDriverWrapper.h"
20
#include "drivers/VideoDriverWrapper.h"
21
#include "dskMainMenu.h"
22
#include "helpers/containerUtils.h"
23
#include "helpers/format.hpp"
24
#include "helpers/mathFuncs.h"
25
#include "helpers/toString.h"
26
#include "ingameWindows/iwAddons.h"
27
#include "ingameWindows/iwMsgbox.h"
28
#include "ingameWindows/iwMusicPlayer.h"
29
#include "ingameWindows/iwTextfile.h"
30
#include "languages.h"
31
#include "ogl/FontStyle.h"
32
#include "gameData/PortraitConsts.h"
33
#include "s25util/StringConversion.h"
34
#include "s25util/colors.h"
35
#include <mygettext/mygettext.h>
36
#include <sstream>
37

38
namespace {
39
using Offset = DrawPoint;
40

41
enum
42
{
43
    ID_btBack = dskMenuBase::ID_FIRST_FREE,
44
    ID_txtOptions,
45
    ID_btAddons,
46
    ID_grpOptions,
47
    ID_btCommon,
48
    ID_btGraphics,
49
    ID_btSound,
50
    ID_grpCommon,
51
    ID_grpGraphics,
52
    ID_grpSound,
53
    ID_txtName,
54
    ID_edtName,
55
    ID_txtLanguage,
56
    ID_cbLanguage,
57
    ID_txtKeyboardLayout,
58
    ID_btKeyboardLayout,
59
    ID_txtPort,
60
    ID_edtPort,
61
    ID_txtIpv6,
62
    ID_grpIpv6,
63
    ID_txtProxy,
64
    ID_edtProxy,
65
    ID_edtProxyPort,
66
    ID_txtProxyType,
67
    ID_cbProxyType,
68
    ID_txtDebugData,
69
    ID_grpDebugData,
70
    ID_txtUPNP,
71
    ID_grpUPNP,
72
    ID_txtInvertScroll,
73
    ID_grpInvertScroll,
74
    ID_txtSmartCursor,
75
    ID_grpSmartCursor,
76
    ID_txtWindowPinning,
77
    ID_grpWindowPinning,
78
    ID_txtGFInfo,
79
    ID_grpGFInfo,
80
    ID_txtResolution,
81
    ID_cbResolution,
82
    ID_txtFullscreen,
83
    ID_grpFullscreen,
84
    ID_txtFramerate,
85
    ID_cbFramerate,
86
    ID_txtVBO,
87
    ID_grpVBO,
88
    ID_txtVideoDriver,
89
    ID_cbVideoDriver,
90
    ID_txtOptTextures,
91
    ID_grpOptTextures,
92
    ID_txtGuiScale,
93
    ID_cbGuiScale,
94
    ID_txtAudioDriver,
95
    ID_cbAudioDriver,
96
    ID_txtMusic,
97
    ID_grpMusic,
98
    ID_pgMusicVol,
99
    ID_txtEffects,
100
    ID_grpEffects,
101
    ID_pgEffectsVol,
102
    ID_btMusicPlayer,
103
    ID_txtCommonPortrait,
104
    ID_btCommonPortrait,
105
    ID_cbCommonPortrait,
106
    ID_txtBirdSounds,
107
    ID_grpBirdSounds,
108
};
109
// Use these as IDs in dedicated groups
110
constexpr auto ID_btOn = 1;
111
constexpr auto ID_btOff = 0;
112
// Special case: Submit debug data uses "2" for "ask user" and "0" for "unset, ask at start"
113
constexpr auto ID_btSubmitDebugOn = 1;
114
constexpr auto ID_btSubmitDebugAsk = 2;
115

116
constexpr auto rowHeight = 30;
117
constexpr auto sectionSpacing = 20;
118
constexpr auto sectionSpacingCommon = 10;
119
constexpr auto optionRowsStartPosition = DrawPoint(80, 75);
120
constexpr auto tabButtonsStartPosition = optionRowsStartPosition + Offset(0, static_cast<int>(rowHeight * 15.5));
121

122
constexpr Offset ctrlOffset(200, -5);                       // Offset of control to its description text
123
constexpr Offset ctrlOffset2 = ctrlOffset + Offset(200, 0); // Offset of 2nd control to its description text
124
constexpr Extent ctrlSize(190, 22);
125
constexpr Extent ctrlSizeLarge = ctrlSize + Extent(ctrlOffset2 - ctrlOffset);
126
} // namespace
127

128
static VideoMode getAspectRatio(const VideoMode& vm)
×
129
{
130
    // First some a bit off values where the aspect ratio is defined by convention
131
    if(vm == VideoMode(1360, 1024))
×
132
        return VideoMode(4, 3);
×
133
    else if(vm == VideoMode(1360, 768) || vm == VideoMode(1366, 768))
×
134
        return VideoMode(16, 9);
×
135

136
    // Normally Aspect ration is simply width/height as integer numbers (e.g. 4:3)
137
    int divisor = helpers::gcd(vm.width, vm.height);
×
138
    VideoMode ratio(vm.width / divisor, vm.height / divisor);
×
139
    // But there are some special cases:
140
    if(ratio == VideoMode(8, 5))
×
141
        return VideoMode(16, 10);
×
142
    else if(ratio == VideoMode(5, 3))
×
143
        return VideoMode(15, 9);
×
144
    else
145
        return ratio;
×
146
}
147

148
dskOptions::dskOptions() : Desktop(LOADER.GetImageN("setup013", 0))
×
149
{
150
    AddText(ID_txtOptions, DrawPoint(400, 10), _("Options"), COLOR_YELLOW, FontStyle::CENTER, LargeFont);
×
151

152
    ctrlOptionGroup* mainGroup = AddOptionGroup(ID_grpOptions, GroupSelectType::Check);
×
153

154
    DrawPoint curPos = tabButtonsStartPosition;
×
155
    mainGroup->AddTextButton(ID_btCommon, DrawPoint(curPos.x, curPos.y), Extent(200, 22), TextureColor::Green2,
×
156
                             _("Common"), NormalFont);
×
157
    mainGroup->AddTextButton(ID_btGraphics, DrawPoint(curPos.x + 220, curPos.y), Extent(200, 22), TextureColor::Green2,
×
158
                             _("Graphics"), NormalFont);
×
159
    mainGroup->AddTextButton(ID_btSound, DrawPoint(curPos.x + 440, curPos.y), Extent(200, 22), TextureColor::Green2,
×
160
                             _("Sound/Music"), NormalFont);
×
NEW
161
    curPos.y += rowHeight;
×
162

163
    AddTextButton(ID_btBack, DrawPoint(curPos.x + 220, curPos.y), Extent(200, 22), TextureColor::Red1, _("Back"),
×
164
                  NormalFont);
×
165
    AddTextButton(ID_btAddons, DrawPoint(curPos.x + 440, curPos.y), Extent(200, 22), TextureColor::Green2, _("Addons"),
×
166
                  NormalFont);
×
167

168
    ctrlGroup* groupCommon = AddGroup(ID_grpCommon);
×
169
    ctrlGroup* groupGraphics = AddGroup(ID_grpGraphics);
×
170
    ctrlGroup* groupSound = AddGroup(ID_grpSound);
×
171
    ctrlComboBox* combo;
172

173
    // Common
174
    // {
175

176
    curPos = optionRowsStartPosition;
×
177

178
    groupCommon->AddText(ID_txtName, curPos, _("Name in Game:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
179
    ctrlEdit* name =
180
      groupCommon->AddEdit(ID_edtName, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont, 15);
×
181
    name->SetText(SETTINGS.lobby.name);
×
182

183
    const auto& currentPortrait = Portraits[SETTINGS.lobby.portraitIndex];
×
NEW
184
    groupCommon->AddImageButton(ID_btCommonPortrait, DrawPoint(500, curPos.y + ctrlOffset.y), Extent(40, rowHeight * 2),
×
185
                                TextureColor::Grey,
186
                                LOADER.GetImageN(currentPortrait.resourceId, currentPortrait.resourceIndex));
×
187
    curPos.y += rowHeight;
×
188

NEW
189
    groupCommon->AddText(ID_txtCommonPortrait, curPos, _("Portrait:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
190
    combo =
NEW
191
      groupCommon->AddComboBox(ID_cbCommonPortrait, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont, 100);
×
192

193
    for(unsigned i = 0; i < Portraits.size(); ++i)
×
194
    {
195
        combo->AddString(_(Portraits[i].name));
×
196
        if(SETTINGS.lobby.portraitIndex == i)
×
UNCOV
197
            combo->SetSelection(i);
×
198
    }
UNCOV
199
    curPos.y += rowHeight;
×
200

201
    groupCommon->AddText(ID_txtLanguage, curPos, _("Language:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
202
    combo = groupCommon->AddComboBox(ID_cbLanguage, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont, 100);
×
203

204
    bool selected = false;
×
205
    for(unsigned i = 0; i < LANGUAGES.size(); ++i)
×
206
    {
207
        const Language& l = LANGUAGES.getLanguage(i);
×
208

209
        combo->AddString(_(l.name));
×
210
        if(SETTINGS.language.language == l.code)
×
211
        {
212
            combo->SetSelection(static_cast<unsigned short>(i));
×
213
            selected = true;
×
214
        }
215
    }
216
    if(!selected)
×
217
        combo->SetSelection(0);
×
218
    curPos.y += rowHeight;
×
219

220
    groupCommon->AddTextButton(ID_btKeyboardLayout, curPos + ctrlOffset, ctrlSizeLarge, TextureColor::Grey,
×
221
                               _("Keyboard layout"), NormalFont);
×
NEW
222
    curPos.y += rowHeight;
×
223

NEW
224
    curPos.y += sectionSpacingCommon;
×
UNCOV
225
    groupCommon->AddText(ID_txtPort, curPos, _("Local Port:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
226
    ctrlEdit* edtPort =
227
      groupCommon->AddEdit(ID_edtPort, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont, 15);
×
228
    edtPort->SetNumberOnly(true);
×
229
    edtPort->SetText(SETTINGS.server.localPort);
×
230
    curPos.y += rowHeight;
×
231

232
    // IPv4/6
233
    groupCommon->AddText(ID_txtIpv6, curPos, _("Use IPv6:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
234

235
    ctrlOptionGroup* ipv6 = groupCommon->AddOptionGroup(ID_grpIpv6, GroupSelectType::Check);
×
236
    ipv6->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("IPv6"), NormalFont);
×
237
    ipv6->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("IPv4"), NormalFont);
×
238
    ipv6->SetSelection(SETTINGS.server.ipv6);
×
239
    // Enable/disable the IPv6 field if necessary
240
    ipv6->GetCtrl<ctrlButton>(1)->SetEnabled(SETTINGS.proxy.type != ProxyType::Socks5); //-V807
×
NEW
241
    curPos.y += rowHeight;
×
242

NEW
243
    curPos.y += sectionSpacingCommon;
×
244
    // Proxy server
245
    groupCommon->AddText(ID_txtProxy, curPos, _("Proxyserver:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
246
    ctrlEdit* proxy = groupCommon->AddEdit(ID_edtProxy, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont);
×
247
    proxy->SetText(SETTINGS.proxy.hostname);
×
248
    proxy =
249
      groupCommon->AddEdit(ID_edtProxyPort, curPos + ctrlOffset2, Extent(50, 22), TextureColor::Grey, NormalFont, 5);
×
250
    proxy->SetNumberOnly(true);
×
251
    proxy->SetText(SETTINGS.proxy.port);
×
252
    curPos.y += rowHeight;
×
253

254
    groupCommon->AddText(ID_txtUPNP, curPos, _("Use UPnP"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
255
    ctrlOptionGroup* upnp = groupCommon->AddOptionGroup(ID_grpUPNP, GroupSelectType::Check);
×
256
    upnp->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont);
×
257
    upnp->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont);
×
258
    upnp->SetSelection(SETTINGS.global.use_upnp);
×
259
    curPos.y += rowHeight;
×
260

261
    // Proxy type
262
    groupCommon->AddText(ID_txtProxyType, curPos, _("Proxytyp:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
263
    combo =
264
      groupCommon->AddComboBox(ID_cbProxyType, curPos + ctrlOffset, ctrlSizeLarge, TextureColor::Grey, NormalFont, 100);
×
265
    combo->AddString(_("No Proxy"));
×
266
    combo->AddString(_("Socks v4"));
×
267
    // TODO: not implemented
268
    // combo->AddString(_("Socks v5"));
269

270
    switch(SETTINGS.proxy.type)
×
271
    {
272
        default: combo->SetSelection(0); break;
×
273
        case ProxyType::Socks4: combo->SetSelection(1); break;
×
274
        case ProxyType::Socks5: combo->SetSelection(2); break;
×
275
    }
NEW
276
    curPos.y += rowHeight;
×
277

NEW
278
    curPos.y += sectionSpacingCommon;
×
279
    groupCommon->AddText(ID_txtInvertScroll, curPos, _("Invert Mouse Pan:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
280
    ctrlOptionGroup* invertScroll = groupCommon->AddOptionGroup(ID_grpInvertScroll, GroupSelectType::Check);
×
281
    invertScroll->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont,
×
282
                                _("Map moves in the opposite direction the mouse is moved when scrolling/panning."));
283
    invertScroll->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont,
×
284
                                _("Map moves in the same direction the mouse is moved when scrolling/panning."));
285
    invertScroll->SetSelection(SETTINGS.interface.invertMouse);
×
286
    curPos.y += rowHeight;
×
287

288
    groupCommon->AddText(ID_txtSmartCursor, curPos, _("Smart Cursor"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
289
    ctrlOptionGroup* smartCursor = groupCommon->AddOptionGroup(ID_grpSmartCursor, GroupSelectType::Check);
×
290
    smartCursor->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont,
×
291
                               _("Place cursor on default button for new dialogs / action windows (default)"));
292
    smartCursor->AddTextButton(
×
293
      ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont,
×
294
      _("Don't move cursor automatically\nUseful e.g. for split-screen / dual-mice multiplayer (see wiki)"));
295
    smartCursor->SetSelection(SETTINGS.global.smartCursor);
×
NEW
296
    curPos.y += rowHeight;
×
297

NEW
298
    groupCommon->AddText(ID_txtWindowPinning, curPos, _("Window pinning"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
NEW
299
    ctrlOptionGroup* windowPinning = groupCommon->AddOptionGroup(ID_grpWindowPinning, GroupSelectType::Check);
×
NEW
300
    windowPinning->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont,
×
301
                                 _("Replace minimize button on windows by pin button avoiding closing the window with "
302
                                   "ESC.\nMinimize by double-clicking the title bar."));
NEW
303
    windowPinning->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont);
×
NEW
304
    windowPinning->SetSelection(SETTINGS.interface.enableWindowPinning);
×
NEW
305
    curPos.y += rowHeight;
×
306

NEW
307
    curPos.y += sectionSpacingCommon;
×
308
    groupCommon->AddText(ID_txtDebugData, curPos, _("Submit debug data:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
309
    mainGroup = groupCommon->AddOptionGroup(ID_grpDebugData, GroupSelectType::Check);
×
310
    mainGroup->AddTextButton(ID_btSubmitDebugOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"),
×
311
                             NormalFont);
×
312
    mainGroup->AddTextButton(ID_btSubmitDebugAsk, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Ask always"),
×
313
                             NormalFont);
×
314

315
    mainGroup->SetSelection((SETTINGS.global.submit_debug_data == 1) ? ID_btSubmitDebugOn :
×
316
                                                                       ID_btSubmitDebugAsk); //-V807
317
    curPos.y += rowHeight;
×
318

319
    groupCommon->AddText(ID_txtGFInfo, curPos, _("Show GameFrame Info:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
320
    mainGroup = groupCommon->AddOptionGroup(ID_grpGFInfo, GroupSelectType::Check);
×
321
    mainGroup->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont);
×
322
    mainGroup->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont);
×
323

324
    mainGroup->SetSelection(SETTINGS.global.showGFInfo);
×
325

326
    curPos = optionRowsStartPosition;
×
327
    groupGraphics->AddText(ID_txtResolution, curPos, _("Fullscreen resolution:"), COLOR_YELLOW, FontStyle{},
×
328
                           NormalFont);
×
329
    groupGraphics->AddComboBox(ID_cbResolution, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont, 150);
×
NEW
330
    curPos.y += rowHeight;
×
331

NEW
332
    curPos.y += sectionSpacing;
×
333
    groupGraphics->AddText(ID_txtFullscreen, curPos, _("Mode:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
334
    mainGroup = groupGraphics->AddOptionGroup(ID_grpFullscreen, GroupSelectType::Check);
×
335
    mainGroup->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("Fullscreen"), NormalFont);
×
336
    mainGroup->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Windowed"), NormalFont);
×
NEW
337
    curPos.y += rowHeight;
×
338

NEW
339
    curPos.y += sectionSpacing;
×
340
    groupGraphics->AddText(ID_txtFramerate, curPos, _("Limit Framerate:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
341
    groupGraphics->AddComboBox(ID_cbFramerate, curPos + ctrlOffset, ctrlSizeLarge, TextureColor::Grey, NormalFont, 150);
×
NEW
342
    curPos.y += rowHeight;
×
343

NEW
344
    curPos.y += sectionSpacing;
×
345
    groupGraphics->AddText(ID_txtVBO, curPos, _("Vertex Buffer Objects:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
346
    mainGroup = groupGraphics->AddOptionGroup(ID_grpVBO, GroupSelectType::Check);
×
347
    mainGroup->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont);
×
348
    mainGroup->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont);
×
NEW
349
    curPos.y += rowHeight;
×
350

NEW
351
    curPos.y += sectionSpacing;
×
352
    groupGraphics->AddText(ID_txtVideoDriver, curPos, _("Graphics Driver"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
353
    combo = groupGraphics->AddComboBox(ID_cbVideoDriver, curPos + ctrlOffset, ctrlSizeLarge, TextureColor::Grey,
×
354
                                       NormalFont, 100);
×
355

356
    const auto video_drivers = drivers::DriverWrapper::LoadDriverList(drivers::DriverType::Video);
×
357

358
    for(const auto& video_driver : video_drivers)
×
359
    {
360
        combo->AddString(video_driver.GetName());
×
361
        if(video_driver.GetName() == SETTINGS.driver.video)
×
362
            combo->SetSelection(combo->GetNumItems() - 1);
×
363
    }
NEW
364
    curPos.y += rowHeight;
×
365

NEW
366
    curPos.y += sectionSpacing;
×
367
    groupGraphics->AddText(ID_txtOptTextures, curPos, _("Optimized Textures:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
368
    mainGroup = groupGraphics->AddOptionGroup(ID_grpOptTextures, GroupSelectType::Check);
×
369

370
    mainGroup->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont);
×
371
    mainGroup->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont);
×
NEW
372
    curPos.y += rowHeight;
×
373

NEW
374
    curPos.y += sectionSpacing;
×
375
    groupGraphics->AddText(ID_txtGuiScale, curPos, _("GUI Scale:"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
376
    groupGraphics->AddComboBox(ID_cbGuiScale, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, NormalFont, 100);
×
377
    updateGuiScale();
×
378

379
    curPos = optionRowsStartPosition;
×
380
    constexpr Offset bt1Offset(200, -5);
×
381
    constexpr Offset bt2Offset(300, -5);
×
382
    constexpr Offset volOffset(400, -5);
×
383
    constexpr Extent ctrlSizeSmall(90, ctrlSize.y);
×
384

385
    groupSound->AddText(ID_txtEffects, curPos, _("Effects"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
386
    mainGroup = groupSound->AddOptionGroup(ID_grpEffects, GroupSelectType::Check);
×
387
    mainGroup->AddTextButton(ID_btOn, curPos + bt1Offset, ctrlSizeSmall, TextureColor::Grey, _("On"), NormalFont);
×
388
    mainGroup->AddTextButton(ID_btOff, curPos + bt2Offset, ctrlSizeSmall, TextureColor::Grey, _("Off"), NormalFont);
×
389

390
    ctrlProgress* FXvolume =
391
      groupSound->AddProgress(ID_pgEffectsVol, curPos + volOffset, ctrlSize, TextureColor::Grey, 139, 138, 100);
×
392
    FXvolume->SetPosition((SETTINGS.sound.effectsVolume * 100) / 255);
×
NEW
393
    curPos.y += rowHeight;
×
394

NEW
395
    curPos.y += sectionSpacing;
×
396
    groupSound->AddText(ID_txtBirdSounds, curPos, _("Bird sounds"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
397
    mainGroup = groupSound->AddOptionGroup(ID_grpBirdSounds, GroupSelectType::Check);
×
398
    mainGroup->AddTextButton(ID_btOn, curPos + bt1Offset, ctrlSizeSmall, TextureColor::Grey, _("On"), NormalFont);
×
399
    mainGroup->AddTextButton(ID_btOff, curPos + bt2Offset, ctrlSizeSmall, TextureColor::Grey, _("Off"), NormalFont);
×
NEW
400
    curPos.y += rowHeight;
×
401

NEW
402
    curPos.y += sectionSpacing;
×
403
    groupSound->AddText(ID_txtMusic, curPos, _("Music"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
404
    mainGroup = groupSound->AddOptionGroup(ID_grpMusic, GroupSelectType::Check);
×
405
    mainGroup->AddTextButton(ID_btOn, curPos + bt1Offset, ctrlSizeSmall, TextureColor::Grey, _("On"), NormalFont);
×
406
    mainGroup->AddTextButton(ID_btOff, curPos + bt2Offset, ctrlSizeSmall, TextureColor::Grey, _("Off"), NormalFont);
×
407

408
    ctrlProgress* Mvolume =
409
      groupSound->AddProgress(ID_pgMusicVol, curPos + volOffset, ctrlSize, TextureColor::Grey, 139, 138, 100);
×
410
    Mvolume->SetPosition((SETTINGS.sound.musicVolume * 100) / 255); //-V807
×
NEW
411
    curPos.y += rowHeight;
×
412

NEW
413
    curPos.y += sectionSpacing;
×
414
    groupSound->AddTextButton(ID_btMusicPlayer, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("Music player"),
×
415
                              NormalFont);
×
NEW
416
    curPos.y += rowHeight;
×
417

NEW
418
    curPos.y += sectionSpacing;
×
419
    groupSound->AddText(ID_txtAudioDriver, curPos, _("Sounddriver"), COLOR_YELLOW, FontStyle{}, NormalFont);
×
420
    combo = groupSound->AddComboBox(ID_cbAudioDriver, curPos + ctrlOffset, ctrlSizeLarge, TextureColor::Grey,
×
421
                                    NormalFont, 100);
×
422

423
    const auto audio_drivers = drivers::DriverWrapper::LoadDriverList(drivers::DriverType::Audio);
×
424

425
    for(const auto& audio_driver : audio_drivers)
×
426
    {
427
        combo->AddString(audio_driver.GetName());
×
428
        if(audio_driver.GetName() == SETTINGS.driver.audio)
×
429
            combo->SetSelection(combo->GetNumItems() - 1);
×
430
    }
431

432
    // Select "General"
433
    mainGroup = GetCtrl<ctrlOptionGroup>(ID_grpOptions);
×
434
    mainGroup->SetSelection(ID_btCommon, true);
×
435

436
    // Graphics
437
    // {
438

439
    loadVideoModes();
×
440

441
    // and add to the combo box
442
    ctrlComboBox& cbVideoModes = *groupGraphics->GetCtrl<ctrlComboBox>(ID_cbResolution);
×
443
    for(const auto& videoMode : video_modes)
×
444
    {
445
        VideoMode ratio = getAspectRatio(videoMode);
×
446
        s25util::ClassicImbuedStream<std::ostringstream> str;
×
447
        str << videoMode.width << "x" << videoMode.height;
×
448
        // Make the length always the same as 'iiiixiiii' to align the ratio
449
        int len = str.str().length();
×
450
        for(int i = len; i < 4 + 1 + 4; i++)
×
451
            str << " ";
×
452
        str << " (" << ratio.width << ":" << ratio.height << ")";
×
453

454
        cbVideoModes.AddString(str.str());
×
455

456
        // Select, if this is the current resolution
457
        if(videoMode == SETTINGS.video.fullscreenSize) //-V807
×
458
            cbVideoModes.SetSelection(cbVideoModes.GetNumItems() - 1);
×
459
    }
460

461
    // Set "Fullscreen"
462
    groupGraphics->GetCtrl<ctrlOptionGroup>(ID_grpFullscreen)->SetSelection(SETTINGS.video.fullscreen); //-V807
×
463

464
    // Fill "Limit Framerate"
465
    auto* cbFrameRate = groupGraphics->GetCtrl<ctrlComboBox>(ID_cbFramerate);
×
466
    if(VIDEODRIVER.HasVSync())
×
467
        cbFrameRate->AddString(_("Dynamic (Limits to display refresh rate, works with most drivers)"));
×
468
    for(int framerate : Settings::SCREEN_REFRESH_RATES)
×
469
    {
470
        if(framerate == -1)
×
471
            cbFrameRate->AddString(_("Disabled"));
×
472
        else
473
            cbFrameRate->AddString(helpers::toString(framerate) + " FPS");
×
474
        if(SETTINGS.video.framerate == framerate)
×
475
            cbFrameRate->SetSelection(cbFrameRate->GetNumItems() - 1);
×
476
    }
477
    if(!cbFrameRate->GetSelection())
×
478
        cbFrameRate->SetSelection(0);
×
479

480
    groupGraphics->GetCtrl<ctrlOptionGroup>(ID_grpVBO)->SetSelection(SETTINGS.video.vbo);
×
481

482
    groupGraphics->GetCtrl<ctrlOptionGroup>(ID_grpOptTextures)->SetSelection(SETTINGS.video.shared_textures);
×
483
    // }
484

485
    // Sound
486
    // {
487

488
    groupSound->GetCtrl<ctrlOptionGroup>(ID_grpEffects)->SetSelection(SETTINGS.sound.effectsEnabled);
×
489
    groupSound->GetCtrl<ctrlOptionGroup>(ID_grpBirdSounds)->SetSelection(SETTINGS.sound.birdsEnabled);
×
490
    groupSound->GetCtrl<ctrlOptionGroup>(ID_grpMusic)->SetSelection(SETTINGS.sound.musicEnabled);
×
491

492
    // }
493

494
    // Load game settings
495
    ggs.LoadSettings();
×
496
}
×
497

498
dskOptions::~dskOptions()
×
499
{
500
    // Save game settings
501
    ggs.SaveSettings();
×
502
}
×
503

504
void dskOptions::Msg_Group_ProgressChange(const unsigned /*group_id*/, const unsigned ctrl_id,
×
505
                                          const unsigned short position)
506
{
507
    switch(ctrl_id)
×
508
    {
509
        case ID_pgEffectsVol:
×
510
            SETTINGS.sound.effectsVolume = static_cast<uint8_t>((position * 255) / 100);
×
511
            AUDIODRIVER.SetMasterEffectVolume(SETTINGS.sound.effectsVolume);
×
512
            break;
×
513
        case ID_pgMusicVol:
×
514
            SETTINGS.sound.musicVolume = static_cast<uint8_t>((position * 255) / 100);
×
515
            AUDIODRIVER.SetMusicVolume(SETTINGS.sound.musicVolume);
×
516
            break;
×
517
    }
518
}
×
519

520
void dskOptions::Msg_Group_ComboSelectItem(const unsigned group_id, const unsigned ctrl_id, const unsigned selection)
×
521
{
522
    auto* group = GetCtrl<ctrlGroup>(group_id);
×
523
    auto* combo = group->GetCtrl<ctrlComboBox>(ctrl_id);
×
524

525
    switch(ctrl_id)
×
526
    {
527
        case ID_cbCommonPortrait:
×
528
            SETTINGS.lobby.portraitIndex = selection;
×
529
            updatePortraitControls();
×
530
            break;
×
531
        case ID_cbLanguage:
×
532
        {
533
            // Language changed?
534
            std::string old_lang = SETTINGS.language.language; //-V807
×
535
            SETTINGS.language.language = LANGUAGES.setLanguage(selection);
×
536
            if(SETTINGS.language.language != old_lang)
×
537
                WINDOWMANAGER.Switch(std::make_unique<dskOptions>());
×
538
        }
539
        break;
×
540
        case ID_cbProxyType:
×
541
            switch(selection)
542
            {
543
                case 0: SETTINGS.proxy.type = ProxyType::None; break;
×
544
                case 1: SETTINGS.proxy.type = ProxyType::Socks4; break;
×
545
                case 2: SETTINGS.proxy.type = ProxyType::Socks5; break;
×
546
            }
547

548
            // Disable IPv6 visually
549
            if(SETTINGS.proxy.type == ProxyType::Socks4 && SETTINGS.server.ipv6)
×
550
            {
551
                GetCtrl<ctrlGroup>(ID_grpCommon)->GetCtrl<ctrlOptionGroup>(ID_grpIpv6)->SetSelection(0);
×
552
                GetCtrl<ctrlGroup>(ID_grpCommon)
×
553
                  ->GetCtrl<ctrlOptionGroup>(ID_grpIpv6)
×
554
                  ->GetCtrl<ctrlButton>(1)
555
                  ->SetEnabled(false);
×
556
                SETTINGS.server.ipv6 = false;
×
557
            }
558

559
            if(SETTINGS.proxy.type != ProxyType::Socks4)
×
560
                GetCtrl<ctrlGroup>(ID_grpCommon)
×
561
                  ->GetCtrl<ctrlOptionGroup>(ID_grpIpv6)
×
562
                  ->GetCtrl<ctrlButton>(1)
563
                  ->SetEnabled(true);
×
564
            break;
×
565
        case ID_cbResolution: SETTINGS.video.fullscreenSize = video_modes[selection]; break;
×
566
        case ID_cbFramerate:
×
567
            if(VIDEODRIVER.HasVSync())
×
568
            {
569
                if(selection == 0)
×
570
                    SETTINGS.video.framerate = 0;
×
571
                else
572
                    SETTINGS.video.framerate = Settings::SCREEN_REFRESH_RATES[selection - 1];
×
573
            } else
574
                SETTINGS.video.framerate = Settings::SCREEN_REFRESH_RATES[selection];
×
575

576
            VIDEODRIVER.setTargetFramerate(SETTINGS.video.framerate);
×
577
            break;
×
578
        case ID_cbVideoDriver: SETTINGS.driver.video = combo->GetText(selection); break;
×
579
        case ID_cbGuiScale:
×
580
            SETTINGS.video.guiScale = guiScales_[selection];
×
581
            VIDEODRIVER.setGuiScalePercent(SETTINGS.video.guiScale);
×
582
            break;
×
583
        case ID_cbAudioDriver: SETTINGS.driver.audio = combo->GetText(selection); break;
×
584
    }
585
}
×
586

587
void dskOptions::Msg_Group_OptionGroupChange(const unsigned /*group_id*/, const unsigned ctrl_id,
×
588
                                             const unsigned selection)
589
{
590
    const bool enabled = selection == ID_btOn;
×
591
    switch(ctrl_id)
×
592
    {
593
        case ID_grpIpv6: SETTINGS.server.ipv6 = enabled; break;
×
594
        case ID_grpFullscreen: SETTINGS.video.fullscreen = enabled; break;
×
595
        case ID_grpVBO: SETTINGS.video.vbo = enabled; break;
×
596
        case ID_grpOptTextures: SETTINGS.video.shared_textures = enabled; break;
×
597
        case ID_grpEffects: SETTINGS.sound.effectsEnabled = enabled; break;
×
598
        case ID_grpBirdSounds: SETTINGS.sound.birdsEnabled = enabled; break;
×
599
        case ID_grpMusic:
×
600
            SETTINGS.sound.musicEnabled = enabled;
×
601
            if(enabled)
×
602
                MUSICPLAYER.Play();
×
603
            else
604
                MUSICPLAYER.Stop();
×
605
            break;
×
606
        case ID_grpDebugData:
×
607
            // Special case: Uses e.g. ID_btSubmitDebugOn directly
608
            SETTINGS.global.submit_debug_data = selection;
×
609
            break;
×
610
        case ID_grpUPNP: SETTINGS.global.use_upnp = enabled; break;
×
611
        case ID_grpInvertScroll: SETTINGS.interface.invertMouse = enabled; break;
×
612
        case ID_grpSmartCursor:
×
613
            SETTINGS.global.smartCursor = enabled;
×
614
            VIDEODRIVER.SetMouseWarping(enabled);
×
615
            break;
×
NEW
616
        case ID_grpWindowPinning: SETTINGS.interface.enableWindowPinning = enabled; break;
×
UNCOV
617
        case ID_grpGFInfo: SETTINGS.global.showGFInfo = enabled; break;
×
618
    }
619
}
×
620

621
void dskOptions::Msg_OptionGroupChange(const unsigned ctrl_id, const unsigned selection)
×
622
{
623
    if(ctrl_id == ID_grpOptions)
×
624
    {
625
        const auto visGrp = selection + ID_grpCommon - ID_btCommon;
×
626
        for(const unsigned id : {ID_grpCommon, ID_grpGraphics, ID_grpSound})
×
627
            GetCtrl<ctrlGroup>(id)->SetVisible(id == visGrp);
×
628
    }
629
}
×
630

631
/// Check that the port is valid and sets outPort to it. Shows an error otherwise
632
static bool validatePort(const std::string& sPort, uint16_t& outPort)
×
633
{
634
    boost::optional<uint16_t> port = validate::checkPort(sPort);
×
635
    if(port)
×
636
        outPort = *port;
×
637
    else
638
    {
639
        WINDOWMANAGER.Show(std::make_unique<iwMsgbox>(_("Error"),
×
640
                                                      _("Invalid port. The valid port-range is 1 to 65535!"), nullptr,
×
641
                                                      MsgboxButton::Ok, MsgboxIcon::ExclamationRed, 1));
×
642
    }
643
    return static_cast<bool>(port);
×
644
}
645

646
void dskOptions::Msg_ButtonClick(const unsigned ctrl_id)
×
647
{
648
    switch(ctrl_id)
×
649
    {
650
        case ID_btBack:
×
651
        {
652
            auto* groupCommon = GetCtrl<ctrlGroup>(ID_grpCommon);
×
653

654
            // Save the name
655
            SETTINGS.lobby.name = groupCommon->GetCtrl<ctrlEdit>(ID_edtName)->GetText();
×
656
            if(!validatePort(groupCommon->GetCtrl<ctrlEdit>(ID_edtPort)->GetText(), SETTINGS.server.localPort))
×
657
                return;
×
658

659
            SETTINGS.proxy.hostname = groupCommon->GetCtrl<ctrlEdit>(ID_edtProxy)->GetText();
×
660
            if(!validatePort(groupCommon->GetCtrl<ctrlEdit>(ID_edtProxyPort)->GetText(), SETTINGS.proxy.port))
×
661
                return;
×
662

663
            SETTINGS.Save();
×
664

665
            // Is the selected backend required to support GUI scaling to fullfill the user's choice?
666
            // If so, warn the user if the backend is unable to support GUI scaling.
667
            if(VIDEODRIVER.getGuiScale().percent() == 100
×
668
               && (SETTINGS.video.guiScale != 100
×
669
                   || (SETTINGS.video.guiScale == 0 && VIDEODRIVER.getGuiScaleRange().recommendedPercent != 100)))
×
670
            {
671
                WINDOWMANAGER.Show(std::make_unique<iwMsgbox>(
×
672
                  _("Sorry!"), _("The selected video driver does not support GUI scaling! Setting won't be used."),
×
673
                  this, MsgboxButton::Ok, MsgboxIcon::ExclamationGreen, 1));
×
674
            }
675

676
            if((SETTINGS.video.fullscreen && SETTINGS.video.fullscreenSize != VIDEODRIVER.GetWindowSize()) //-V807
×
677
               || SETTINGS.video.fullscreen != VIDEODRIVER.IsFullscreen())
×
678
            {
679
                const auto screenSize =
680
                  SETTINGS.video.fullscreen ? SETTINGS.video.fullscreenSize : SETTINGS.video.windowedSize;
×
681
                if(!VIDEODRIVER.ResizeScreen(screenSize, SETTINGS.video.fullscreen))
×
682
                {
683
                    WINDOWMANAGER.Show(std::make_unique<iwMsgbox>(
×
684
                      _("Sorry!"), _("You need to restart your game to change the screen resolution!"), this,
×
685
                      MsgboxButton::Ok, MsgboxIcon::ExclamationGreen, 1));
×
686
                    return;
×
687
                }
688
            }
689
            if(SETTINGS.driver.video != VIDEODRIVER.GetName() || SETTINGS.driver.audio != AUDIODRIVER.GetName())
×
690
            {
691
                WINDOWMANAGER.Show(std::make_unique<iwMsgbox>(
×
692
                  _("Sorry!"), _("You need to restart your game to change the video or audio driver!"), this,
×
693
                  MsgboxButton::Ok, MsgboxIcon::ExclamationGreen, 1));
×
694
                return;
×
695
            }
696

697
            WINDOWMANAGER.Switch(std::make_unique<dskMainMenu>());
×
698
        }
699
        break;
×
700
        case ID_btAddons: WINDOWMANAGER.ToggleWindow(std::make_unique<iwAddons>(ggs)); break;
×
701
    }
702
}
703

704
void dskOptions::Msg_Group_ButtonClick(const unsigned /*group_id*/, const unsigned ctrl_id)
×
705
{
706
    switch(ctrl_id)
×
707
    {
708
        default: break;
×
709
        case ID_btCommonPortrait:
×
710
            SETTINGS.lobby.portraitIndex = (SETTINGS.lobby.portraitIndex + 1) % Portraits.size();
×
711
            updatePortraitControls();
×
712
            break;
×
713
        case ID_btMusicPlayer: WINDOWMANAGER.ToggleWindow(std::make_unique<iwMusicPlayer>()); break;
×
714
        case ID_btKeyboardLayout:
×
715
            WINDOWMANAGER.ToggleWindow(std::make_unique<iwTextfile>("keyboardlayout.txt", _("Keyboard layout")));
×
716
            break;
×
717
    }
718
}
×
719

720
void dskOptions::Msg_MsgBoxResult(const unsigned msgbox_id, const MsgboxResult /*mbr*/)
×
721
{
722
    switch(msgbox_id)
×
723
    {
724
        default: break;
×
725
        // "You need to restart your game ..."
726
        // "The selected video driver does not support GUI scaling!"
727
        case 1: WINDOWMANAGER.Switch(std::make_unique<dskMainMenu>()); break;
×
728
    }
729
}
×
730

731
static bool cmpVideoModes(const VideoMode& left, const VideoMode& right)
×
732
{
733
    if(left.width == right.width)
×
734
        return left.height > right.height;
×
735
    else
736
        return left.width > right.width;
×
737
}
738

739
void dskOptions::loadVideoModes()
×
740
{
741
    // Get available modes
742
    VIDEODRIVER.ListVideoModes(video_modes);
×
743
    // Remove everything below 800x600
744
    helpers::erase_if(video_modes, [](const auto& it) { return it.width < 800 && it.height < 600; });
×
745
    // Sort by aspect ratio
746
    std::sort(video_modes.begin(), video_modes.end(), cmpVideoModes);
×
747
}
×
748

749
void dskOptions::Msg_ScreenResize(const ScreenResizeEvent& sr)
×
750
{
751
    Desktop::Msg_ScreenResize(sr);
×
752
    updateGuiScale();
×
753
}
×
754

755
bool dskOptions::Msg_WheelUp(const MouseCoords& mc)
×
756
{
757
    if(VIDEODRIVER.GetModKeyState().ctrl)
×
758
    {
759
        scrollGuiScale(true);
×
760
        return true;
×
761
    } else
762
        return Desktop::Msg_WheelUp(mc);
×
763
}
764

765
bool dskOptions::Msg_WheelDown(const MouseCoords& mc)
×
766
{
767
    if(VIDEODRIVER.GetModKeyState().ctrl)
×
768
    {
769
        scrollGuiScale(false);
×
770
        return true;
×
771
    } else
772
        return Desktop::Msg_WheelDown(mc);
×
773
}
774

775
void dskOptions::updateGuiScale()
×
776
{
777
    // generate GUI scale percentages in 10% increments
778
    constexpr auto stepSize = 10u;
×
779
    const auto roundGuiScale = [=](unsigned percent) {
×
780
        return helpers::iround<unsigned>(static_cast<float>(percent) / stepSize) * stepSize;
×
781
    };
782

783
    const auto range = VIDEODRIVER.getGuiScaleRange();
×
784
    const auto recommendedPercentRounded = roundGuiScale(range.recommendedPercent);
×
785
    auto* combo = GetCtrl<ctrlGroup>(ID_grpGraphics)->GetCtrl<ctrlComboBox>(ID_cbGuiScale);
×
786

787
    guiScales_.clear();
×
788
    combo->DeleteAllItems();
×
789

790
    guiScales_.push_back(0);
×
791
    combo->AddString(helpers::format(_("Auto (%u%%)"), range.recommendedPercent));
×
792
    if(SETTINGS.video.guiScale == 0)
×
793
        combo->SetSelection(0);
×
794

795
    for(unsigned percent = roundGuiScale(range.minPercent); percent <= range.maxPercent; percent += stepSize)
×
796
    {
797
        if(percent == recommendedPercentRounded)
×
798
            recommendedGuiScaleIndex_ = guiScales_.size();
×
799
        guiScales_.push_back(percent);
×
800

801
        combo->AddString(helpers::toString(percent) + "%");
×
802
        if(percent == SETTINGS.video.guiScale)
×
803
            combo->SetSelection(combo->GetNumItems() - 1);
×
804
    }
805

806
    // if GUI scale exceeds maximum, lower it to keep UI elements on screen
807
    if(SETTINGS.video.guiScale > guiScales_.back())
×
808
    {
809
        combo->SetSelection(combo->GetNumItems() - 1);
×
810
        SETTINGS.video.guiScale = guiScales_.back();
×
811
        VIDEODRIVER.setGuiScalePercent(SETTINGS.video.guiScale);
×
812
    }
813
}
×
814

815
void dskOptions::scrollGuiScale(bool up)
×
816
{
817
    auto* combo = GetCtrl<ctrlGroup>(ID_grpGraphics)->GetCtrl<ctrlComboBox>(ID_cbGuiScale);
×
818
    const auto& selection = combo->GetSelection();
×
819
    unsigned newSelection = 0;
×
820
    if(!selection || *selection == 0) // No selection or "Auto" item selected
×
821
        newSelection = recommendedGuiScaleIndex_;
×
822
    else
823
        newSelection = std::clamp<unsigned>(*selection + (up ? 1 : -1), 1, combo->GetNumItems() - 1);
×
824

825
    if(newSelection != selection)
×
826
    {
827
        combo->SetSelection(newSelection);
×
828
        SETTINGS.video.guiScale = guiScales_[newSelection];
×
829
        VIDEODRIVER.setGuiScalePercent(SETTINGS.video.guiScale);
×
830
    }
831
}
×
832

833
void dskOptions::updatePortraitControls()
×
834
{
835
    const auto& newPortrait = Portraits[SETTINGS.lobby.portraitIndex];
×
836
    auto* groupCommon = GetCtrl<ctrlGroup>(ID_grpCommon);
×
837

838
    auto* portraitButton = groupCommon->GetCtrl<ctrlImageButton>(ID_btCommonPortrait);
×
839
    auto* newPortraitTexture = LOADER.GetTextureN(newPortrait.resourceId, newPortrait.resourceIndex);
×
840
    portraitButton->SetImage(newPortraitTexture);
×
841

842
    auto* portraitCombo = groupCommon->GetCtrl<ctrlComboBox>(ID_cbCommonPortrait);
×
843
    portraitCombo->SetSelection(SETTINGS.lobby.portraitIndex);
×
844
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc