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

mcallegari / qlcplus / 19144422256

06 Nov 2025 05:33PM UTC coverage: 34.256% (-0.1%) from 34.358%
19144422256

push

github

mcallegari
Back to 5.1.0 debug

17718 of 51723 relevant lines covered (34.26%)

19528.23 hits per line

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

0.0
/ui/src/fixturemanager.cpp
1
/*
2
  Q Light Controller
3
  fixturemanager.cpp
4

5
  Copyright (c) Heikki Junnila
6

7
  Licensed under the Apache License, Version 2.0 (the "License");
8
  you may not use this file except in compliance with the License.
9
  You may obtain a copy of the License at
10

11
      http://www.apache.org/licenses/LICENSE-2.0.txt
12

13
  Unless required by applicable law or agreed to in writing, software
14
  distributed under the License is distributed on an "AS IS" BASIS,
15
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
  See the License for the specific language governing permissions and
17
  limitations under the License.
18
*/
19

20
#include <QXmlStreamReader>
21
#include <QXmlStreamWriter>
22
#include <QTreeWidgetItem>
23
#include <QTextBrowser>
24
#include <QVBoxLayout>
25
#include <QTreeWidget>
26
#include <QScrollArea>
27
#include <QMessageBox>
28
#include <QToolButton>
29
#include <QFileDialog>
30
#include <QTabWidget>
31
#include <QSplitter>
32
#include <QToolBar>
33
#include <QAction>
34
#include <QString>
35
#include <QDebug>
36
#include <QIcon>
37
#include <QMenu>
38
#include <QtGui>
39

40
#include "qlcfixturemode.h"
41
#include "qlcfixturedef.h"
42
#include "qlcchannel.h"
43
#include "qlcfile.h"
44

45
#include "createfixturegroup.h"
46
#include "fixturegroupeditor.h"
47
#include "fixturetreewidget.h"
48
#include "channelsselection.h"
49
#include "addchannelsgroup.h"
50
#include "fixturemanager.h"
51
#include "fixtureremap.h"
52
#include "addrgbpanel.h"
53
#include "addfixture.h"
54
#include "rdmmanager.h"
55
#include "universe.h"
56
#include "fixture.h"
57
#include "apputil.h"
58
#include "doc.h"
59

60
#define SETTINGS_SPLITTER "fixturemanager/splitterstate"
61

62
// List view column numbers
63
#define KColumnName     0
64
#define KColumnChannels 1
65
#define KColumnAddress  2
66

67
FixtureManager* FixtureManager::s_instance = NULL;
68

69
/*****************************************************************************
70
 * Initialization
71
 *****************************************************************************/
72

73
FixtureManager::FixtureManager(QWidget* parent, Doc* doc)
×
74
    : QWidget(parent)
75
    , m_doc(doc)
×
76
    , m_splitter(NULL)
×
77
    , m_fixtures_tree(NULL)
×
78
    , m_channel_groups_tree(NULL)
×
79
    , m_rdmManager(NULL)
×
80
    , m_info(NULL)
×
81
    , m_groupEditor(NULL)
×
82
    , m_currentTabIndex(0)
×
83
    , m_addAction(NULL)
×
84
    , m_addRGBAction(NULL)
×
85
    , m_removeAction(NULL)
×
86
    , m_propertiesAction(NULL)
×
87
    , m_fadeConfigAction(NULL)
×
88
    , m_remapAction(NULL)
×
89
    , m_groupAction(NULL)
×
90
    , m_unGroupAction(NULL)
×
91
    , m_newGroupAction(NULL)
×
92
    , m_moveUpAction(NULL)
×
93
    , m_moveDownAction(NULL)
×
94
    , m_importAction(NULL)
×
95
    , m_exportAction(NULL)
×
96
    , m_groupMenu(NULL)
×
97
{
98
    Q_ASSERT(s_instance == NULL);
×
99
    s_instance = this;
×
100

101
    Q_ASSERT(doc != NULL);
×
102

103
    new QVBoxLayout(this);
×
104
    layout()->setContentsMargins(0, 0, 0, 0);
×
105
    layout()->setSpacing(0);
×
106

107
    initActions();
×
108
    initToolBar();
×
109
    initDataView();
×
110
    updateView();
×
111
    updateChannelsGroupView();
×
112

113
    QTreeWidgetItem* grpItem = m_fixtures_tree->topLevelItem(0);
×
114
    if (grpItem != NULL)
×
115
        grpItem->setExpanded(true);
×
116

117
    /* Connect fixture list change signals from the new document object */
118
    connect(m_doc, SIGNAL(fixtureRemoved(quint32)),
×
119
            this, SLOT(slotFixtureRemoved(quint32)));
120

121
    connect(m_doc, SIGNAL(channelsGroupRemoved(quint32)),
×
122
            this, SLOT(slotChannelsGroupRemoved(quint32)));
123

124
    connect(m_doc, SIGNAL(modeChanged(Doc::Mode)),
×
125
            this, SLOT(slotModeChanged(Doc::Mode)));
126

127
    connect(m_doc, SIGNAL(fixtureGroupRemoved(quint32)),
×
128
            this, SLOT(slotFixtureGroupRemoved(quint32)));
129

130
    connect(m_doc, SIGNAL(fixtureGroupChanged(quint32)),
×
131
            this, SLOT(slotFixtureGroupChanged(quint32)));
132

133
    connect(m_doc, SIGNAL(loaded()),
×
134
            this, SLOT(slotDocLoaded()));
135

136
    slotModeChanged(m_doc->mode());
×
137

138
    QSettings settings;
×
139
    QVariant var = settings.value(SETTINGS_SPLITTER);
×
140
    if (var.isValid() == true)
×
141
        m_splitter->restoreState(var.toByteArray());
×
142
    else
143
        m_splitter->setSizes(QList <int> () << int(this->width() / 2) << int(this->width() / 2));
×
144
}
×
145

146
FixtureManager::~FixtureManager()
×
147
{
148
    QSettings settings;
×
149
    settings.setValue(SETTINGS_SPLITTER, m_splitter->saveState());
×
150
    FixtureManager::s_instance = NULL;
×
151

152
    s_instance = NULL;
×
153
}
×
154

155
FixtureManager* FixtureManager::instance()
×
156
{
157
    return s_instance;
×
158
}
159

160
/*****************************************************************************
161
 * Doc signal handlers
162
 *****************************************************************************/
163

164
void FixtureManager::slotFixtureRemoved(quint32 id)
×
165
{
166
    QList<QTreeWidgetItem*> groupsToDelete;
×
167

168
    for (int i = 0; i < m_fixtures_tree->topLevelItemCount(); i++)
×
169
    {
170
        QTreeWidgetItem* grpItem = m_fixtures_tree->topLevelItem(i);
×
171
        Q_ASSERT(grpItem != NULL);
×
172
        for (int j = 0; j < grpItem->childCount(); j++)
×
173
        {
174
            QTreeWidgetItem* fxiItem = grpItem->child(j);
×
175
            Q_ASSERT(fxiItem != NULL);
×
176
            QVariant var = fxiItem->data(KColumnName, PROP_ID);
×
177
            if (var.isValid() == true && var.toUInt() == id)
×
178
            {
179
                delete fxiItem;
×
180
                break;
×
181
            }
182
        }
×
183
        if (grpItem->childCount() == 0)
×
184
            groupsToDelete << grpItem;
×
185
    }
186
    foreach (QTreeWidgetItem* groupToDelete, groupsToDelete)
×
187
    {
188
        QVariant var = groupToDelete->data(KColumnName, PROP_GROUP);
×
189
        // If the group is a fixture group, delete it from doc.
190
        // If not, it is a universe, just "hide" it from the ui.
191
        if (var.isValid() == true)
×
192
            m_doc->deleteFixtureGroup(groupToDelete->data(KColumnName, PROP_GROUP).toUInt());
×
193
        else
194
            delete groupToDelete;
×
195
    }
×
196
}
×
197

198
void FixtureManager::slotChannelsGroupRemoved(quint32 id)
×
199
{
200
    qDebug() << "Channel group removed: " << id;
×
201
    for (int i = 0; i < m_channel_groups_tree->topLevelItemCount(); i++)
×
202
    {
203
        QTreeWidgetItem* grpItem = m_channel_groups_tree->topLevelItem(i);
×
204
        Q_ASSERT(grpItem != NULL);
×
205
        QVariant var = grpItem->data(KColumnName, PROP_ID);
×
206
        if (var.isValid() == true && var.toUInt() == id)
×
207
            delete grpItem;
×
208
    }
×
209
}
×
210

