1 /*
2     SPDX-FileCopyrightText: 2003 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "indiproperty.h"
8 
9 #include "clientmanager.h"
10 #include "indidevice.h"
11 #include "indielement.h"
12 #include "indigroup.h"
13 #include "kstars.h"
14 #include "Options.h"
15 #include "skymap.h"
16 #include "dialogs/timedialog.h"
17 
18 #include <indicom.h>
19 #include <indiproperty.h>
20 
21 #include <KLed>
22 #include <KSqueezedTextLabel>
23 
24 #include <QAbstractButton>
25 #include <QButtonGroup>
26 #include <QCheckBox>
27 #include <QComboBox>
28 #include <QHBoxLayout>
29 #include <QPushButton>
30 #include <QVBoxLayout>
31 
32 extern const char *libindi_strings_context;
33 
34 /*******************************************************************
35 ** INDI Property: contains widgets, labels, and their status
36 *******************************************************************/
INDI_P(INDI_G * ipg,INDI::Property prop)37 INDI_P::INDI_P(INDI_G *ipg, INDI::Property prop) : QWidget(ipg), pg(ipg), dataProp(prop)
38 {
39     name = QString(prop.getName());
40 
41     PHBox = new QHBoxLayout(this);
42     PHBox->setObjectName("Property Horizontal Layout");
43     PHBox->setContentsMargins(0, 0, 0, 0);
44     PVBox = new QVBoxLayout;
45     PVBox->setContentsMargins(0, 0, 0, 0);
46     PVBox->setObjectName("Property Vertical Layout");
47 
48     initGUI();
49 }
50 
updateStateLED()51 void INDI_P::updateStateLED()
52 {
53     /* set state light */
54     switch (dataProp.getState())
55     {
56         case IPS_IDLE:
57             ledStatus->setColor(Qt::gray);
58             break;
59 
60         case IPS_OK:
61             ledStatus->setColor(Qt::green);
62             break;
63 
64         case IPS_BUSY:
65             ledStatus->setColor(Qt::yellow);
66             break;
67 
68         case IPS_ALERT:
69             ledStatus->setColor(Qt::red);
70             break;
71     }
72 }
73 
74 /* build widgets for property pp using info in root.
75  */
initGUI()76 void INDI_P::initGUI()
77 {
78     QString label = i18nc(libindi_strings_context, dataProp.getLabel());
79 
80     if (label == "(I18N_EMPTY_MESSAGE)")
81         label = dataProp.getLabel();
82 
83     /* add to GUI group */
84     ledStatus = new KLed(this);
85     ledStatus->setMaximumSize(16, 16);
86     ledStatus->setLook(KLed::Sunken);
87 
88     updateStateLED();
89 
90     /* Create a horizontally layout widget around light and label */
91     QWidget *labelWidget = new QWidget(this);
92     QHBoxLayout *labelLayout =  new QHBoxLayout(labelWidget);
93     labelLayout->setContentsMargins(0, 0, 0, 0);
94 
95     /* #1 First widget is the LED status indicator */
96     labelLayout->addWidget(ledStatus);
97 
98     if (label.isEmpty())
99     {
100         label = i18nc(libindi_strings_context, name.toUtf8());
101         if (label == "(I18N_EMPTY_MESSAGE)")
102             label = name.toUtf8();
103 
104         labelW = new KSqueezedTextLabel(label, this);
105     }
106     else
107         labelW = new KSqueezedTextLabel(label, this);
108 
109     //labelW->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
110     labelW->setFrameShape(QFrame::Box);
111     labelW->setFrameShadow(QFrame::Sunken);
112     labelW->setMargin(2);
113     labelW->setFixedWidth(PROPERTY_LABEL_WIDTH * KStars::Instance()->devicePixelRatio());
114     labelW->setTextFormat(Qt::RichText);
115     labelW->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
116     labelW->setWordWrap(true);
117 
118     labelLayout->addWidget(labelW);
119     PHBox->addWidget(labelWidget, 0, Qt::AlignTop | Qt::AlignLeft);
120 
121     ledStatus->show();
122     labelW->show();
123 
124     // #3 Add the Vertical layout which may contain several elements
125     PHBox->addLayout(PVBox);
126 
127     switch (dataProp.getType())
128     {
129         case INDI_SWITCH:
130             if (dataProp.getSwitch()->getRule() == ISR_NOFMANY)
131                 guiType = PG_RADIO;
132             else if (dataProp.getSwitch()->count() > 4)
133                 guiType = PG_MENU;
134             else
135                 guiType = PG_BUTTONS;
136 
137             if (guiType == PG_MENU)
138                 buildMenuGUI();
139             else
140                 buildSwitchGUI();
141             break;
142 
143         case INDI_TEXT:
144             buildTextGUI();
145             break;
146 
147         case INDI_NUMBER:
148             buildNumberGUI();
149             break;
150 
151         case INDI_LIGHT:
152             buildLightGUI();
153             break;
154 
155         case INDI_BLOB:
156             buildBLOBGUI();
157             break;
158 
159         default:
160             break;
161     }
162 }
163 
buildSwitchGUI()164 void INDI_P::buildSwitchGUI()
165 {
166     auto svp = dataProp.getSwitch();
167 
168     if (!svp)
169         return;
170 
171     groupB = new QButtonGroup(this);
172 
173     if (guiType == PG_BUTTONS)
174     {
175         if (svp->getRule() == ISR_1OFMANY)
176             groupB->setExclusive(true);
177         else
178             groupB->setExclusive(false);
179     }
180     else if (guiType == PG_RADIO)
181         groupB->setExclusive(false);
182 
183     if (svp->p != IP_RO)
184         QObject::connect(groupB, SIGNAL(buttonClicked(QAbstractButton*)), this, SLOT(newSwitch(QAbstractButton*)));
185 
186     for (auto &it : *svp)
187     {
188         auto lp = new INDI_E(this, dataProp);
189         lp->buildSwitch(groupB, &it);
190         elementList.append(lp);
191     }
192 
193     horSpacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
194 
195     PHBox->addItem(horSpacer);
196 }
197 
buildTextGUI()198 void INDI_P::buildTextGUI()
199 {
200     auto tvp = dataProp.getText();
201 
202     if (!tvp)
203         return;
204 
205     for (auto &it : *tvp)
206     {
207         auto lp = new INDI_E(this, dataProp);
208         lp->buildText(&it);
209         elementList.append(lp);
210     }
211 
212     horSpacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
213 
214     PHBox->addItem(horSpacer);
215 
216     if (tvp->getPermission() == IP_RO)
217         return;
218 
219     // INDI STD, but we use our own controls
220     if (name == "TIME_UTC")
221         setupSetButton(i18n("Time"));
222     else
223         setupSetButton(i18n("Set"));
224 }
225 
buildNumberGUI()226 void INDI_P::buildNumberGUI()
227 {
228     auto nvp = dataProp.getNumber();
229 
230     if (!nvp)
231         return;
232 
233     for (auto &it : *nvp)
234     {
235         auto lp = new INDI_E(this, dataProp);
236         lp->buildNumber(&it);
237         elementList.append(lp);
238     }
239 
240     horSpacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
241 
242     PHBox->addItem(horSpacer);
243 
244     if (nvp->getPermission() == IP_RO)
245         return;
246 
247     setupSetButton(i18n("Set"));
248 }
249 
buildLightGUI()250 void INDI_P::buildLightGUI()
251 {
252     auto lvp = dataProp.getLight();
253 
254     if (!lvp)
255         return;
256 
257     for (auto &it : *lvp)
258     {
259         auto ep = new INDI_E(this, dataProp);
260         ep->buildLight(&it);
261         elementList.append(ep);
262     }
263 
264     horSpacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
265 
266     PHBox->addItem(horSpacer);
267 }
268 
buildBLOBGUI()269 void INDI_P::buildBLOBGUI()
270 {
271     auto bvp = dataProp.getBLOB();
272 
273     if (!bvp)
274         return;
275 
276     for (auto &it : *bvp)
277     {
278         auto lp = new INDI_E(this, dataProp);
279         lp->buildBLOB(&it);
280         elementList.append(lp);
281     }
282 
283     horSpacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
284 
285     PHBox->addItem(horSpacer);
286 
287     enableBLOBC = new QCheckBox();
288     enableBLOBC->setIcon(QIcon::fromTheme("network-modem"));
289     enableBLOBC->setChecked(true);
290     enableBLOBC->setToolTip(i18n("Enable binary data transfer from this property to KStars and vice-versa."));
291 
292     PHBox->addWidget(enableBLOBC);
293 
294     connect(enableBLOBC, SIGNAL(stateChanged(int)), this, SLOT(setBLOBOption(int)));
295 
296     if (dataProp.getPermission() != IP_RO)
297         setupSetButton(i18n("Upload"));
298 }
299 
setBLOBOption(int state)300 void INDI_P::setBLOBOption(int state)
301 {
302     pg->getDevice()->getClientManager()->setBLOBEnabled(state == Qt::Checked, dataProp.getDeviceName(), dataProp.getName());
303 }
304 
newSwitch(QAbstractButton * button)305 void INDI_P::newSwitch(QAbstractButton *button)
306 {
307     auto svp = dataProp.getSwitch();
308     QString buttonText = button->text();
309 
310     if (!svp)
311         return;
312 
313     buttonText.remove('&');
314 
315     for (auto &el : elementList)
316     {
317         if (el->getLabel() == buttonText)
318         {
319             newSwitch(el->getName());
320             return;
321         }
322     }
323 }
324 
resetSwitch()325 void INDI_P::resetSwitch()
326 {
327     auto svp = dataProp.getSwitch();
328 
329     if (!svp)
330         return;
331 
332     if (menuC != nullptr)
333     {
334         menuC->setCurrentIndex(svp->findOnSwitchIndex());
335     }
336 }
337 
newSwitch(int index)338 void INDI_P::newSwitch(int index)
339 {
340     auto svp = dataProp.getSwitch();
341 
342     if (!svp)
343         return;
344 
345     if (index >= svp->count() || index < 0)
346         return;
347 
348     auto sp = svp->at(index);
349 
350     svp->reset();
351     sp->setState(ISS_ON);
352 
353     sendSwitch();
354 }
355 
newSwitch(const QString & name)356 void INDI_P::newSwitch(const QString &name)
357 {
358     auto svp = dataProp.getSwitch();
359 
360     if (!svp)
361         return;
362 
363     auto sp = svp->findWidgetByName(name.toLatin1().constData());
364 
365     if (!sp)
366         return;
367 
368     if (svp->getRule() == ISR_1OFMANY)
369     {
370         svp->reset();
371         sp->setState(ISS_ON);
372     }
373     else
374     {
375         if (svp->getRule() == ISR_ATMOST1)
376         {
377             ISState prev_state = sp->getState();
378             svp->reset();
379             sp->setState(prev_state);
380         }
381 
382         sp->setState(sp->getState() == ISS_ON ? ISS_OFF : ISS_ON);
383     }
384 
385     sendSwitch();
386 }
387 
sendSwitch()388 void INDI_P::sendSwitch()
389 {
390     auto svp = dataProp.getSwitch();
391 
392     if (!svp)
393         return;
394 
395     svp->setState(IPS_BUSY);
396 
397     for (auto &el : elementList)
398         el->syncSwitch();
399 
400     updateStateLED();
401 
402     // Send it to server
403     pg->getDevice()->getClientManager()->sendNewSwitch(svp);
404 }
405 
sendText()406 void INDI_P::sendText()
407 {
408     switch (dataProp.getType())
409     {
410         case INDI_TEXT:
411         {
412             auto tvp = dataProp.getText();
413             if (!tvp)
414                 return;
415 
416             tvp->setState(IPS_BUSY);
417 
418             for (auto &el : elementList)
419                 el->updateTP();
420 
421             pg->getDevice()->getClientManager()->sendNewText(tvp);
422 
423             break;
424         }
425 
426         case INDI_NUMBER:
427         {
428             auto nvp = dataProp.getNumber();
429             if (!nvp)
430                 return;
431 
432             nvp->setState(IPS_BUSY);
433 
434             for (auto &el : elementList)
435                 el->updateNP();
436 
437             pg->getDevice()->getClientManager()->sendNewNumber(nvp);
438             break;
439         }
440         default:
441             break;
442     }
443 
444     updateStateLED();
445 }
446 
buildMenuGUI()447 void INDI_P::buildMenuGUI()
448 {
449     QStringList menuOptions;
450     QString oneOption;
451     int onItem = -1;
452     auto svp = dataProp.getSwitch();
453 
454     if (!svp)
455         return;
456 
457     menuC = new QComboBox(this);
458 
459     if (svp->getPermission() == IP_RO)
460         connect(menuC, SIGNAL(activated(int)), this, SLOT(resetSwitch()));
461     else
462         connect(menuC, SIGNAL(activated(int)), this, SLOT(newSwitch(int)));
463 
464     for (int i = 0; i < svp->nsp; i++)
465     {
466         auto tp = svp->at(i);
467 
468         if (tp->getState() == ISS_ON)
469             onItem = i;
470 
471         auto lp = new INDI_E(this, dataProp);
472 
473         lp->buildMenuItem(tp);
474 
475         oneOption = i18nc(libindi_strings_context, lp->getLabel().toUtf8());
476 
477         if (oneOption == "(I18N_EMPTY_MESSAGE)")
478             oneOption = lp->getLabel().toUtf8();
479 
480         menuOptions.append(oneOption);
481 
482         elementList.append(lp);
483     }
484 
485     menuC->addItems(menuOptions);
486     menuC->setCurrentIndex(onItem);
487 
488     horSpacer = new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
489 
490     PHBox->addWidget(menuC);
491     PHBox->addItem(horSpacer);
492 }
493 
setupSetButton(const QString & caption)494 void INDI_P::setupSetButton(const QString &caption)
495 {
496     setB = new QPushButton(caption, this);
497     setB->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
498     setB->setMinimumWidth(MIN_SET_WIDTH * KStars::Instance()->devicePixelRatio());
499     setB->setMaximumWidth(MAX_SET_WIDTH * KStars::Instance()->devicePixelRatio());
500 
501     connect(setB, SIGNAL(clicked()), this, SLOT(processSetButton()));
502 
503     PHBox->addWidget(setB);
504 }
505 
addWidget(QWidget * w)506 void INDI_P::addWidget(QWidget *w)
507 {
508     PHBox->addWidget(w);
509 }
510 
addLayout(QHBoxLayout * layout)511 void INDI_P::addLayout(QHBoxLayout *layout)
512 {
513     PVBox->addLayout(layout);
514 }
515 
updateMenuGUI()516 void INDI_P::updateMenuGUI()
517 {
518     auto svp = dataProp.getSwitch();
519 
520     if (!svp)
521         return;
522 
523     int currentIndex = svp->findOnSwitchIndex();
524     menuC->setCurrentIndex(currentIndex);
525 }
526 
processSetButton()527 void INDI_P::processSetButton()
528 {
529     switch (dataProp.getType())
530     {
531         case INDI_TEXT:
532             //if (!strcmp(dataProp.getName(), "TIME_UTC"))
533             if (dataProp.isNameMatch("TIME_UTC"))
534                 newTime();
535             else
536                 sendText();
537 
538             break;
539 
540         case INDI_NUMBER:
541             sendText();
542             break;
543 
544         case INDI_BLOB:
545             sendBlob();
546             break;
547 
548         default:
549             break;
550     }
551 }
552 
sendBlob()553 void INDI_P::sendBlob()
554 {
555     //int index=0;
556     //bool openingTag=false;
557     auto bvp = dataProp.getBLOB();
558 
559     if (!bvp)
560         return;
561 
562     bvp->setState(IPS_BUSY);
563 
564     pg->getDevice()->getClientManager()->startBlob(bvp->getDeviceName(), bvp->getName(), timestamp());
565 
566     for (int i = 0; i < elementList.count(); i++)
567     {
568         INDI::WidgetView<IBLOB> *bp = bvp->at(i);
569 #if (INDI_VERSION_MINOR >= 4 && INDI_VERSION_RELEASE >= 2)
570         pg->getDevice()->getClientManager()->sendOneBlob(bp);
571 #else
572         pg->getDevice()->getClientManager()->sendOneBlob(bp->getName(), bp->getSize(), bp->getFormat(), const_cast<void *>(bp->getBlob()));
573 #endif
574     }
575 
576     // JM: Why we need dirty here? We should be able to upload multiple time
577     /*foreach(INDI_E *ep, elementList)
578     {
579         if (ep->getBLOBDirty() == true)
580         {
581 
582             if (openingTag == false)
583             {
584                 pg->getDevice()->getClientManager()->startBlob(bvp->device, bvp->name, timestamp());
585                 openingTag = true;
586             }
587 
588             IBLOB *bp = &(bvp->bp[index]);
589             ep->setBLOBDirty(false);
590 
591             //qDebug() << "SENDING BLOB " << bp->name << " has size of " << bp->size << " and bloblen of " << bp->bloblen << endl;
592             pg->getDevice()->getClientManager()->sendOneBlob(bp->name, bp->size, bp->format, bp->blob);
593 
594         }
595 
596         index++;
597 
598     }*/
599 
600     //if (openingTag)
601     pg->getDevice()->getClientManager()->finishBlob();
602 
603     updateStateLED();
604 }
605 
newTime()606 void INDI_P::newTime()
607 {
608     INDI_E *timeEle   = getElement("UTC");
609     INDI_E *offsetEle = getElement("OFFSET");
610     if (!timeEle || !offsetEle)
611         return;
612 
613     TimeDialog timedialog(KStars::Instance()->data()->ut(), KStars::Instance()->data()->geo(), KStars::Instance(),
614                           true);
615 
616     if (timedialog.exec() == QDialog::Accepted)
617     {
618         QTime newTime(timedialog.selectedTime());
619         QDate newDate(timedialog.selectedDate());
620 
621         timeEle->setText(QString("%1-%2-%3T%4:%5:%6")
622                          .arg(newDate.year())
623                          .arg(newDate.month())
624                          .arg(newDate.day())
625                          .arg(newTime.hour())
626                          .arg(newTime.minute())
627                          .arg(newTime.second()));
628 
629         offsetEle->setText(QString().setNum(KStars::Instance()->data()->geo()->TZ(), 'g', 2));
630 
631         sendText();
632     }
633     else
634         return;
635 }
636 
getElement(const QString & elementName) const637 INDI_E *INDI_P::getElement(const QString &elementName) const
638 {
639     for (auto *ep : elementList)
640     {
641         if (ep->getName() == elementName)
642             return ep;
643     }
644 
645     return nullptr;
646 }
647 
isRegistered() const648 bool INDI_P::isRegistered() const
649 {
650     return (dataProp && dataProp.getRegistered());
651 }
652