211
void FixtureManager::slotModeChanged(Doc::Mode mode)
×
212
{
213
    if (mode == Doc::Design)
×
214
    {
215
        int selected = m_fixtures_tree->selectedItems().size();
×
216

217
        QTreeWidgetItem* item = m_fixtures_tree->currentItem();
×
218
        if (item == NULL)
×
219
        {
220
            m_addAction->setEnabled(true);
×
221
            m_addRGBAction->setEnabled(true);
×
222
            m_removeAction->setEnabled(false);
×
223
            m_propertiesAction->setEnabled(false);
×
224
            m_groupAction->setEnabled(false);
×
225
            m_unGroupAction->setEnabled(false);
×
226
            m_importAction->setEnabled(true);
×
227
        }
228
        else if (item->data(KColumnName, PROP_ID).isValid() == true)
×
229
        {
230
            // Fixture selected
231
            m_addAction->setEnabled(true);
×
232
            m_addRGBAction->setEnabled(true);
×
233
            m_removeAction->setEnabled(true);
×
234
            if (selected == 1)
×
235
                m_propertiesAction->setEnabled(true);
×
236
            else
237
                m_propertiesAction->setEnabled(false);
×
238
            m_groupAction->setEnabled(true);
×
239

240
            // Don't allow ungrouping from the "All fixtures" group
241
            if (item->parent()->data(KColumnName, PROP_GROUP).isValid() == true)
×
242
                m_unGroupAction->setEnabled(true);
×
243
            else
244
                m_unGroupAction->setEnabled(false);
×
245
        }
246
        else if (item->data(KColumnName, PROP_GROUP).isValid() == true)
×
247
        {
248
            // Fixture group selected
249
            m_addAction->setEnabled(true);
×
250
            m_addRGBAction->setEnabled(true);
×
251
            m_removeAction->setEnabled(true);
×
252
            m_propertiesAction->setEnabled(false);
×
253
            m_groupAction->setEnabled(false);
×
254
            m_unGroupAction->setEnabled(false);
×
255
        }
256
        else
257
        {
258
            // All fixtures selected
259
            m_addAction->setEnabled(true);
×
260
            m_addRGBAction->setEnabled(true);
×
261
            m_removeAction->setEnabled(false);
×
262
            m_propertiesAction->setEnabled(false);
×
263
            m_groupAction->setEnabled(false);
×
264
            m_unGroupAction->setEnabled(false);
×
265
        }
266
        if (m_doc->fixtures().count() > 0)
×
267
            m_fadeConfigAction->setEnabled(true);
×
268
        else
269
            m_fadeConfigAction->setEnabled(false);
×
270
    }
271
    else
272
    {
273
        m_addAction->setEnabled(false);
×
274
        m_addRGBAction->setEnabled(false);
×
275
        m_removeAction->setEnabled(false);
×
276
        m_propertiesAction->setEnabled(false);
×
277
        m_fadeConfigAction->setEnabled(false);
×
278
        m_groupAction->setEnabled(false);
×
279
        m_unGroupAction->setEnabled(false);
×
280
    }
281
}
×
282

283
void FixtureManager::slotFixtureGroupRemoved(quint32 id)
×
284
{
285
    for (int i = 0; i < m_fixtures_tree->topLevelItemCount(); i++)
×
286
    {
287
        QTreeWidgetItem* item = m_fixtures_tree->topLevelItem(i);
×
288
        Q_ASSERT(item != NULL);
×
289
        QVariant var = item->data(KColumnName, PROP_GROUP);
×
290
        if (var.isValid() && var.toUInt() == id)
×
291
        {
292
            delete item;
×
293
            break;
×
294
        }
295
    }
×
296

297
    updateGroupMenu();
×
298
}
×
299

300
void FixtureManager::slotFixtureGroupChanged(quint32 id)
×
301
{
302
    QTreeWidgetItem* item = m_fixtures_tree->groupItem(id);
×
303
    if (item == NULL)
×
304
        return;
×
305

306
    FixtureGroup* grp = m_doc->fixtureGroup(id);
×
307
    Q_ASSERT(grp != NULL);
×
308
    m_fixtures_tree->updateGroupItem(item, grp);
×
309
    updateGroupMenu();
×
310
}
311

312
void FixtureManager::slotDocLoaded()
×
313
{
314
    slotTabChanged(m_currentTabIndex);
×
315
}
×
316

317
/*****************************************************************************
318
 * Data view
319
 *****************************************************************************/
320

321
void FixtureManager::initDataView()
×
322
{
323
    // Create a splitter to divide list view and text view
324
    m_splitter = new QSplitter(Qt::Horizontal, this);
×
325
    layout()->addWidget(m_splitter);
×
326
    m_splitter->setSizePolicy(QSizePolicy::Expanding,
×
327
                              QSizePolicy::Expanding);
328

329
    QTabWidget *tabs = new QTabWidget(this);
×
330
    m_splitter->addWidget(tabs);
×
331

332
    /* Create a tree widget to the left part of the splitter */
333
    quint32 treeFlags = FixtureTreeWidget::UniverseNumber |
×
334
                        FixtureTreeWidget::AddressRange |
335
                        FixtureTreeWidget::ShowGroups;
336

337
    m_fixtures_tree = new FixtureTreeWidget(m_doc, treeFlags, this);
×
338
    m_fixtures_tree->setIconSize(QSize(32, 32));
×
339
    m_fixtures_tree->setContextMenuPolicy(Qt::CustomContextMenu);
×
340
    m_fixtures_tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
×
341
    m_fixtures_tree->sortByColumn(KColumnAddress, Qt::AscendingOrder);
×
342

343
    connect(m_fixtures_tree, SIGNAL(itemSelectionChanged()),
×
344
            this, SLOT(slotSelectionChanged()));
345

346
    connect(m_fixtures_tree, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)),
×
347
            this, SLOT(slotDoubleClicked(QTreeWidgetItem*)));
348

349
    connect(m_fixtures_tree, SIGNAL(customContextMenuRequested(const QPoint&)),
×
350
            this, SLOT(slotContextMenuRequested(const QPoint&)));
351

352
    connect(m_fixtures_tree, SIGNAL(expanded(QModelIndex)),
×
353
            this, SLOT(slotFixtureItemExpanded()));
354

355
    connect(m_fixtures_tree, SIGNAL(collapsed(QModelIndex)),
×
356
            this, SLOT(slotFixtureItemExpanded()));
357

358
    tabs->addTab(m_fixtures_tree, tr("Fixture Groups"));
×
359

360
    m_channel_groups_tree = new QTreeWidget(this);
×
361
    QStringList chan_labels;
×
362
    chan_labels << tr("Name") << tr("Channels");
×
363
    m_channel_groups_tree->setHeaderLabels(chan_labels);
×
364
    m_channel_groups_tree->setRootIsDecorated(false);
×
365
    m_channel_groups_tree->setAllColumnsShowFocus(true);
×
366
    m_channel_groups_tree->setIconSize(QSize(32, 32));
×
367
    m_channel_groups_tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
×
368

369
    connect(m_channel_groups_tree, SIGNAL(itemSelectionChanged()),
×
370
            this, SLOT(slotChannelsGroupSelectionChanged()));
371
    connect(m_channel_groups_tree, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)),
×
372
            this, SLOT(slotChannelsGroupDoubleClicked(QTreeWidgetItem*)));
373

374
    tabs->addTab(m_channel_groups_tree, tr("Channel Groups"));
×
375

376
    m_rdmManager = new RDMManager(this, m_doc);
×
377
    tabs->addTab(m_rdmManager, "RDM");
×
378
    connect(m_rdmManager, SIGNAL(fixtureInfoReady(QString&)),
×
379
            this, SLOT(slotDisplayFixtureInfo(QString&)));
380

381
    connect(tabs, SIGNAL(currentChanged(int)), this, SLOT(slotTabChanged(int)));
×
382

383
    /* Create the text view */
384
    createInfo();
×
385

386
    slotSelectionChanged();
×
387
}
×
388

389
void FixtureManager::updateView()
×
390
{
391
    // Record which top level items are open
392
    QList <QVariant> openGroups;
×
393
    for (int i = 0; i < m_fixtures_tree->topLevelItemCount(); i++)
×
394
    {
395
        QTreeWidgetItem* item = m_fixtures_tree->topLevelItem(i);
×
396
        if (item->isExpanded() == true)
×
397
            openGroups << item->data(KColumnName, PROP_GROUP);
×
398
    }
399

400
    if (m_doc->fixtures().count() > 0)
×
401
    {
402
        m_exportAction->setEnabled(true);
×
403
        m_remapAction->setEnabled(true);
×
404
        m_fadeConfigAction->setEnabled(true);
×
405
    }
406
    else
407
    {
408
        m_exportAction->setEnabled(false);
×
409
        m_fadeConfigAction->setEnabled(false);
×
410
        m_remapAction->setEnabled(false);
×
411
    }
412
    m_addRGBAction->setEnabled(true);
×
413
    m_importAction->setEnabled(true);
×
414
    m_moveUpAction->setEnabled(false);
×
415
    m_moveDownAction->setEnabled(false);
×
416

417
    m_fixtures_tree->updateTree();
×
418

419
    // Reopen groups that were open before update
420
    for (int i = 0; i < m_fixtures_tree->topLevelItemCount(); i++)
×
421
    {
422
        QTreeWidgetItem* item = m_fixtures_tree->topLevelItem(i);
×
423
        QVariant var = item->data(KColumnName, PROP_GROUP);
×
424
        if (openGroups.contains(var) == true)
×
425
        {
426
            item->setExpanded(true);
×
427
            openGroups.removeAll(var);
×
428
        }
429
    }
×
430

431
    updateGroupMenu();
×
432
    slotModeChanged(m_doc->mode());
×
433

434
    m_fixtures_tree->header()->resizeSections(QHeaderView::ResizeToContents);
×
435
}
×
436

437
void FixtureManager::updateChannelsGroupView()
×
438
{
439
    quint32 selGroupID = ChannelsGroup::invalidId();
×
440

441
    if (m_channel_groups_tree->selectedItems().size() > 0)
×
442
    {
443
        QTreeWidgetItem *item = m_channel_groups_tree->selectedItems().first();
×
444
        selGroupID = item->data(KColumnName, PROP_ID).toUInt();
×
445
    }
446

447
    if (m_channel_groups_tree->topLevelItemCount() > 0)
×
448
        for (int i = m_channel_groups_tree->topLevelItemCount() - 1; i >= 0; i--)
×
449
            m_channel_groups_tree->takeTopLevelItem(i);
×
450

451
    foreach (ChannelsGroup *grp, m_doc->channelsGroups())
×
452
    {
453
        QTreeWidgetItem *grpItem = new QTreeWidgetItem(m_channel_groups_tree);
×
454
        grpItem->setText(KColumnName, grp->name());
×
455
        grpItem->setData(KColumnName, PROP_ID, grp->id());
×
456
        grpItem->setText(KColumnChannels, QString("%1").arg(grp->getChannels().count()));
×
457
        if (grp->getChannels().count() > 0)
×
458
        {
459
            SceneValue scv = grp->getChannels().at(0);
×
460
            Fixture *fxi = m_doc->fixture(scv.fxi);
×
461
            if (fxi == NULL)
×
462
                continue;
×
463

464
            const QLCChannel *ch = fxi->channel(scv.channel);
×
465
            if (ch != NULL)
×
466
                grpItem->setIcon(KColumnName, ch->getIcon());
×
467
        }
×
468
        if (selGroupID == grp->id())
×
469
            grpItem->setSelected(true);
×
470
    }
×
471
    m_addRGBAction->setEnabled(false);
×
472
    m_propertiesAction->setEnabled(false);
×
473
    m_groupAction->setEnabled(false);
×
474
    m_unGroupAction->setEnabled(false);
×
475
    m_fadeConfigAction->setEnabled(false);
×
476
    m_exportAction->setEnabled(false);
×
477
    m_importAction->setEnabled(false);
×
478
    m_remapAction->setEnabled(false);
×
479

480
    m_channel_groups_tree->header()->resizeSections(QHeaderView::ResizeToContents);
×
481
}
×
482

483
void FixtureManager::updateRDMView()
×
484
{
485
    m_addRGBAction->setEnabled(false);
×
486
    m_propertiesAction->setEnabled(false);
×
487
    m_groupAction->setEnabled(false);
×
488
    m_unGroupAction->setEnabled(false);
×
489
    m_fadeConfigAction->setEnabled(false);
×
490
    m_exportAction->setEnabled(false);
×
491
    m_importAction->setEnabled(false);
×
492
    m_remapAction->setEnabled(false);
×
493
}
×
494

495
void FixtureManager::fixtureSelected(quint32 id)
×
496
{
497
    Fixture* fxi = m_doc->fixture(id);
×
498
    if (fxi == NULL)
×
499
        return;
×
500

501
    if (m_info == NULL)
×
502
        createInfo();
×
503

504
    m_info->setText(QString("%1<BODY>%2</BODY></HTML>")
×
505
                    .arg(fixtureInfoStyleSheetHeader())
×
506
                    .arg(fxi->status()));
×
507

508
    // Enable/disable actions
509
    slotModeChanged(m_doc->mode());
×
510
}
511

512
void FixtureManager::fixtureGroupSelected(FixtureGroup* grp)
×
513
{
514
    QByteArray state = m_splitter->saveState();
×
515

516
    if (m_info != NULL)
×
517
    {
518
        delete m_info;
×
519
        m_info = NULL;
×
520
    }
521

522
    if (m_groupEditor != NULL)
×
523
    {
524
        delete m_groupEditor;
×
525
        m_groupEditor = NULL;
×
526
    }
527

528
    m_groupEditor = new FixtureGroupEditor(grp, m_doc, this);
×
529
    m_splitter->addWidget(m_groupEditor);
×
530

531
    m_splitter->restoreState(state);
×
532
}
×
533

534
void FixtureManager::createInfo()
×
535
{
536
    QByteArray state = m_splitter->saveState();
×
537

538
    if (m_info != NULL)
×
539
    {
540
        delete m_info;
×
541
        m_info = NULL;
×
542
    }
543

544
    if (m_groupEditor != NULL)
×
545
    {
546
        delete m_groupEditor;
×
547
        m_groupEditor = NULL;
×
548
    }
549

550
    m_info = new QTextBrowser(this);
×
551
    m_splitter->addWidget(m_info);
×
552

553
    m_splitter->restoreState(state);
×
554
}
×
555

556
void FixtureManager::slotSelectionChanged()
×
557
{
558
    int selectedCount = m_fixtures_tree->selectedItems().size();
×
559
    if (selectedCount == 1)
×
560
    {
561
        QTreeWidgetItem* item = m_fixtures_tree->selectedItems().first();
×
562
        Q_ASSERT(item != NULL);
×
563

564
        // Set the text view's contents
565
        QVariant fxivar = item->data(KColumnName, PROP_ID);
×
566
        QVariant grpvar = item->data(KColumnName, PROP_GROUP);
×
567
        if (fxivar.isValid() == true)
×
568
        {
569
            // Selected a fixture
570
            fixtureSelected(fxivar.toUInt());
×
571
        }
572
        else if (grpvar.isValid() == true)
×
573
        {
574
            FixtureGroup* grp = m_doc->fixtureGroup(grpvar.toUInt());
×
575
            Q_ASSERT(grp != NULL);
×
576
            fixtureGroupSelected(grp);
×
577
        }
578
        else
579
        {
580
            QString info = "<HTML><BODY>";
×
581
            QString uniName;
×
582
            double totalWeight = 0;
×
583
            int totalPower = 0;
×
584
            QVariant uniID = item->data(KColumnName, PROP_UNIVERSE);
×
585
            if (uniID.isValid() == true)
×
586
                uniName = m_doc->inputOutputMap()->getUniverseNameByID(uniID.toUInt());
×
587

588
            foreach (Fixture *fixture, m_doc->fixtures())
×
589
            {
590
                if (fixture == NULL || fixture->universe() != uniID.toUInt() || fixture->fixtureMode() == NULL)
×
591
                    continue;
×
592

593
                QLCFixtureMode *mode = fixture->fixtureMode();
×
594
                totalWeight += mode->physical().weight();
×
595
                totalPower += mode->physical().powerConsumption();
×
596
            }
×
597

598
            if (m_info == NULL)
×
599
                createInfo();
×
600

601
            info += QString("<H1>%1</H1><P>%2 <B>%3</B></P>")
×
602
                    .arg(uniName).arg(tr("This group contains all fixtures of"))
×
603
                    .arg(uniName);
×
604

605
            info += QString("<BR><P><B>%1</B>: %2Kg<BR><B>%3</B>: %4W</P>")
×
606
                    .arg(tr("Total estimated weight")).arg(QString::number(totalWeight))
×
607
                    .arg(tr("Maximum estimated power consumption")).arg(totalPower);
×
608

609
            info += "</BODY></HTML>";
×
610

611
            m_info->setText(info);
×
612
        }
×
613
    }
×
614
    else
615
    {
616
        // More than one or less than one selected
617
        QString info = "<HTML><BODY>";
×
618
        if (selectedCount > 1)
×
619
        {
620
            // Enable removal of multiple items in design mode
621
            if (m_doc->mode() == Doc::Design)
×
622
            {
623
                double totalWeight = 0;
×
624
                int totalPower = 0;
×
625

626
                info += tr("<H1>Multiple fixtures selected</H1>" \
×
627
                          "<P>Click <IMG SRC=\"" ":/edit_remove.png\">" \
628
                          " to remove the selected fixtures.</P>");
×
629

630
                foreach (QTreeWidgetItem *item, m_fixtures_tree->selectedItems())
×
631
                {
632
                    QVariant fxID = item->data(KColumnName, PROP_ID);
×
633
                    if (fxID.isValid() == false)
×
634
                        continue;
×
635

636
                    Fixture *fixture = m_doc->fixture(fxID.toUInt());
×
637

638
                    if (fixture == NULL || fixture->fixtureMode() == NULL)
×
639
                        continue;
×
640

641
                    QLCFixtureMode *mode = fixture->fixtureMode();
×
642
                    totalWeight += mode->physical().weight();
×
643
                    totalPower += mode->physical().powerConsumption();
×
644
                }
×
645

646
                info += QString("<BR><P><B>%1</B>: %2Kg<BR><B>%3</B>: %4W</P>")
×
647
                        .arg(tr("Total estimated weight")).arg(QString::number(totalWeight))
×
648
                        .arg(tr("Maximum estimated power consumption")).arg(totalPower);
×
649
            }
650
            else
651
            {
652
                info += tr("<H1>Multiple fixtures selected</H1>" \
×
653
                          "<P>Fixture list modification is not permitted" \
654
                          " in operate mode.</P>");
×
655
            }
656
        }
657
        else
658
        {
659
            if (m_fixtures_tree->topLevelItemCount() <= 0)
×
660
            {
661
                info += tr("<H1>No fixtures</H1>" \
×
662
                          "<P>Click <IMG SRC=\"" ":/edit_add.png\">" \
663
                          " to add fixtures.</P>");
×
664
            }
665
            else
666
            {
667
                info += tr("<H1>Nothing selected</H1>" \
×
668
                          "<P>Select a fixture from the list or " \
669
                          "click <IMG SRC=\"" ":/edit_add.png\">" \
670
                          " to add fixtures.</P>");
×
671
            }
672
        }
673
        info += "</BODY></HTML>";
×
674

675
        if (m_info == NULL)
×
676
            createInfo();
×
677
        m_info->setText(info);
×
678
    }
×
679

680
    // Enable/disable actions
681
    slotModeChanged(m_doc->mode());
×
682
}
×
683

684
void FixtureManager::slotChannelsGroupSelectionChanged()
×
685
{
686
    if (m_info == NULL)
×
687
        createInfo();
×
688

689
    int selectedCount = m_channel_groups_tree->selectedItems().size();
×
690

691
    if (selectedCount == 1)
×
692
    {
693
        QTreeWidgetItem* item = m_channel_groups_tree->selectedItems().first();
×
694
        Q_ASSERT(item != NULL);
×
695

696
        // Set the text view's contents
697
        QVariant grpvar = item->data(KColumnName, PROP_ID);
×
698
        if (grpvar.isValid() == true)
×
699
        {
700
            ChannelsGroup *chGroup = m_doc->channelsGroup(grpvar.toUInt());
×
701
            if (chGroup != NULL)
×
702
                m_info->setText(QString("%1<BODY>%2</BODY></HTML>")
×
703
                                .arg(channelsGroupInfoStyleSheetHeader())
×
704
                                .arg(chGroup->status(m_doc)));
×
705
        }
706
        m_removeAction->setEnabled(true);
×
707
        m_propertiesAction->setEnabled(true);
×
708
        int selIdx = m_channel_groups_tree->currentIndex().row();
×
709
        if (selIdx == 0)
×
710
            m_moveUpAction->setEnabled(false);
×
711
        else
712
            m_moveUpAction->setEnabled(true);
×
713
        if (selIdx == m_channel_groups_tree->topLevelItemCount() - 1)
×
714
            m_moveDownAction->setEnabled(false);
×
715
        else
716
            m_moveDownAction->setEnabled(true);
×
717
    }
×
718
    else if (selectedCount > 1)
×
719
    {
720
        m_info->setText(tr("<HTML><BODY><H1>Multiple groups selected</H1>" \
×
721
                  "<P>Click <IMG SRC=\"" ":/edit_remove.png\">" \
722
                  " to remove the selected groups.</P></BODY></HTML>"));
723
        m_removeAction->setEnabled(true);
×
724
        m_propertiesAction->setEnabled(false);
×
725
    }
726
    else
727
    {
728
        m_info->setText(tr("<HTML><BODY><H1>Nothing selected</H1>" \
×
729
                  "<P>Select a channel group from the list or " \
730
                  "click <IMG SRC=\"" ":/edit_add.png\">" \
731
                  " to add a new channels group.</P></BODY></HTML>"));
732
        m_removeAction->setEnabled(false);
×
733
        m_propertiesAction->setEnabled(false);
×
734
    }
735
}
×
736

737
void FixtureManager::slotDoubleClicked(QTreeWidgetItem* item)
×
738
{
739
    if (item != NULL && m_doc->mode() != Doc::Operate)
×
740
        slotProperties();
×
741
}
×
742

743
void FixtureManager::slotChannelsGroupDoubleClicked(QTreeWidgetItem*)
×
744
{
745
    slotChannelsGroupSelectionChanged();
×
746
    editChannelGroupProperties();
×
747
}
×
748

749
void FixtureManager::slotTabChanged(int index)
×
750
{
751
    if (index == 1)
×
752
    {
753
        m_addAction->setToolTip(tr("Add group..."));
×
754
        updateChannelsGroupView();
×
755
        slotChannelsGroupSelectionChanged();
×
756
    }
757
    else if (index == 2)
×
758
    {
759
        m_addAction->setToolTip(tr("Add fixture..."));
×
760
        updateRDMView();
×
761
    }
762
    else
763
    {
764
        m_addAction->setToolTip(tr("Add fixture..."));
×
765
        updateView();
×
766
        slotSelectionChanged();
×
767
    }
768

769
    m_currentTabIndex = index;
×
770
}
×
771

772
void FixtureManager::slotFixtureItemExpanded()
×
773
{
774
    m_fixtures_tree->header()->resizeSections(QHeaderView::ResizeToContents);
×
775
}
×
776

777
void FixtureManager::slotDisplayFixtureInfo(QString &info)
×
778
{
779
    m_info->setText(info);
×
780
}
×
781

782
void FixtureManager::selectGroup(quint32 id)
×
783
{
784
    for (int i = 0; i < m_fixtures_tree->topLevelItemCount(); i++)
×
785
    {
786
        QTreeWidgetItem* item = m_fixtures_tree->topLevelItem(i);
×
787
        QVariant var = item->data(KColumnName, PROP_GROUP);
×
788
        if (var.isValid() == false)
×
789
            continue;
×
790

791
        if (var.toUInt() == id)
×
792
        {
793
            m_fixtures_tree->setCurrentItem(item);
×
794
            slotSelectionChanged();
×
795
            break;
×
796
        }
797
    }
×
798
}
×
799

800
QString FixtureManager::fixtureInfoStyleSheetHeader()
×
801
{
802
    QString info;
×
803

804
    QPalette pal;
×
805
    QColor hlBack(pal.color(QPalette::Highlight));
×
806
    QColor hlText(pal.color(QPalette::HighlightedText));
×
807

808
    info += "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">";
×
809
    info += "<HTML><HEAD></HEAD><STYLE>";
×
810
    info += QString(".hilite {" \
×
811
                    "        background-color: %1;" \
812
                    "        color: %2;" \
813
                    "        font-size: x-large;" \
814
                    "}").arg(hlBack.name()).arg(hlText.name());
×
815
    info += QString(".subhi {" \
×
816
                    "        background-color: %1;" \
817
                    "        color: %2;" \
818
                    "        font-weight: bold;" \
819
                    "}").arg(hlBack.name()).arg(hlText.name());
×
820
    info += QString(".emphasis {" \
×
821
                    "        font-weight: bold;" \
822
                    "}");
×
823
    info += QString(".tiny {"\
×
824
                    "   font-size: small;" \
825
                    "}");
×
826
    info += QString(".author {" \
×
827
                    "        font-weight: light;" \
828
                    "        font-style: italic;" \
829
                    "   text-align: right;" \
830
                    "   font-size: small;"  \
831
                    "}");
×
832
    info += "</STYLE>";
×
833
    return info;
×
834
}
×
835

836
QString FixtureManager::channelsGroupInfoStyleSheetHeader()
×
837
{
838
    QString info;
×
839

840
    QPalette pal;
×
841
    QColor hlBack(pal.color(QPalette::Highlight));
×
842
    QColor hlBackSmall(pal.color(QPalette::Shadow));
×
843
    QColor hlText(pal.color(QPalette::HighlightedText));
×
844

845
    info += "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">";
×
846
    info += "<HTML><HEAD></HEAD><STYLE>";
×
847
    info += QString(".hilite {" \
×
848
                    "        background-color: %1;" \
849
                    "        color: %2;" \
850
                    "        font-size: x-large;" \
851
                    "}").arg(hlBack.name()).arg(hlText.name());
×
852
    info += QString(".subhi {" \
×
853
                    "        background-color: %1;" \
854
                    "        color: %2;" \
855
                    "        font-weight: bold;" \
856
                    "}").arg(hlBackSmall.name()).arg(hlText.name());
×
857
    info += QString(".emphasis {" \
×
858
                    "        font-weight: bold;" \
859
                    "}");
×
860
    info += QString(".tiny {"\
×
861
                    "   font-size: small;" \
862
                    "}");
×
863
    info += "</STYLE>";
×
864
    return info;
×
865
}
×
866

867
/*****************************************************************************
868
 * Menu, toolbar and actions
869
 *****************************************************************************/
870

871
void FixtureManager::initActions()
×
872
{
873
    // Fixture actions
874
    m_addAction = new QAction(QIcon(":/edit_add.png"),
×
875
                              tr("Add fixture..."), this);
×
876
    connect(m_addAction, SIGNAL(triggered(bool)),
×
877
            this, SLOT(slotAdd()));
878

879
    m_addRGBAction = new QAction(QIcon(":/rgbpanel.png"),
×
880
                              tr("Add RGB panel..."), this);
×
881
    connect(m_addRGBAction, SIGNAL(triggered(bool)),
×
882
            this, SLOT(slotAddRGBPanel()));
883

884
    m_removeAction = new QAction(QIcon(":/edit_remove.png"),
×
885
                                 tr("Delete items"), this);
×
886
    connect(m_removeAction, SIGNAL(triggered(bool)),
×
887
            this, SLOT(slotRemove()));
888

889
    m_propertiesAction = new QAction(QIcon(":/configure.png"),
×
890
                                     tr("Properties..."), this);
×
891
    connect(m_propertiesAction, SIGNAL(triggered(bool)),
×
892
            this, SLOT(slotProperties()));
893

894
    m_fadeConfigAction = new QAction(QIcon(":/fade.png"),
×
895
                                     tr("Channels Fade Configuration..."), this);
×
896
    connect(m_fadeConfigAction, SIGNAL(triggered(bool)),
×
897
            this, SLOT(slotFadeConfig()));
898

899
    // Group actions
900
    m_groupAction = new QAction(QIcon(":/group.png"),
×
901
                                tr("Add fixture to group..."), this);
×
902

903
    m_unGroupAction = new QAction(QIcon(":/ungroup.png"),
×
904
                                tr("Remove fixture from group"), this);
×
905
    connect(m_unGroupAction, SIGNAL(triggered(bool)),
×
906
            this, SLOT(slotUnGroup()));
907

908
    m_newGroupAction = new QAction(tr("New Group..."), this);
×
909

910
    m_moveUpAction = new QAction(QIcon(":/up.png"),
×
911
                                 tr("Move channel group up..."), this);
×
912
    m_moveUpAction->setEnabled(false);
×
913
    connect(m_moveUpAction, SIGNAL(triggered(bool)),
×
914
            this, SLOT(slotMoveGroupUp()));
915

916
    m_moveDownAction = new QAction(QIcon(":/down.png"),
×
917
                                 tr("Move channel group down..."), this);
×
918
    m_moveDownAction->setEnabled(false);
×
919
    connect(m_moveDownAction, SIGNAL(triggered(bool)),
×
920
            this, SLOT(slotMoveGroupDown()));
921

922
    m_importAction = new QAction(QIcon(":/fileimport.png"),
×
923
                                 tr("Import fixtures..."), this);
×
924
    connect(m_importAction, SIGNAL(triggered(bool)),
×
925
            this, SLOT(slotImport()));
926

927
    m_exportAction = new QAction(QIcon(":/fileexport.png"),
×
928
                                 tr("Export fixtures..."), this);
×
929

930
    connect(m_exportAction, SIGNAL(triggered(bool)),
×
931
            this, SLOT(slotExport()));
932

933
    m_remapAction = new QAction(QIcon(":/remap.png"),
×
934
                               tr("Remap fixtures..."), this);
×
935
    connect(m_remapAction, SIGNAL(triggered(bool)),
×
936
            this, SLOT(slotRemap()));
937
}
×
938

939
void FixtureManager::updateGroupMenu()
×
940
{
941
    if (m_groupMenu == NULL)
×
942
    {
943
        m_groupMenu = new QMenu(this);
×
944
        connect(m_groupMenu, SIGNAL(triggered(QAction*)),
×
945
                this, SLOT(slotGroupSelected(QAction*)));
946
    }
947

948
    foreach (QAction* a, m_groupMenu->actions())
×
949
        m_groupMenu->removeAction(a);
×
950

951
    // Put all known fixture groups to the menu
952
    foreach (FixtureGroup* grp, m_doc->fixtureGroups())
×
953
    {
954
        QAction* a = m_groupMenu->addAction(grp->name());
×
955
        a->setData((qulonglong) grp);
×
956
    }
×
957

958
    // Put a new group action to the group menu
959
    m_groupMenu->addAction(m_newGroupAction);
×
960

961
    // Put the group menu to the group action
962
    m_groupAction->setMenu(m_groupMenu);
×
963
}
×
964

965
void FixtureManager::initToolBar()
×
966
{
967
    QToolBar* toolbar = new QToolBar(tr("Fixture manager"), this);
×
968
    toolbar->setFloatable(false);
×
969
    toolbar->setMovable(false);
×
970
    layout()->setMenuBar(toolbar);
×
971
    toolbar->addAction(m_addAction);
×
972
    toolbar->addAction(m_addRGBAction);
×
973
    toolbar->addAction(m_removeAction);
×
974
    toolbar->addAction(m_propertiesAction);
×
975
    toolbar->addAction(m_fadeConfigAction);
×
976
    toolbar->addSeparator();
×
977
    toolbar->addAction(m_groupAction);
×
978
    toolbar->addAction(m_unGroupAction);
×
979
    toolbar->addSeparator();
×
980
    toolbar->addAction(m_moveUpAction);
×
981
    toolbar->addAction(m_moveDownAction);
×
982
    toolbar->addSeparator();
×
983
    toolbar->addAction(m_importAction);
×
984
    toolbar->addAction(m_exportAction);
×
985
    toolbar->addAction(m_remapAction);
×
986

987
    QToolButton* btn = qobject_cast<QToolButton*> (toolbar->widgetForAction(m_groupAction));
×
988
    Q_ASSERT(btn != NULL);
×
989
    btn->setPopupMode(QToolButton::InstantPopup);
×
990
}
×
991

992
void FixtureManager::addFixture()
×
993
{
994
    AddFixture af(this, m_doc);
×
995
    if (af.exec() == QDialog::Rejected)
×
996
        return;
×
997

998
    if (af.invalidAddress())
×
999
    {
1000
        QMessageBox msg(QMessageBox::Critical, tr("Error"),
×
1001
                tr("Please enter a valid address"), QMessageBox::Ok);
×
1002
        msg.exec();
×
1003
        return;
×
1004
    }
×
1005

1006
    quint32 latestFxi = Fixture::invalidId();
×
1007

1008
    QString name = af.name();
×
1009
    quint32 address = af.address();
×
1010
    quint32 universe = af.universe();
×
1011
    quint32 channels = af.channels();
×
1012
    int gap = af.gap();
×
1013

1014
    QLCFixtureDef* fixtureDef = af.fixtureDef();
×
1015
    QLCFixtureMode* mode = af.mode();
×
1016

1017
    FixtureGroup* addToGroup = NULL;
×
1018
    QTreeWidgetItem* current = m_fixtures_tree->currentItem();
×
1019
    if (current != NULL)
×
1020
    {
1021
        if (current->parent() != NULL)
×
1022
        {
1023
            // Fixture selected
1024
            QVariant var = current->parent()->data(KColumnName, PROP_GROUP);
×
1025
            if (var.isValid() == true)
×
1026
                addToGroup = m_doc->fixtureGroup(var.toUInt());
×
1027
        }
×
1028
        else
1029
        {
1030
            // Group selected
1031
            QVariant var = current->data(KColumnName, PROP_GROUP);
×
1032
            if (var.isValid() == true)
×
1033
                addToGroup = m_doc->fixtureGroup(var.toUInt());
×
1034
        }
×
1035
    }
1036

1037
    /* If an empty name was given use the model instead */
1038
    if (name.simplified().isEmpty())
×
1039
    {
1040
        if (fixtureDef != NULL)
×
1041
            name = fixtureDef->model();
×
1042
        else
1043
            name = tr("Generic Dimmer");
×
1044
    }
1045

1046
    /* Add the rest (if any) WITH address gap */
1047
    for (int i = 0; i < af.amount(); i++)
×
1048
    {
1049
        QString modname;
×
1050

1051
        /* If we're adding more than one fixture,
1052
           append a number to the end of the name */
1053
        if (af.amount() > 1)
×
1054
            modname = QString("%1 #%2").arg(name).arg(i + 1, AppUtil::digits(af.amount()), 10, QChar('0'));
×
1055
        else
1056
            modname = name;
×
1057

1058
        /* Create the fixture */
1059
        Fixture* fxi = new Fixture(m_doc);
×
1060

1061
        /* Assign the next address AFTER the previous fixture
1062
           address space plus gap. */
1063
        fxi->setAddress(address + (i * channels) + (i * gap));
×
1064
        fxi->setUniverse(universe);
×
1065
        fxi->setName(modname);
×
1066
        /* Set a fixture definition & mode if they were
1067
           selected. Otherwise create a fixture definition
1068
           and mode for a generic dimmer. */
1069
        if (fixtureDef != NULL && mode != NULL)
×
1070
        {
1071
            fxi->setFixtureDefinition(fixtureDef, mode);
×
1072
        }
1073
        else
1074
        {
1075
            QLCFixtureDef* genericDef = fxi->genericDimmerDef(channels);
×
1076
            QLCFixtureMode* genericMode = fxi->genericDimmerMode(genericDef, channels);
×
1077
            fxi->setFixtureDefinition(genericDef, genericMode);
×
1078
        }
1079

1080
        m_doc->addFixture(fxi);
×
1081
        latestFxi = fxi->id();
×
1082
        if (addToGroup != NULL)
×
1083
            addToGroup->assignFixture(latestFxi);
×
1084
    }
×
1085

1086
    QTreeWidgetItem* selectItem = m_fixtures_tree->fixtureItem(latestFxi);
×
1087
    if (selectItem != NULL)
×
1088
        m_fixtures_tree->setCurrentItem(selectItem);
×
1089

1090
    updateView();
×
1091
}
×
1092

1093
void FixtureManager::addChannelsGroup()
×
1094
{
1095
    ChannelsGroup *group = new ChannelsGroup(m_doc);
×
1096

1097
    AddChannelsGroup cs(this, m_doc, group);
×
1098
    if (cs.exec() == QDialog::Accepted)
×
1099
    {
1100
        qDebug() << "Channels group added. Count: " << group->getChannels().count();
×
1101
        m_doc->addChannelsGroup(group, group->id());
×
1102
        updateChannelsGroupView();
×
1103
    }
1104
    else
1105
        delete group;
×
1106
}
×
1107

1108
void FixtureManager::slotAdd()
×
1109
{
1110
    if (m_currentTabIndex == 1)
×
1111
        addChannelsGroup();
×
1112
    else
1113
        addFixture();
×
1114
}
×
1115

1116
void FixtureManager::slotAddRGBPanel()
×
1117
{
1118
    AddRGBPanel rgb(this, m_doc);
×
1119
    if (rgb.exec() == QDialog::Accepted)
×
1120
    {
1121
        int rows = rgb.rows();
×
1122
        int columns = rgb.columns();
×
1123
        Fixture::Components components = rgb.components();
×
1124

1125
        FixtureGroup *grp = new FixtureGroup(m_doc);
×
1126
        Q_ASSERT(grp != NULL);
×
1127
        grp->setName(rgb.name());
×
1128
        QSize panelSize(columns, rows);
×
1129
        grp->setSize(panelSize);
×
1130
        m_doc->addFixtureGroup(grp);
×
1131
        updateGroupMenu();
×
1132

1133
        int transpose = 0;
×
1134
        if (rgb.direction() == AddRGBPanel::Vertical)
×
1135
        {
1136
                int tmp = columns;
×
1137
                columns = rows;
×
1138
                rows = tmp;
×
1139
                transpose = 1;
×
1140
        }
1141

1142
        QLCFixtureDef *rowDef = NULL;
×
1143
        QLCFixtureMode *rowMode = NULL;
×
1144
        quint32 address = (quint32)rgb.address();
×
1145
        int uniIndex = rgb.universeIndex();
×
1146
        int currRow = 0;
×
1147
        int rowInc = 1;
×
1148
        int xPosStart = 0;
×
1149
        int xPosEnd = columns - 1;
×
1150
        int xPosInc = 1;
×
1151

1152
        quint32 phyWidth = rgb.physicalWidth();
×
1153
        quint32 phyHeight = rgb.physicalHeight() / rows;
×
1154

1155
        if (transpose)
×
1156
        {
1157
                        if (rgb.orientation() == AddRGBPanel::TopRight ||
×
1158
                                rgb.orientation() == AddRGBPanel::BottomRight)
×
1159
                        {
1160
                                currRow = rows -1;
×
1161
                                rowInc = -1;
×
1162
                        }
1163
                        if (rgb.orientation() == AddRGBPanel::BottomRight ||
×
1164
                                rgb.orientation() == AddRGBPanel::BottomLeft)
×
1165
                        {
1166
                                xPosStart = columns - 1;
×
1167
                                xPosEnd = 0;
×
1168
                                xPosInc = -1;
×
1169
                        }
1170
        }
1171
        else
1172
        {
1173
                        if (rgb.orientation() == AddRGBPanel::BottomLeft ||
×
1174
                                rgb.orientation() == AddRGBPanel::BottomRight)
×
1175
                        {
1176
                                currRow = rows -1;
×
1177
                                rowInc = -1;
×
1178
                        }
1179
                        if (rgb.orientation() == AddRGBPanel::TopRight ||
×
1180
                                rgb.orientation() == AddRGBPanel::BottomRight)
×
1181
                        {
1182
                                xPosStart = columns - 1;
×
1183
                                xPosEnd = 0;
×
1184
                                xPosInc = -1;
×
1185
                        }
1186
        }
1187

1188
        for (int i = 0; i < rows; i++)
×
1189
        {
1190
            Fixture *fxi = new Fixture(m_doc);
×
1191
            Q_ASSERT(fxi != NULL);
×
1192
            fxi->setName(tr("%1 - Row %2").arg(rgb.name()).arg(i + 1));
×
1193
            if (rowDef == NULL)
×
1194
                rowDef = fxi->genericRGBPanelDef(columns, components, rgb.is16Bit());
×
1195
            if (rowMode == NULL)
×
1196
                rowMode = fxi->genericRGBPanelMode(rowDef, components, rgb.is16Bit(), phyWidth, phyHeight);
×
1197
            fxi->setFixtureDefinition(rowDef, rowMode);
×
1198

1199
            // Check universe span
1200
            if (address + fxi->channels() > 512)
×
1201
            {
1202
                if (!rgb.crossUniverse())
×
1203
                {
1204
                    uniIndex++;
×
1205
                    address = 0;
×
1206
                }
1207
            }
1208
            if (m_doc->inputOutputMap()->getUniverseID(uniIndex) == m_doc->inputOutputMap()->invalidUniverse())
×
1209
            {
1210
                m_doc->inputOutputMap()->addUniverse();
×
1211
                m_doc->inputOutputMap()->startUniverses();
×
1212
            }
1213

1214
            fxi->setUniverse(m_doc->inputOutputMap()->getUniverseID(uniIndex));
×
1215
            if (address + fxi->channels() > 512)
×
1216
                fxi->setCrossUniverse(rgb.crossUniverse());
×
1217
            fxi->setAddress(address);
×
1218
            m_doc->addFixture(fxi, Fixture::invalidId(), rgb.crossUniverse());
×
1219

1220
            address += fxi->channels();
×
1221
            if (address >= 512 && rgb.crossUniverse())
×
1222
            {
1223
                address -= 512;
×
1224
                uniIndex++;
×
1225
            }
1226

1227
            if (rgb.type() == AddRGBPanel::ZigZag)
×
1228
            {
1229
                int xPos = xPosStart;
×
1230
                for (int h = 0; h < fxi->heads(); h++)
×
1231
                {
1232
                        if (transpose)
×
1233
                                grp->assignHead(QLCPoint(currRow, xPos), GroupHead(fxi->id(), h));
×
1234
                        else
1235
                                grp->assignHead(QLCPoint(xPos, currRow), GroupHead(fxi->id(), h));
×
1236
                    xPos += xPosInc;
×
1237
                }
1238
            }
1239
            else if (rgb.type() == AddRGBPanel::Snake)
×
1240
            {
1241
                if (i%2 == 0)
×
1242
                {
1243
                    int xPos = xPosStart;
×
1244
                    for (int h = 0; h < fxi->heads(); h++)
×
1245
                    {
1246
                            if (transpose)
×
1247
                                    grp->assignHead(QLCPoint(currRow, xPos), GroupHead(fxi->id(), h));
×
1248
                            else
1249
                                    grp->assignHead(QLCPoint(xPos, currRow), GroupHead(fxi->id(), h));
×
1250
                        xPos += xPosInc;
×
1251
                    }
1252
                }
1253
                else
1254
                {
1255
                    int xPos = xPosEnd;
×
1256
                    for (int h = 0; h < fxi->heads(); h++)
×
1257
                    {
1258
                            if (transpose)
×
1259
                                    grp->assignHead(QLCPoint(currRow, xPos), GroupHead(fxi->id(), h));
×
1260
                            else
1261
                                    grp->assignHead(QLCPoint(xPos, currRow), GroupHead(fxi->id(), h));
×
1262
                        xPos += (-xPosInc);
×
1263
                    }
1264
                }
1265
            }
1266
            currRow += rowInc;
×
1267
        }
1268

1269
        updateView();
×
1270
        m_doc->setModified();
×
1271
    }
1272
}
×
1273

1274
void FixtureManager::removeFixture()
×
1275
{
1276
    // Ask before deletion
1277
    if (QMessageBox::question(this, tr("Delete Fixtures"),
×
1278
                              tr("Do you want to delete the selected items?"),
×
1279
                              QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
×
1280
    {
1281
        return;
×
1282
    }
1283

1284
    QListIterator <QTreeWidgetItem*> it(m_fixtures_tree->selectedItems());
×
1285

1286
    // We put items to delete in sets,
1287
    // so no segfault happens when the same fixture is selected twice
1288
    QSet <quint32> groupsToDelete;
×
1289
    QSet <quint32> fixturesToDelete;
×
1290
    while (it.hasNext() == true)
×
1291
    {
1292
        QTreeWidgetItem* item(it.next());
×
1293
        Q_ASSERT(item != NULL);
×
1294

1295
        // Is the item a fixture ?
1296
        QVariant var = item->data(KColumnName, PROP_ID);
×
1297
        if (var.isValid() == true)
×
1298
            fixturesToDelete << var.toUInt();
×
1299
        else
1300
        {
1301
            // Is the item a fixture group ?
1302
            var = item->data(KColumnName, PROP_GROUP);
×
1303
            if (var.isValid() == true)
×
1304
                groupsToDelete << var.toUInt();
×
1305
        }
1306
    }
×
1307

1308
    // delete fixture groups
1309
    foreach (quint32 id, groupsToDelete)
×
1310
        m_doc->deleteFixtureGroup(id);
×
1311

1312
    // delete fixtures
1313
    foreach (quint32 id, fixturesToDelete)
×
1314
    {
1315
        /** @todo This is REALLY bogus here, since Fixture or Doc should do
1316
            this. However, FixtureManager is the only place to destroy fixtures,
1317
            so it's rather safe to reset the fixture's address space here. */
1318
        Fixture* fxi = m_doc->fixture(id);
×
1319
        Q_ASSERT(fxi != NULL);
×
1320
        QList<Universe*> ua = m_doc->inputOutputMap()->claimUniverses();
×
1321
        int universe = fxi->universe();
×
1322
        if (universe < ua.count())
×
1323
            ua[universe]->reset(fxi->address(), fxi->channels());
×
1324
        m_doc->inputOutputMap()->releaseUniverses();
×
1325

1326
        m_doc->deleteFixture(id);
×
1327
    }
×
1328
}
×
1329

1330
void FixtureManager::removeChannelsGroup()
×
1331
{
1332
    // Ask before deletion
1333
    if (QMessageBox::question(this, tr("Delete Channels Group"),
×
1334
                              tr("Do you want to delete the selected groups?"),
×
1335
                              QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
×
1336
    {
1337
        return;
×
1338
    }
1339

1340
    disconnect(m_channel_groups_tree, SIGNAL(itemSelectionChanged()),
×
1341
            this, SLOT(slotChannelsGroupSelectionChanged()));
1342

1343
    QListIterator <QTreeWidgetItem*> it(m_channel_groups_tree->selectedItems());
×
1344
    while (it.hasNext() == true)
×
1345
    {
1346
        QTreeWidgetItem* item(it.next());
×
1347
        Q_ASSERT(item != NULL);
×
1348

1349
        QVariant var = item->data(KColumnName, PROP_ID);
×
1350
        if (var.isValid() == true)
×
1351
            m_doc->deleteChannelsGroup(var.toUInt());
×
1352
    }
×
1353
    updateChannelsGroupView();
×
1354

1355
    connect(m_channel_groups_tree, SIGNAL(itemSelectionChanged()),
×
1356
            this, SLOT(slotChannelsGroupSelectionChanged()));
1357
}
×
1358

1359
void FixtureManager::slotRemove()
×
1360
{
1361
    if (m_currentTabIndex == 1)
×
1362
        removeChannelsGroup();
×
1363
    else
1364
        removeFixture();
×
1365
}
×
1366

1367
void FixtureManager::editFixtureProperties()
×
1368
{
1369
    QTreeWidgetItem* item = m_fixtures_tree->currentItem();
×
1370
    if (item == NULL)
×
1371
        return;
×
1372

1373
    QVariant var = item->data(KColumnName, PROP_ID);
×
1374
    if (var.isValid() == false)
×
1375
        return;
×
1376

1377
    quint32 id = var.toUInt();
×
1378
    Fixture* fxi = m_doc->fixture(id);
×
1379
    if (fxi == NULL)
×
1380
        return;
×
1381

1382
    QString manuf;
×
1383
    QString model;
×
1384
    QString mode;
×
1385

1386
    if (fxi->fixtureDef() != NULL)
×
1387
    {
1388
        manuf = fxi->fixtureDef()->manufacturer();
×
1389
        model = fxi->fixtureDef()->model();
×
1390
        mode = fxi->fixtureMode()->name();
×
1391
    }
1392

1393
    AddFixture af(this, m_doc, fxi);
×
1394
    af.setWindowTitle(tr("Change fixture properties"));
×
1395
    if (af.exec() == QDialog::Accepted)
×
1396
    {
1397
        if (af.invalidAddress() == false)
×
1398
        {
1399
            bool changed = false;
×
1400

1401
            fxi->blockSignals(true);
×
1402
            if (fxi->name() != af.name())
×
1403
            {
1404
                fxi->setName(af.name());
×
1405
                changed = true;
×
1406
            }
1407
            if (fxi->universe() != af.universe())
×
1408
            {
1409
                fxi->setUniverse(af.universe());
×
1410
                changed = true;
×
1411
            }
1412
            if (fxi->address() != af.address())
×
1413
            {
1414
                fxi->setAddress(af.address());
×
1415
                changed = true;
×
1416
            }
1417
            fxi->blockSignals(false);
×
1418

1419
            if (af.fixtureDef() != NULL && af.mode() != NULL)
×
1420
            {
1421
                if (af.fixtureDef()->manufacturer() == KXMLFixtureGeneric &&
×
1422
                    af.fixtureDef()->model() == KXMLFixtureGeneric)
×
1423
                {
1424
                    if (fxi->channels() != af.channels())
×
1425
                    {
1426
                        QLCFixtureDef* fixtureDef = fxi->genericDimmerDef(af.channels());
×
1427
                        QLCFixtureMode* fixtureMode = fxi->genericDimmerMode(fixtureDef, af.channels());
×
1428
                        fxi->setFixtureDefinition(fixtureDef, fixtureMode);
×
1429
                    }
1430
                }
1431
                else
1432
                {
1433
                    fxi->setFixtureDefinition(af.fixtureDef(), af.mode());
×
1434
                }
1435
            }
1436
            else
1437
            {
1438
                /* Generic dimmer */
1439
                fxi->setFixtureDefinition(NULL, NULL);
×
1440
                fxi->setChannels(af.channels());
×
1441
            }
1442

1443
            // Emit changed signal
1444
            if (changed)
×
1445
                fxi->setID(fxi->id());
×
1446

1447
            updateView();
×
1448
            slotSelectionChanged();
×
1449
        }
1450
        else
1451
        {
1452
            QMessageBox msg(QMessageBox::Critical, tr("Error"),
×
1453
                    tr("Please enter a valid address"), QMessageBox::Ok);
×
1454
            msg.exec();
×
1455
        }
×
1456
    }
1457
}
×
1458

1459
void FixtureManager::editChannelGroupProperties()
×
1460
{
1461
    int selectedCount = m_channel_groups_tree->selectedItems().size();
×
1462

1463
    if (selectedCount > 0)
×
1464
    {
1465
        QTreeWidgetItem* current = m_channel_groups_tree->selectedItems().first();
×
1466
        QVariant var = current->data(KColumnName, PROP_ID);
×
1467
        if (var.isValid() == true)
×
1468
        {
1469
            ChannelsGroup *group = m_doc->channelsGroup(var.toUInt());
×
1470

1471
            AddChannelsGroup cs(this, m_doc, group);
×
1472
            if (cs.exec() == QDialog::Accepted)
×
1473
            {
1474
                qDebug() << "CHANNEL GROUP MODIFIED. Count: " << group->getChannels().count();
×
1475
                m_doc->addChannelsGroup(group, group->id());
×
1476
                updateChannelsGroupView();
×
1477
            }
1478
        }
×
1479
    }
×
1480
}
×
1481

1482
int FixtureManager::headCount(const QList <QTreeWidgetItem*>& items) const
×
1483
{
1484
    int count = 0;
×
1485
    QListIterator <QTreeWidgetItem*> it(items);
×
1486
    while (it.hasNext() == true)
×
1487
    {
1488
        QTreeWidgetItem* item = it.next();
×
1489
        Q_ASSERT(item != NULL);
×
1490

1491
        QVariant var = item->data(KColumnName, PROP_ID);
×
1492
        if (var.isValid() == false)
×
1493
            continue;
×
1494

1495
        Fixture* fxi = m_doc->fixture(var.toUInt());
×
1496
        count += fxi->heads();
×
1497
    }
×
1498

1499
    return count;
×
1500
}
×
1501

1502
void FixtureManager::slotProperties()
×
1503
{
1504
    if (m_currentTabIndex == 1)
×
1505
        editChannelGroupProperties();
×
1506
    else
1507
        editFixtureProperties();
×
1508
}
×
1509

1510
void FixtureManager::slotFadeConfig()
×
1511
{
1512
    ChannelsSelection cfg(m_doc, this, ChannelsSelection::ConfigurationMode);
×
1513
    if (cfg.exec() == QDialog::Rejected)
×
1514
        return; // User pressed cancel
×
1515
    m_doc->setModified();
×
1516
}
×
1517

1518
void FixtureManager::slotRemap()
×
1519
{
1520
    FixtureRemap fxr(m_doc);
×
1521
    if (fxr.exec() == QDialog::Rejected)
×
1522
        return; // User pressed cancel
×
1523

1524
    updateView();
×
1525
}
×
1526

1527
void FixtureManager::slotUnGroup()
×
1528
{
1529
    if (QMessageBox::question(this, tr("Ungroup fixtures?"),
×
1530
                              tr("Do you want to ungroup the selected fixtures?"),
×
1531
                              QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
×
1532
    {
1533
        return;
×
1534
    }
1535

1536
    // Because FixtureGroup::resignFixture() emits changed(), which makes the tree
1537
    // update its contents in the middle, invalidating m_tree->selectedItems(),
1538
    // we must pick the list of fixtures and groups first and then resign them in
1539
    // one big bunch.
1540
    QList <QPair<quint32,quint32> > resignList;
×
1541

1542
    foreach (QTreeWidgetItem* item, m_fixtures_tree->selectedItems())
×
1543
    {
1544
        if (item->parent() == NULL)
×
1545
            continue;
×
1546

1547
        QVariant var = item->parent()->data(KColumnName, PROP_GROUP);
×
1548
        if (var.isValid() == false)
×
1549
            continue;
×
1550
        quint32 grp = var.toUInt();
×
1551

1552
        var = item->data(KColumnName, PROP_ID);
×
1553
        if (var.isValid() == false)
×
1554
            continue;
×
1555
        quint32 fxi = var.toUInt();
×
1556

1557
        resignList << QPair <quint32,quint32> (grp, fxi);
×
1558
    }
×
1559

1560
    QListIterator <QPair<quint32,quint32> > it(resignList);
×
1561
    while (it.hasNext() == true)
×
1562
    {
1563
        QPair <quint32,quint32> pair(it.next());
×
1564
        FixtureGroup* grp = m_doc->fixtureGroup(pair.first);
×
1565
        Q_ASSERT(grp != NULL);
×
1566
        grp->resignFixture(pair.second);
×
1567
    }
1568
}
×
1569

1570
void FixtureManager::slotGroupSelected(QAction* action)
×
1571
{
1572
    FixtureGroup* grp = NULL;
×
1573

1574
    if (action->data().isValid() == true)
×
1575
    {
1576
        // Existing group selected
1577
        grp = (FixtureGroup*) (action->data().toULongLong());
×
1578
        Q_ASSERT(grp != NULL);
×
1579
    }
1580
    else
1581
    {
1582
        // New Group selected.
1583

1584
        // Suggest an equilateral grid
1585
        qreal side = sqrt(headCount(m_fixtures_tree->selectedItems()));
×
1586
        if (side != floor(side))
×
1587
            side += 1; // Fixture number doesn't provide a full square
×
1588

1589
        CreateFixtureGroup cfg(this);
×
1590
        cfg.setSize(QSize(side, side));
×
1591
        if (cfg.exec() != QDialog::Accepted)
×
1592
            return; // User pressed cancel
×
1593

1594
        grp = new FixtureGroup(m_doc);
×
1595
        Q_ASSERT(grp != NULL);
×
1596
        grp->setName(cfg.name());
×
1597
        grp->setSize(cfg.size());
×
1598
        m_doc->addFixtureGroup(grp);
×
1599
        updateGroupMenu();
×
1600
    }
×
1601

1602
    // Assign selected fixture items to the group
1603
    foreach (QTreeWidgetItem* item, m_fixtures_tree->selectedItems())
×
1604
    {
1605
        QVariant var = item->data(KColumnName, PROP_ID);
×
1606
        if (var.isValid() == false)
×
1607
            continue;
×
1608

1609
        grp->assignFixture(var.toUInt());
×
1610
    }
×
1611

1612
    updateView();
×
1613
}
1614

1615
void FixtureManager::slotMoveGroupUp()
×
1616
{
1617
    if (m_channel_groups_tree->selectedItems().size() > 0)
×
1618
    {
1619
        QTreeWidgetItem* item = m_channel_groups_tree->selectedItems().first();
×
1620
        quint32 grpID = item->data(KColumnName, PROP_ID).toUInt();
×
1621
        m_doc->moveChannelGroup(grpID, -1);
×
1622
        updateChannelsGroupView();
×
1623
    }
1624
}
×
1625

1626
void FixtureManager::slotMoveGroupDown()
×
1627
{
1628
    if (m_channel_groups_tree->selectedItems().size() > 0)
×
1629
    {
1630
        QTreeWidgetItem* item = m_channel_groups_tree->selectedItems().first();
×
1631
        quint32 grpID = item->data(KColumnName, PROP_ID).toUInt();
×
1632
        m_doc->moveChannelGroup(grpID, 1);
×
1633
        updateChannelsGroupView();
×
1634
    }
1635
}
×
1636

1637
QString FixtureManager::createDialog(bool import)
×
1638
{
1639
    QString fileName;
×
1640

1641
    /* Create a file save dialog */
1642
    QFileDialog dialog(this);
×
1643
    if (import == true)
×
1644
    {
1645
        dialog.setWindowTitle(tr("Import Fixtures List"));
×
1646
        dialog.setAcceptMode(QFileDialog::AcceptOpen);
×
1647
    }
1648
    else
1649
    {
1650
        dialog.setWindowTitle(tr("Export Fixtures List As"));
×
1651
        dialog.setAcceptMode(QFileDialog::AcceptSave);
×
1652
    }
1653

1654
    /* Append file filters to the dialog */
1655
    QStringList filters;
×
1656
    filters << tr("Fixtures List (*%1)").arg(KExtFixtureList);
×
1657
#if defined(WIN32) || defined(Q_OS_WIN)
1658
    filters << tr("All Files (*.*)");
1659
#else
1660
    filters << tr("All Files (*)");
×
1661
#endif
1662
    dialog.setNameFilters(filters);
×
1663

1664
    /* Append useful URLs to the dialog */
1665
    QList <QUrl> sidebar;
×
1666
    sidebar.append(QUrl::fromLocalFile(QDir::homePath()));
×
1667
    sidebar.append(QUrl::fromLocalFile(QDir::rootPath()));
×
1668
    dialog.setSidebarUrls(sidebar);
×
1669

1670
    /* Get file name */
1671
    if (dialog.exec() != QDialog::Accepted)
×
1672
        return "";
×
1673

1674
    fileName = dialog.selectedFiles().first();
×
1675
    if (fileName.isEmpty() == true)
×
1676
        return "";
×
1677

1678
    /* Always use the fixture definition suffix */
1679
    if (import == false && fileName.right(5) != KExtFixtureList)
×
1680
        fileName += KExtFixtureList;
×
1681

1682
    return fileName;
×
1683
}
×
1684

1685
void FixtureManager::slotImport()
×
1686
{
1687
    QString fileName = createDialog(true);
×
1688

1689
    QXmlStreamReader *doc = QLCFile::getXMLReader(fileName);
×
1690
    if (doc == NULL || doc->device() == NULL || doc->hasError())
×
1691
    {
1692
        qWarning() << Q_FUNC_INFO << "Unable to read from" << fileName;
×
1693
        return;
×
1694
    }
1695

1696
    while (!doc->atEnd())
×
1697
    {
1698
        if (doc->readNext() == QXmlStreamReader::DTD)
×
1699
            break;
×
1700
    }
1701
    if (doc->hasError())
×
1702
    {
1703
        QLCFile::releaseXMLReader(doc);
×
1704
        return;
×
1705
    }
1706

1707
    if (doc->dtdName() == KXMLQLCFixturesList)
×
1708
    {
1709
        doc->readNextStartElement();
×
1710
        if (doc->name() != KXMLQLCFixturesList)
×
1711
        {
1712
            qWarning() << Q_FUNC_INFO << "Fixture Definition node not found";
×
1713
            QLCFile::releaseXMLReader(doc);
×
1714
            return;
×
1715
        }
1716

1717
        while (doc->readNextStartElement())
×
1718
        {
1719
            if (doc->name() == KXMLFixture)
×
1720
            {
1721
                Fixture* fxi = new Fixture(m_doc);
×
1722
                Q_ASSERT(fxi != NULL);
×
1723

1724
                if (fxi->loadXML(*doc, m_doc, m_doc->fixtureDefCache()) == true)
×
1725
                {
1726
                    if (m_doc->addFixture(fxi /*, fxi->id()*/) == true)
×
1727
                    {
1728
                        /* Success */
1729
                        qWarning() << Q_FUNC_INFO << "Fixture" << fxi->name() << "successfully created.";
×
1730
                    }
1731
                    else
1732
                    {
1733
                        /* Doc is full */
1734
                        qWarning() << Q_FUNC_INFO << "Fixture" << fxi->name() << "cannot be created.";
×
1735
                        delete fxi;
×
1736
                    }
1737
                }
1738
                else
1739
                {
1740
                    qWarning() << Q_FUNC_INFO << "Fixture" << fxi->name() << "cannot be loaded.";
×
1741
                    delete fxi;
×
1742
                }
1743
            }
1744
            else if (doc->name() == KXMLQLCFixtureGroup)
×
1745
            {
1746
                FixtureGroup* grp = new FixtureGroup(m_doc);
×
1747
                Q_ASSERT(grp != NULL);
×
1748

1749
                if (grp->loadXML(*doc) == true)
×
1750
                {
1751
                    m_doc->addFixtureGroup(grp, grp->id());
×
1752
                }
1753
                else
1754
                {
1755
                    qWarning() << Q_FUNC_INFO << "FixtureGroup" << grp->name() << "cannot be loaded.";
×
1756
                    delete grp;
×
1757
                }
1758
            }
1759
            else
1760
            {
1761
                qWarning() << Q_FUNC_INFO << "Unknown label tag:" << doc->name().toString();
×
1762
                doc->skipCurrentElement();
×
1763
            }
1764
        }
1765
        updateView();
×
1766
    }
1767
    QLCFile::releaseXMLReader(doc);
×
1768
}
×
1769

1770
void FixtureManager::slotExport()
×
1771
{
1772
    QString fileName = createDialog(false);
×
1773

1774
    QFile file(fileName);
×
1775
    if (file.open(QIODevice::WriteOnly) == false)
×
1776
        return;
×
1777

1778
    QXmlStreamWriter doc(&file);
×
1779
    doc.setAutoFormatting(true);
×
1780
    doc.setAutoFormattingIndent(1);
×
1781
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1782
    doc.setCodec("UTF-8");
1783
#endif
1784
    QLCFile::writeXMLHeader(&doc, KXMLQLCFixturesList);
×
1785

1786
    QListIterator <Fixture*> fxit(m_doc->fixtures());
×
1787
    while (fxit.hasNext() == true)
×
1788
    {
1789
        Fixture* fxi(fxit.next());
×
1790
        Q_ASSERT(fxi != NULL);
×
1791
        fxi->saveXML(&doc);
×
1792
    }
1793

1794
    QListIterator <FixtureGroup*>grpit(m_doc->fixtureGroups());
×
1795
    while (grpit.hasNext() == true)
×
1796
    {
1797
        FixtureGroup *fxgrp(grpit.next());
×
1798
        Q_ASSERT(fxgrp != NULL);
×
1799
        fxgrp->saveXML(&doc);
×
1800
    }
1801

1802
    doc.writeEndDocument();
×
1803
    file.close();
×
1804
}
×
1805

1806
void FixtureManager::slotContextMenuRequested(const QPoint&)
×
1807
{
1808
    QMenu menu(this);
×
1809
    menu.addAction(m_addAction);
×
1810
    menu.addAction(m_addRGBAction);
×
1811
    menu.addAction(m_propertiesAction);
×
1812
    menu.addAction(m_removeAction);
×
1813
    menu.addSeparator();
×
1814
    menu.addAction(m_groupAction);
×
1815
    menu.addAction(m_unGroupAction);
×
1816
    menu.exec(QCursor::pos());
×
1817
}
×
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