Kstars

profileeditor.cpp
1 /*
2  SPDX-FileCopyrightText: 2016 Jasem Mutlaq <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "profileeditor.h"
8 
9 #include "geolocation.h"
10 #include "kstarsdata.h"
11 #include "ksnotification.h"
12 #include "Options.h"
13 #include "guide/guide.h"
14 #include "indi/driverinfo.h"
15 #include "indi/drivermanager.h"
16 #include "oal/equipmentwriter.h"
17 #include "profilescriptdialog.h"
18 #include "ui_indihub.h"
19 
20 #include "ekos_debug.h"
21 
22 #include <QNetworkInterface>
23 
24 ProfileEditorUI::ProfileEditorUI(QWidget *p) : QFrame(p)
25 {
26  setupUi(this);
27 }
28 
29 ProfileEditor::ProfileEditor(QWidget *w) : QDialog(w)
30 {
31  setObjectName("profileEditorDialog");
32 #ifdef Q_OS_OSX
34 #endif
35  ui = new ProfileEditorUI(this);
36 
37  pi = nullptr;
38 
39  m_MountModel = new QStandardItemModel(this);
40  m_CameraModel = new QStandardItemModel(this);
41  m_GuiderModel = new QStandardItemModel(this);
42  m_FocuserModel = new QStandardItemModel(this);
43 
44  QVBoxLayout *mainLayout = new QVBoxLayout;
45  mainLayout->addWidget(ui);
46  setLayout(mainLayout);
47 
48  setWindowTitle(i18nc("@title:window", "Profile Editor"));
49 
50  // Create button box and link it to save and reject functions
52 
53  buttonBox->setObjectName("dialogButtons");
54  mainLayout->addWidget(buttonBox);
55  connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
56  connect(buttonBox, SIGNAL(accepted()), this, SLOT(saveProfile()));
57 
58  connect(ui->scriptsB, &QPushButton::clicked, this, &ProfileEditor::executeScriptEditor);
59 
60  connect(ui->openWebManagerB, &QPushButton::clicked, this, [this]()
61  {
62  QUrl url(QString("http://" + ui->remoteHost->text() + ":8624"));
63  QDesktopServices::openUrl(url);
64  });
65 
66  connect(ui->INDIWebManagerCheck, &QCheckBox::toggled, this, [&](bool enabled)
67  {
68  ui->openWebManagerB->setEnabled(enabled);
69  ui->remoteDrivers->setEnabled(enabled || ui->localMode->isChecked());
70  ui->scriptsB->setEnabled(enabled || ui->localMode->isChecked());
71  });
72 
73  connect(ui->guideTypeCombo, SIGNAL(activated(int)), this, SLOT(updateGuiderSelection(int)));
74 
75  connect(ui->addScopeB, &QPushButton::clicked, this, [this]()
76  {
77  QPointer<EquipmentWriter> equipmentdlg = new EquipmentWriter();
78  equipmentdlg->loadEquipment();
79  equipmentdlg->exec();
80  delete equipmentdlg;
81  loadScopeEquipment();
82  });
83 
84  connect(ui->scanB, &QPushButton::clicked, this, &ProfileEditor::scanNetwork);
85 
86 #ifdef Q_OS_WIN
87  ui->remoteMode->setChecked(true);
88  ui->localMode->setEnabled(false);
89  setRemoteMode(true);
90 #else
91  connect(ui->remoteMode, SIGNAL(toggled(bool)), this, SLOT(setRemoteMode(bool)));
92 #endif
93 
94  connect(ui->indihubB, &QPushButton::clicked, this, &ProfileEditor::showINDIHub);
95 
96  // Load all drivers
97  loadDrivers();
98  // Load scope equipment
99  loadScopeEquipment();
100 
101  // Shared tooltips
102  ui->remoteDrivers->setToolTip(ui->remoteDriversLabel->toolTip());
103  ui->aux1Combo->setToolTip(ui->aux1Label->toolTip());
104  ui->aux2Combo->setToolTip(ui->aux2Label->toolTip());
105  ui->aux3Combo->setToolTip(ui->aux3Label->toolTip());
106  ui->aux4Combo->setToolTip(ui->aux4Label->toolTip());
107  ui->filterCombo->setToolTip(ui->filterLabel->toolTip());
108  ui->AOCombo->setToolTip(ui->AOLabel->toolTip());
109  ui->domeCombo->setToolTip(ui->domeLabel->toolTip());
110  ui->weatherCombo->setToolTip(ui->weatherLabel->toolTip());
111  ui->localMode->setToolTip(ui->modeLabel->toolTip());
112  ui->remoteMode->setToolTip(ui->modeLabel->toolTip());
113  ui->remoteHostLabel->setToolTip(ui->remoteHost->toolTip());
114  ui->remotePortLabel->setToolTip(ui->remotePort->toolTip());
115  ui->externalGuidePortLabel->setToolTip(ui->externalGuidePort->toolTip());
116  ui->INDIWebManagerPortLabel->setToolTip(ui->INDIWebManagerPort->toolTip());
117  ui->guideTypeCombo->setToolTip(ui->guidingTypeLabel->toolTip());
118  ui->externalGuideHostLabel->setToolTip(ui->externalGuideHost->toolTip());
119 }
120 
121 void ProfileEditor::loadScopeEquipment()
122 {
123  // Get all OAL equipment filter list
124  KStarsData::Instance()->userdb()->GetAllScopes(m_scopeList);
125 
126  ui->primaryScopeCombo->clear();
127  ui->guideScopeCombo->clear();
128 
129  ui->primaryScopeCombo->addItem(i18n("Default"));
130  ui->primaryScopeCombo->setItemData(0, i18n("Use scope data from INDI"), Qt::ToolTipRole);
131  ui->guideScopeCombo->addItem(i18n("Default"));
132  ui->guideScopeCombo->setItemData(0, i18n("Use scope data from INDI"), Qt::ToolTipRole);
133 
134  int primaryScopeIndex = 0;
135  int guideScopeIndex = 0;
136 
137  for (int i = 0; i < m_scopeList.count(); i++)
138  {
139  OAL::Scope *oneScope = m_scopeList[i];
140 
141  ui->primaryScopeCombo->addItem(oneScope->name());
142  if (pi && oneScope->id().toInt() == pi->primaryscope)
143  primaryScopeIndex = i + 1;
144 
145  ui->guideScopeCombo->addItem(oneScope->name());
146  if (pi && oneScope->id().toInt() == pi->guidescope)
147  guideScopeIndex = i + 1;
148 
149  double FocalLength = oneScope->focalLength();
150  double Aperture = oneScope->aperture();
151 
152  ui->primaryScopeCombo->setItemData(i + 1,
153  i18nc("F-Number, Focal length, Aperture",
154  "<nobr>F<b>%1</b> Focal length: <b>%2</b> mm Aperture: <b>%3</b> mm<sup>2</sup></nobr>",
155  QString::number(FocalLength / Aperture, 'f', 1), QString::number(FocalLength, 'f', 2),
156  QString::number(Aperture, 'f', 2)),
158 
159  ui->guideScopeCombo->setItemData(i + 1,
160  i18nc("F-Number, Focal length, Aperture",
161  "<nobr>F<b>%1</b> Focal length: <b>%2</b> mm Aperture: <b>%3</b> mm<sup>2</sup></nobr>",
162  QString::number(FocalLength / Aperture, 'f', 1), QString::number(FocalLength, 'f', 2),
163  QString::number(Aperture, 'f', 2)),
165  }
166 
167  ui->primaryScopeCombo->setCurrentIndex(primaryScopeIndex);
168  ui->guideScopeCombo->setCurrentIndex(guideScopeIndex);
169 }
170 
171 void ProfileEditor::saveProfile()
172 {
173  bool newProfile = (pi == nullptr);
174 
175  if (ui->profileIN->text().isEmpty())
176  {
177  KSNotification::error(i18n("Cannot save an empty profile."));
178  return;
179  }
180 
181  if (newProfile)
182  {
183  QList<std::shared_ptr<ProfileInfo>> existingProfiles;
184  KStarsData::Instance()->userdb()->GetAllProfiles(existingProfiles);
185  for (auto &profileInfo : existingProfiles)
186  {
187  if (ui->profileIN->text() == profileInfo->name)
188  {
189  KSNotification::error(i18n("Profile name already exists."));
190  return;
191  }
192  }
193  int id = KStarsData::Instance()->userdb()->AddProfile(ui->profileIN->text());
194  pi = new ProfileInfo(id, ui->profileIN->text());
195  }
196  else
197  pi->name = ui->profileIN->text();
198 
199  // Local Mode
200  if (ui->localMode->isChecked())
201  {
202  pi->host.clear();
203  pi->port = -1;
204  pi->INDIWebManagerPort = -1;
205  //pi->customDrivers = ui->customDriversIN->text();
206  }
207  // Remote Mode
208  else
209  {
210  pi->host = ui->remoteHost->text().trimmed();
211  pi->port = ui->remotePort->text().toInt();
212  if (ui->INDIWebManagerCheck->isChecked())
213  pi->INDIWebManagerPort = ui->INDIWebManagerPort->text().toInt();
214  else
215  pi->INDIWebManagerPort = -1;
216  //pi->customDrivers.clear();
217  }
218 
219  // City Info
220  if (ui->loadSiteCheck->isEnabled() && ui->loadSiteCheck->isChecked())
221  {
222  pi->city = KStarsData::Instance()->geo()->name();
223  pi->province = KStarsData::Instance()->geo()->province();
224  pi->country = KStarsData::Instance()->geo()->country();
225  }
226  else
227  {
228  pi->city.clear();
229  pi->province.clear();
230  pi->country.clear();
231  }
232 
233  pi->indihub = m_INDIHub;
234 
235  // Auto Connect
236  pi->autoConnect = ui->autoConnectCheck->isChecked();
237  // Port Selector
238  pi->portSelector = ui->portSelectorCheck->isChecked();
239 
240  // Guider Type
241  pi->guidertype = ui->guideTypeCombo->currentIndex();
242  if (pi->guidertype != Ekos::Guide::GUIDE_INTERNAL)
243  {
244  pi->guiderhost = ui->externalGuideHost->text();
245  pi->guiderport = ui->externalGuidePort->text().toInt();
246 
247  if (pi->guidertype == Ekos::Guide::GUIDE_PHD2)
248  {
249  Options::setPHD2Host(pi->guiderhost);
250  Options::setPHD2Port(pi->guiderport);
251  }
252  else if (pi->guidertype == Ekos::Guide::GUIDE_LINGUIDER)
253  {
254  Options::setLinGuiderHost(pi->guiderhost);
255  Options::setLinGuiderPort(pi->guiderport);
256  }
257  }
258 
259  // Scope list
260  pi->primaryscope = 0;
261  pi->guidescope = 0;
262 
263  QString selectedScope = ui->primaryScopeCombo->currentText();
264  QString selectedGuide = ui->guideScopeCombo->currentText();
265 
266  foreach(OAL::Scope *oneScope, m_scopeList)
267  {
268  if (selectedScope == oneScope->name())
269  pi->primaryscope = oneScope->id().toInt();
270  if (selectedGuide == oneScope->name())
271  pi->guidescope = oneScope->id().toInt();
272  }
273 
274  if (ui->mountCombo->currentText().isEmpty() || ui->mountCombo->currentText() == "--")
275  pi->drivers.remove("Mount");
276  else
277  pi->drivers["Mount"] = ui->mountCombo->currentText();
278 
279  if (ui->ccdCombo->currentText().isEmpty() || ui->ccdCombo->currentText() == "--")
280  pi->drivers.remove("CCD");
281  else
282  pi->drivers["CCD"] = ui->ccdCombo->currentText();
283 
284  if (ui->guiderCombo->currentText().isEmpty() || ui->guiderCombo->currentText() == "--")
285  pi->drivers.remove("Guider");
286  else
287  pi->drivers["Guider"] = ui->guiderCombo->currentText();
288 
289  if (ui->focuserCombo->currentText().isEmpty() || ui->focuserCombo->currentText() == "--")
290  pi->drivers.remove("Focuser");
291  else
292  pi->drivers["Focuser"] = ui->focuserCombo->currentText();
293 
294  if (ui->filterCombo->currentText().isEmpty() || ui->filterCombo->currentText() == "--")
295  pi->drivers.remove("Filter");
296  else
297  pi->drivers["Filter"] = ui->filterCombo->currentText();
298 
299  if (ui->AOCombo->currentText().isEmpty() || ui->AOCombo->currentText() == "--")
300  pi->drivers.remove("AO");
301  else
302  pi->drivers["AO"] = ui->AOCombo->currentText();
303 
304  if (ui->domeCombo->currentText().isEmpty() || ui->domeCombo->currentText() == "--")
305  pi->drivers.remove("Dome");
306  else
307  pi->drivers["Dome"] = ui->domeCombo->currentText();
308 
309  if (ui->weatherCombo->currentText().isEmpty() || ui->weatherCombo->currentText() == "--")
310  pi->drivers.remove("Weather");
311  else
312  pi->drivers["Weather"] = ui->weatherCombo->currentText();
313 
314  if (ui->aux1Combo->currentText().isEmpty() || ui->aux1Combo->currentText() == "--")
315  pi->drivers.remove("Aux1");
316  else
317  pi->drivers["Aux1"] = ui->aux1Combo->currentText();
318 
319  if (ui->aux2Combo->currentText().isEmpty() || ui->aux2Combo->currentText() == "--")
320  pi->drivers.remove("Aux2");
321  else
322  pi->drivers["Aux2"] = ui->aux2Combo->currentText();
323 
324  if (ui->aux3Combo->currentText().isEmpty() || ui->aux3Combo->currentText() == "--")
325  pi->drivers.remove("Aux3");
326  else
327  pi->drivers["Aux3"] = ui->aux3Combo->currentText();
328 
329  if (ui->aux4Combo->currentText().isEmpty() || ui->aux4Combo->currentText() == "--")
330  pi->drivers.remove("Aux4");
331  else
332  pi->drivers["Aux4"] = ui->aux4Combo->currentText();
333 
334  pi->remotedrivers = ui->remoteDrivers->text();
335 
336  KStarsData::Instance()->userdb()->SaveProfile(pi);
337 
338  // Ekos manager will reload and new profiles will be created
339  if (newProfile)
340  delete (pi);
341 
342  accept();
343 }
344 
345 void ProfileEditor::setRemoteMode(bool enable)
346 {
347  loadDrivers(); //This is needed to reload the drivers because some may not be available locally
348 
349  ui->remoteHost->setEnabled(enable);
350  ui->remoteHostLabel->setEnabled(enable);
351  ui->remotePort->setEnabled(enable);
352  ui->remotePortLabel->setEnabled(enable);
353 
354  //ui->customLabel->setEnabled(!enable);
355  //ui->customDriversIN->setEnabled(!enable);
356 
357  ui->mountCombo->setEditable(enable);
358  ui->ccdCombo->setEditable(enable);
359  ui->guiderCombo->setEditable(enable);
360  ui->focuserCombo->setEditable(enable);
361  ui->filterCombo->setEditable(enable);
362  ui->AOCombo->setEditable(enable);
363  ui->domeCombo->setEditable(enable);
364  ui->weatherCombo->setEditable(enable);
365  ui->aux1Combo->setEditable(enable);
366  ui->aux2Combo->setEditable(enable);
367  ui->aux3Combo->setEditable(enable);
368  ui->aux4Combo->setEditable(enable);
369 
370  ui->remoteDrivers->setEnabled(!enable);
371 
372  ui->loadSiteCheck->setEnabled(enable);
373 
374  ui->INDIWebManagerCheck->setEnabled(enable);
375  if (enable == false)
376  ui->INDIWebManagerCheck->setChecked(false);
377  ui->INDIWebManagerPort->setEnabled(enable);
378 
379  ui->scriptsB->setEnabled(!enable || ui->INDIWebManagerCheck->isChecked());
380 }
381 
382 void ProfileEditor::setPi(ProfileInfo *newProfile)
383 {
384  pi = newProfile;
385 
386  ui->profileIN->setText(pi->name);
387 
388  ui->loadSiteCheck->setChecked(!pi->city.isEmpty());
389  ui->autoConnectCheck->setChecked(pi->autoConnect);
390  ui->portSelectorCheck->setChecked(pi->portSelector);
391 
392  if (pi->city.isEmpty() == false)
393  {
394  if (pi->province.isEmpty())
395  ui->loadSiteCheck->setText(ui->loadSiteCheck->text() + QString(" (%1, %2)").arg(pi->country, pi->city));
396  else
397  ui->loadSiteCheck->setText(ui->loadSiteCheck->text() +
398  QString(" (%1, %2, %3)").arg(pi->country, pi->province, pi->city));
399  }
400 
401  if (pi->host.isEmpty() == false)
402  {
403  ui->remoteHost->setText(pi->host);
404  ui->remotePort->setText(QString::number(pi->port));
405 
406  ui->remoteMode->setChecked(true);
407 
408  if (pi->INDIWebManagerPort > 0)
409  {
410  ui->INDIWebManagerCheck->setChecked(true);
411  ui->INDIWebManagerPort->setText(QString::number(pi->INDIWebManagerPort));
412  }
413  else
414  {
415  ui->INDIWebManagerCheck->setChecked(false);
416  ui->INDIWebManagerPort->setText("8624");
417  }
418  }
419 
420  if (pi->remotedrivers.isEmpty() == false)
421  ui->remoteDrivers->setText(pi->remotedrivers);
422 
423  ui->guideTypeCombo->setCurrentIndex(pi->guidertype);
424  updateGuiderSelection(ui->guideTypeCombo->currentIndex());
425  if (pi->guidertype == Ekos::Guide::GUIDE_PHD2)
426  {
427  Options::setPHD2Host(pi->guiderhost);
428  Options::setPHD2Port(pi->guiderport);
429  }
430  else if (pi->guidertype == Ekos::Guide::GUIDE_LINGUIDER)
431  {
432  Options::setLinGuiderHost(pi->guiderhost);
433  Options::setLinGuiderPort(pi->guiderport);
434  }
435 
436  QMapIterator<QString, QString> i(pi->drivers);
437 
438  while (i.hasNext())
439  {
440  int row = 0;
441  i.next();
442 
443  QString key = i.key();
444  QString value = i.value();
445 
446  if (key == "Mount")
447  {
448  // If driver doesn't exist, let's add it to the list
449  if ((row = ui->mountCombo->findText(value)) == -1)
450  {
451  ui->mountCombo->addItem(value);
452  row = ui->mountCombo->count() - 1;
453  }
454 
455  // Set index to our driver
456  ui->mountCombo->setCurrentIndex(row);
457  }
458  else if (key == "CCD")
459  {
460  if ((row = ui->ccdCombo->findText(value)) == -1)
461  {
462  ui->ccdCombo->addItem(value);
463  row = ui->ccdCombo->count() - 1;
464  }
465 
466  ui->ccdCombo->setCurrentIndex(row);
467  }
468  else if (key == "Guider")
469  {
470  if ((row = ui->guiderCombo->findText(value)) == -1)
471  {
472  ui->guiderCombo->addItem(value);
473  row = ui->guiderCombo->count() - 1;
474  }
475 
476  ui->guiderCombo->setCurrentIndex(row);
477  }
478  else if (key == "Focuser")
479  {
480  if ((row = ui->focuserCombo->findText(value)) == -1)
481  {
482  ui->focuserCombo->addItem(value);
483  row = ui->focuserCombo->count() - 1;
484  }
485 
486  ui->focuserCombo->setCurrentIndex(row);
487  }
488  else if (key == "Filter")
489  {
490  if ((row = ui->filterCombo->findText(value)) == -1)
491  {
492  ui->filterCombo->addItem(value);
493  row = ui->filterCombo->count() - 1;
494  }
495 
496  ui->filterCombo->setCurrentIndex(row);
497  }
498  else if (key == "AO")
499  {
500  if ((row = ui->AOCombo->findText(value)) == -1)
501  {
502  ui->AOCombo->addItem(value);
503  row = ui->AOCombo->count() - 1;
504  }
505 
506  ui->AOCombo->setCurrentIndex(row);
507  }
508  else if (key == "Dome")
509  {
510  if ((row = ui->domeCombo->findText(value)) == -1)
511  {
512  ui->domeCombo->addItem(value);
513  row = ui->domeCombo->count() - 1;
514  }
515 
516  ui->domeCombo->setCurrentIndex(row);
517  }
518  else if (key == "Weather")
519  {
520  if ((row = ui->weatherCombo->findText(value)) == -1)
521  {
522  ui->weatherCombo->addItem(value);
523  row = ui->weatherCombo->count() - 1;
524  }
525 
526  ui->weatherCombo->setCurrentIndex(row);
527  }
528  else if (key == "Aux1")
529  {
530  if ((row = ui->aux1Combo->findText(value)) == -1)
531  {
532  ui->aux1Combo->addItem(value);
533  row = ui->aux1Combo->count() - 1;
534  }
535 
536  ui->aux1Combo->setCurrentIndex(row);
537  }
538  else if (key == "Aux2")
539  {
540  if ((row = ui->aux2Combo->findText(value)) == -1)
541  {
542  ui->aux2Combo->addItem(value);
543  row = ui->aux2Combo->count() - 1;
544  }
545 
546  ui->aux2Combo->setCurrentIndex(row);
547  }
548  else if (key == "Aux3")
549  {
550  if ((row = ui->aux3Combo->findText(value)) == -1)
551  {
552  ui->aux3Combo->addItem(value);
553  row = ui->aux3Combo->count() - 1;
554  }
555 
556  ui->aux3Combo->setCurrentIndex(row);
557  }
558  else if (key == "Aux4")
559  {
560  if ((row = ui->aux4Combo->findText(value)) == -1)
561  {
562  ui->aux4Combo->addItem(value);
563  row = ui->aux4Combo->count() - 1;
564  }
565 
566  ui->aux4Combo->setCurrentIndex(row);
567  }
568  }
569 
570  m_INDIHub = pi->indihub;
571 
572  loadScopeEquipment();
573 }
574 
575 QString ProfileEditor::getTooltip(DriverInfo *dv)
576 {
577  bool locallyAvailable = false;
578  if (dv->getAuxInfo().contains("LOCALLY_AVAILABLE"))
579  locallyAvailable = dv->getAuxInfo().value("LOCALLY_AVAILABLE", false).toBool();
580  QString toolTipText;
581  if (!locallyAvailable)
582  toolTipText = i18n(
583  "<nobr>Available as <b>Remote</b> Driver. To use locally, install the corresponding driver.<nobr/>");
584  else
585  toolTipText = i18n("<nobr><b>Label</b>: %1 &#9473; <b>Driver</b>: %2 &#9473; <b>Exec</b>: %3<nobr/>",
586  dv->getLabel(), dv->getName(), dv->getExecutable());
587 
588  return toolTipText;
589 }
590 
591 void ProfileEditor::loadDrivers()
592 {
593  // We need to save this now since we have two models for the mounts
594  QString selectedMount = ui->mountCombo->currentText();
595  QString selectedCamera = ui->ccdCombo->currentText();
596  QString selectedGuider = ui->guiderCombo->currentText();
597  QString selectedFocuser = ui->focuserCombo->currentText();
598  QString selectedAux1 = ui->aux1Combo->currentText();
599  QString selectedAux2 = ui->aux2Combo->currentText();
600  QString selectedAux3 = ui->aux3Combo->currentText();
601  QString selectedAux4 = ui->aux4Combo->currentText();
602 
603  QVector<QComboBox *> boxes;
604  boxes.append(ui->mountCombo);
605  boxes.append(ui->ccdCombo);
606  boxes.append(ui->guiderCombo);
607  boxes.append(ui->AOCombo);
608  boxes.append(ui->focuserCombo);
609  boxes.append(ui->filterCombo);
610  boxes.append(ui->domeCombo);
611  boxes.append(ui->weatherCombo);
612  boxes.append(ui->aux1Combo);
613  boxes.append(ui->aux2Combo);
614  boxes.append(ui->aux3Combo);
615  boxes.append(ui->aux4Combo);
616 
617  QVector<QString> selectedItems;
618 
619  for (QComboBox *box : boxes)
620  {
621  selectedItems.append(box->currentText());
622  box->clear();
623  box->addItem("--");
624  box->setMaxVisibleItems(20);
625  }
626 
627  QIcon remoteIcon = QIcon::fromTheme("network-modem");
628 
629  // Create the model
630  delete (m_MountModel);
631  m_MountModel = new QStandardItemModel(this);
632  delete (m_CameraModel);
633  m_CameraModel = new QStandardItemModel(this);
634  delete (m_GuiderModel);
635  m_GuiderModel = new QStandardItemModel(this);
636  delete (m_FocuserModel);
637  m_FocuserModel = new QStandardItemModel(this);
638  delete (m_Aux1Model);
639  m_Aux1Model = new QStandardItemModel(this);
640  delete (m_Aux2Model);
641  m_Aux2Model = new QStandardItemModel(this);
642  delete (m_Aux3Model);
643  m_Aux3Model = new QStandardItemModel(this);
644  delete (m_Aux4Model);
645  m_Aux4Model = new QStandardItemModel(this);
646 
647  const bool isLocal = ui->localMode->isChecked();
648  const QList<DeviceFamily> auxFamily = QList<DeviceFamily>()
649  << KSTARS_AUXILIARY
650  << KSTARS_CCD
651  << KSTARS_FOCUSER
652  << KSTARS_FILTER
653  << KSTARS_WEATHER
654  << KSTARS_SPECTROGRAPHS
655  << KSTARS_DETECTORS;
656 
657  populateManufacturerCombo(m_MountModel, ui->mountCombo, selectedMount, isLocal, QList<DeviceFamily>() << KSTARS_TELESCOPE);
658  populateManufacturerCombo(m_CameraModel, ui->ccdCombo, selectedCamera, isLocal, QList<DeviceFamily>() << KSTARS_CCD);
659  populateManufacturerCombo(m_GuiderModel, ui->guiderCombo, selectedGuider, isLocal, QList<DeviceFamily>() << KSTARS_CCD);
660  populateManufacturerCombo(m_FocuserModel, ui->focuserCombo, selectedFocuser, isLocal,
661  QList<DeviceFamily>() << KSTARS_FOCUSER);
662  populateManufacturerCombo(m_Aux1Model, ui->aux1Combo, selectedAux1, isLocal, auxFamily);
663  populateManufacturerCombo(m_Aux2Model, ui->aux2Combo, selectedAux2, isLocal, auxFamily);
664  populateManufacturerCombo(m_Aux3Model, ui->aux3Combo, selectedAux3, isLocal, auxFamily);
665  populateManufacturerCombo(m_Aux4Model, ui->aux4Combo, selectedAux4, isLocal, auxFamily);
666 
667  for (DriverInfo *dv : DriverManager::Instance()->getDrivers())
668  {
669  bool locallyAvailable = false;
670  QIcon icon;
671  if (dv->getAuxInfo().contains("LOCALLY_AVAILABLE"))
672  locallyAvailable = dv->getAuxInfo().value("LOCALLY_AVAILABLE", false).toBool();
673  if (!locallyAvailable)
674  {
675  if (ui->localMode->isChecked())
676  continue;
677  else
678  icon = remoteIcon;
679  }
680 
681  QString toolTipText = getTooltip(dv);
682 
683  switch (dv->getType())
684  {
685  case KSTARS_CCD:
686  break;
687 
688  case KSTARS_ADAPTIVE_OPTICS:
689  {
690  ui->AOCombo->addItem(icon, dv->getLabel());
691  ui->AOCombo->setItemData(ui->AOCombo->count() - 1, toolTipText, Qt::ToolTipRole);
692  }
693  break;
694 
695  case KSTARS_FOCUSER:
696  break;
697 
698  case KSTARS_FILTER:
699  {
700  ui->filterCombo->addItem(icon, dv->getLabel());
701  ui->filterCombo->setItemData(ui->filterCombo->count() - 1, toolTipText, Qt::ToolTipRole);
702  }
703  break;
704 
705  case KSTARS_DOME:
706  {
707  ui->domeCombo->addItem(icon, dv->getLabel());
708  ui->domeCombo->setItemData(ui->domeCombo->count() - 1, toolTipText, Qt::ToolTipRole);
709  }
710  break;
711 
712  case KSTARS_WEATHER:
713  {
714  ui->weatherCombo->addItem(icon, dv->getLabel());
715  ui->weatherCombo->setItemData(ui->weatherCombo->count() - 1, toolTipText, Qt::ToolTipRole);
716  }
717  break;
718 
719  case KSTARS_AUXILIARY:
720  case KSTARS_SPECTROGRAPHS:
721  case KSTARS_DETECTORS:
722  break;
723 
724  default:
725  continue;
726  }
727  }
728 
729  // Skip mount/ccd/guider/focuser since we handled it above
730  for (int i = 4; i < boxes.count(); i++)
731  {
732  QComboBox *box = boxes.at(i);
733  QString selectedItemText = selectedItems.at(i);
734  int index = box->findText(selectedItemText);
735  if (index == -1)
736  {
737  if (ui->localMode->isChecked())
738  box->setCurrentIndex(0);
739  else
740  box->addItem(remoteIcon, selectedItemText);
741  }
742  else
743  {
744  box->setCurrentIndex(index);
745  }
746 
747  box->model()->sort(0);
748  }
749 }
750 
751 void ProfileEditor::setProfileName(const QString &name)
752 {
753  ui->profileIN->setText(name);
754 }
755 
756 void ProfileEditor::setAuxDrivers(const QStringList &aux)
757 {
758  QStringList auxList(aux);
759 
760  if (auxList.isEmpty())
761  return;
762  ui->aux1Combo->setCurrentText(auxList.first());
763  auxList.removeFirst();
764 
765  if (auxList.isEmpty())
766  return;
767  ui->aux2Combo->setCurrentText(auxList.first());
768  auxList.removeFirst();
769 
770  if (auxList.isEmpty())
771  return;
772  ui->aux3Combo->setCurrentText(auxList.first());
773  auxList.removeFirst();
774 
775  if (auxList.isEmpty())
776  return;
777  ui->aux4Combo->setCurrentText(auxList.first());
778 }
779 
780 void ProfileEditor::setHostPort(const QString &host, const QString &port)
781 {
782  ui->remoteMode->setChecked(true);
783  ui->remoteHost->setText(host);
784  ui->remotePort->setText(port);
785 }
786 
787 void ProfileEditor::setWebManager(bool enabled, const QString &port)
788 {
789  ui->INDIWebManagerCheck->setChecked(enabled);
790  ui->INDIWebManagerPort->setText(port);
791 }
792 
793 void ProfileEditor::setGuiderType(int type)
794 {
795  ui->guideTypeCombo->setCurrentIndex(type);
796  if (type != Ekos::Guide::GUIDE_INTERNAL)
797  {
798  ui->externalGuideHostLabel->setEnabled(true);
799  ui->externalGuideHost->setEnabled(true);
800  ui->externalGuidePortLabel->setEnabled(true);
801  ui->externalGuidePort->setEnabled(true);
802  }
803 }
804 
805 void ProfileEditor::setConnectionOptionsEnabled(bool enable)
806 {
807  // Enable or disable connection related options
808  ui->modeLabel->setEnabled(enable);
809  ui->localMode->setEnabled(enable);
810  ui->remoteMode->setEnabled(enable);
811  ui->remoteHostLabel->setEnabled(enable);
812  ui->remoteHost->setEnabled(enable);
813  ui->remotePortLabel->setEnabled(enable);
814  ui->remotePort->setEnabled(enable);
815  ui->INDIWebManagerCheck->setEnabled(enable);
816  ui->INDIWebManagerPort->setEnabled(enable);
817  ui->INDIWebManagerPortLabel->setEnabled(enable);
818  ui->guidingTypeLabel->setEnabled(enable);
819  ui->guideTypeCombo->setEnabled(enable);
820  ui->remoteDrivers->setEnabled(enable);
821 
822  updateGuiderSelection(ui->guideTypeCombo->currentIndex());
823 
824  if (enable == false)
825  ui->mountCombo->setFocus();
826 }
827 
828 void ProfileEditor::updateGuiderSelection(int id)
829 {
830 
831  if (id == Ekos::Guide::GUIDE_INTERNAL)
832  {
833  ui->externalGuideHost->setText("localhost");
834  ui->externalGuidePort->clear();
835 
836  ui->externalGuideHost->setEnabled(false);
837  ui->externalGuideHostLabel->setEnabled(false);
838  ui->externalGuidePort->setEnabled(false);
839  ui->externalGuidePortLabel->setEnabled(false);
840  return;
841  }
842 
843  QString host;
844  int port = -1;
845 
846  ui->externalGuideHost->setEnabled(true);
847  ui->externalGuideHostLabel->setEnabled(true);
848  ui->externalGuidePort->setEnabled(true);
849  ui->externalGuidePortLabel->setEnabled(true);
850 
851  if (pi && pi->guidertype == id)
852  {
853  host = pi->guiderhost;
854  port = pi->guiderport;
855  }
856 
857  if (id == Ekos::Guide::GUIDE_PHD2)
858  {
859  if (host.isEmpty())
860  host = Options::pHD2Host();
861  if (port < 0)
862  port = Options::pHD2Port();
863  }
864  else if (id == Ekos::Guide::GUIDE_LINGUIDER)
865  {
866  if (host.isEmpty())
867  host = Options::linGuiderHost();
868  if (port < 0)
869  port = Options::linGuiderPort();
870  }
871 
872  ui->externalGuideHost->setText(host);
873  ui->externalGuidePort->setText(QString::number(port));
874 
875 }
876 
877 void ProfileEditor::setSettings(const QJsonObject &profile)
878 {
879  ui->profileIN->setText(profile["name"].toString());
880  ui->autoConnectCheck->setChecked(profile["auto_connect"].toBool(true));
881  ui->portSelectorCheck->setChecked(profile["port_selector"].toBool(false));
882  ui->localMode->setChecked(profile["mode"].toString() == "local");
883  ui->remoteMode->setChecked(profile["mode"].toString() == "remote");
884  ui->remoteHost->setText(profile["remote_host"].toString("localhost"));
885  ui->remotePort->setText(profile["remote_port"].toString("7624"));
886  ui->guideTypeCombo->setCurrentText(profile["guiding"].toString(i18n("Internal")));
887  ui->externalGuideHost->setText(profile["remote_guiding_host"].toString(("localhost")));
888  ui->externalGuideHost->setText(profile["remote_guiding_port"].toString());
889  ui->INDIWebManagerCheck->setChecked(profile["use_web_manager"].toBool());
890 
891  m_INDIHub = profile["indihub"].toInt(m_INDIHub);
892 
893  int primaryID = profile["primary_scope"].toInt(-1);
894  int guideID = profile["guide_scope"].toInt(-1);
895 
896  if (primaryID <= 0)
897  ui->primaryScopeCombo->setCurrentIndex(0);
898  else
899  {
900  for (int i = 1; i < ui->primaryScopeCombo->count(); i++)
901  {
902  if (m_scopeList[i - 1]->id().toInt() == primaryID)
903  {
904  ui->primaryScopeCombo->setCurrentIndex(i);
905  break;
906  }
907  }
908  }
909 
910  if (guideID <= 0)
911  ui->guideScopeCombo->setCurrentIndex(0);
912  else
913  {
914  for (int i = 1; i < ui->guideScopeCombo->count(); i++)
915  {
916  if (m_scopeList[i - 1]->id().toInt() == guideID)
917  {
918  ui->guideScopeCombo->setCurrentIndex(i);
919  break;
920  }
921  }
922  }
923 
924  // Drivers
925  const QString mount = profile["mount"].toString("--");
926  if (mount == "--")
927  ui->mountCombo->setCurrentIndex(0);
928  else
929  {
930  ui->mountCombo->addItem(mount);
931  ui->mountCombo->setCurrentIndex(ui->mountCombo->count() - 1);
932  }
933 
934  const QString ccd = profile["ccd"].toString("--");
935  if (ccd == "--")
936  ui->ccdCombo->setCurrentIndex(0);
937  else
938  {
939  ui->ccdCombo->addItem(ccd);
940  ui->ccdCombo->setCurrentIndex(ui->ccdCombo->count() - 1);
941  }
942 
943  const QString guider = profile["guider"].toString("--");
944  if (guider == "--")
945  ui->guiderCombo->setCurrentIndex(0);
946  else
947  {
948  ui->guiderCombo->addItem(guider);
949  ui->guiderCombo->setCurrentIndex(ui->guiderCombo->count() - 1);
950  }
951 
952  const QString focuser = profile["focuser"].toString("--");
953  if (focuser == "--")
954  ui->focuserCombo->setCurrentIndex(0);
955  else
956  {
957  ui->focuserCombo->addItem(focuser);
958  ui->focuserCombo->setCurrentIndex(ui->focuserCombo->count() - 1);
959  }
960 
961  ui->filterCombo->setCurrentText(profile["filter"].toString("--"));
962  ui->AOCombo->setCurrentText(profile["ao"].toString("--"));
963  ui->domeCombo->setCurrentText(profile["dome"].toString("--"));
964  ui->weatherCombo->setCurrentText(profile["weather"].toString("--"));
965 
966  const auto aux1 = profile["aux1"].toString("--");
967  if (aux1.isEmpty() || aux1 == "--")
968  ui->aux1Combo->setCurrentIndex(0);
969  else
970  {
971  ui->aux1Combo->addItem(aux1);
972  ui->aux1Combo->setCurrentIndex(ui->aux1Combo->count() - 1);
973  }
974 
975  const auto aux2 = profile["aux2"].toString("--");
976  if (aux2.isEmpty() || aux2 == "--")
977  ui->aux2Combo->setCurrentIndex(0);
978  else
979  {
980  ui->aux2Combo->addItem(aux2);
981  ui->aux2Combo->setCurrentIndex(ui->aux2Combo->count() - 1);
982  }
983 
984  const auto aux3 = profile["aux3"].toString("--");
985  if (aux3.isEmpty() || aux3 == "--")
986  ui->aux3Combo->setCurrentIndex(0);
987  else
988  {
989  ui->aux3Combo->addItem(aux3);
990  ui->aux3Combo->setCurrentIndex(ui->aux3Combo->count() - 1);
991  }
992 
993  const auto aux4 = profile["aux4"].toString("--");
994  if (aux4.isEmpty() || aux4 == "--")
995  ui->aux4Combo->setCurrentIndex(0);
996  else
997  {
998  ui->aux4Combo->addItem(aux4);
999  ui->aux4Combo->setCurrentIndex(ui->aux4Combo->count() - 1);
1000  }
1001 }
1002 
1003 void ProfileEditor::scanNetwork()
1004 {
1005  delete (m_ProgressDialog);
1006  m_ProgressDialog = new QProgressDialog(this);
1007  m_ProgressDialog->setWindowTitle(i18nc("@title:window", "Scanning Network"));
1008  m_ProgressDialog->setLabelText(i18n("Scanning network for INDI Web Managers..."));
1009  connect(m_ProgressDialog, &QProgressDialog::canceled, this, [this]()
1010  {
1011  m_CancelScan = true;
1012  clearAllRequests();
1013  });
1014  m_ProgressDialog->setMinimum(0);
1015  m_ProgressDialog->setMaximum(0);
1016  m_ProgressDialog->show();
1017  m_ProgressDialog->raise();
1018 
1019  m_CancelScan = false;
1020 
1022  std::sort(addresses.begin(), addresses.end(), [](const QHostAddress & a, const QHostAddress & b) -> bool
1023  { return a.toString() < b.toString();});
1024 
1025  for(QHostAddress address : addresses)
1026  {
1027  if (address.isLoopback() || address.protocol() & QAbstractSocket::IPv6Protocol)
1028  continue;
1029 
1030  QString ipv4 = address.toString();
1031 
1032  if (ipv4.startsWith("10.250"))
1033  {
1034  scanIP("10.250.250.1");
1035  }
1036  else
1037  {
1038  QString prefixIP = ipv4.remove(ipv4.lastIndexOf("."), 10);
1039  // Blind search all over subnet
1040  // TODO better subnet detection instead of assuming it finishes at 254
1041  for (int i = 1; i <= 254; i++)
1042  {
1043  scanIP(prefixIP + "." + QString::number(i));
1044  }
1045  }
1046  }
1047 
1048 }
1049 
1050 void ProfileEditor::scanIP(const QString &ip)
1051 {
1052  QUrl url(QString("http://%1:8624/api/server/status").arg(ip));
1053 
1054  qCDebug(KSTARS_EKOS) << "Scanning" << url;
1055 
1056  QNetworkReply *response = m_Manager.get(QNetworkRequest(url));
1057  m_Replies.append(response);
1058  connect(response, &QNetworkReply::finished, [this, response, ip]()
1059  {
1060  m_Replies.removeOne(response);
1061  response->deleteLater();
1062  if (m_CancelScan)
1063  return;
1064  if (response->error() == QNetworkReply::NoError)
1065  {
1066  clearAllRequests();
1067  m_ProgressDialog->close();
1068  ui->remoteHost->setText(ip);
1069 
1070  qCDebug(KSTARS_EKOS) << "Found Web Manager server at" << ip;
1071 
1072  KSNotification::info(i18n("Found INDI Web Manager at %1", ip));
1073  }
1074  });
1075 }
1076 
1077 void ProfileEditor::clearAllRequests()
1078 {
1079  for (QNetworkReply *oneReply : m_Replies)
1080  {
1081  oneReply->abort();
1082  oneReply->deleteLater();
1083  }
1084 
1085  m_Replies.clear();
1086 }
1087 
1088 void ProfileEditor::showINDIHub()
1089 {
1090  QDialog hub;
1091  Ui::INDIHub indihub;
1092  indihub.setupUi(&hub);
1093 
1094  indihub.modeButtonGroup->setId(indihub.offR, 0);
1095  indihub.modeButtonGroup->setId(indihub.solorR, 1);
1096  indihub.modeButtonGroup->setId(indihub.shareR, 2);
1097  indihub.modeButtonGroup->setId(indihub.roboticR, 3);
1098 
1099  indihub.logoLabel->setPixmap(QIcon(":/icons/indihub_logo.svg").pixmap(QSize(128, 128)));
1100 
1101  indihub.modeButtonGroup->button(m_INDIHub)->setChecked(true);
1102  connect(indihub.closeB, &QPushButton::clicked, &hub, &QDialog::close);
1103 
1104  hub.exec();
1105 
1106  m_INDIHub = indihub.modeButtonGroup->checkedId();
1107 }
1108 
1109 void ProfileEditor::populateManufacturerCombo(QStandardItemModel *model, QComboBox *combo, const QString &selectedDriver,
1110  bool isLocal, const QList<DeviceFamily> &families)
1111 {
1112  if (isLocal)
1113  {
1114  QStandardItem *selectedItem = nullptr;
1115  model->appendRow(new QStandardItem("--"));
1116  for (DriverInfo *dv : DriverManager::Instance()->getDrivers())
1117  {
1118  if (!families.contains(dv->getType()))
1119  continue;
1120 
1121  QString manufacturer = dv->manufacturer();
1122  QList<QStandardItem*> manufacturers = model->findItems(manufacturer);
1123 
1124  QStandardItem *parentItem = nullptr;
1125  if (model->findItems(manufacturer).empty())
1126  {
1127  parentItem = new QStandardItem(manufacturer);
1128  parentItem->setSelectable(false);
1129  model->appendRow(parentItem);
1130  }
1131  else
1132  {
1133  parentItem = manufacturers.first();
1134  }
1135 
1136  QStandardItem *item = new QStandardItem(dv->getLabel());
1137  item->setData(getTooltip(dv), Qt::ToolTipRole);
1138  parentItem->appendRow(item);
1139  if (selectedDriver == dv->getLabel())
1140  selectedItem = item;
1141  }
1142  QTreeView *view = new QTreeView(this);
1143  view->setModel(model);
1144  view->sortByColumn(0, Qt::AscendingOrder);
1145  combo->setView(view);
1146  combo->setModel(model);
1147  if (selectedItem)
1148  {
1149  // JM: Only way to make it the QTreeView sets the current index
1150  // in the combo way
1151 
1152  QModelIndex index = model->indexFromItem(selectedItem);
1153 
1154  // First set current index to the child
1155  combo->setRootModelIndex(index.parent());
1156  combo->setModelColumn(index.column());
1157  combo->setCurrentIndex(index.row());
1158 
1159  // Now reset
1160  combo->setRootModelIndex(QModelIndex());
1161  view->setCurrentIndex(index);
1162  }
1163  }
1164  else
1165  {
1166  QIcon remoteIcon = QIcon::fromTheme("network-modem");
1167  combo->setView(new QListView(this));
1168  model->appendRow(new QStandardItem("--"));
1169  QIcon icon;
1170  for (DriverInfo *dv : DriverManager::Instance()->getDrivers())
1171  {
1172  if (!families.contains(dv->getType()))
1173  continue;
1174 
1175  bool locallyAvailable = false;
1176  if (dv->getAuxInfo().contains("LOCALLY_AVAILABLE"))
1177  locallyAvailable = dv->getAuxInfo().value("LOCALLY_AVAILABLE", false).toBool();
1178  icon = locallyAvailable ? QIcon() : remoteIcon;
1179 
1180  QStandardItem *mount = new QStandardItem(icon, dv->getLabel());
1181  mount->setData(getTooltip(dv), Qt::ToolTipRole);
1182  model->appendRow(mount);
1183  }
1184  combo->setModel(model);
1185  combo->setCurrentText(selectedDriver);
1186  }
1187 }
1188 
1189 void ProfileEditor::executeScriptEditor()
1190 {
1191  if (pi == nullptr)
1192  return;
1193  QStringList currentDrivers;
1194  for (auto &oneCombo : ui->driversGroupBox->findChildren<QComboBox *>())
1195  currentDrivers << oneCombo->currentText();
1196  currentDrivers.removeAll("--");
1197  currentDrivers.removeAll("");
1198  currentDrivers.sort();
1199  ProfileScriptDialog dialog(currentDrivers, pi->scripts, this);
1200  dialog.exec();
1201  auto settings = dialog.jsonSettings();
1202  pi->scripts = QJsonDocument(settings).toJson(QJsonDocument::Compact);
1203 }
T & first()
void setCurrentText(const QString &text)
ToolTipRole
QModelIndex indexFromItem(const QStandardItem *item) const const
QString number(int n, int base)
void GetAllProfiles(QList< std::shared_ptr< ProfileInfo >> &profiles)
GetAllProfiles Return all profiles in a QList.
Definition: ksuserdb.cpp:2010
virtual void reject()
virtual void setModel(QAbstractItemModel *model) override
int removeAll(const T &value)
int column() const const
QNetworkReply::NetworkError error() const const
void clear()
void append(const T &value)
void clicked(bool checked)
AscendingOrder
QIcon fromTheme(const QString &name)
void sortByColumn(int column)
void setCurrentIndex(int index)
KSUserDB * userdb()
Definition: kstarsdata.h:214
QAbstractItemModel * model() const const
bool close()
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool contains(const T &value) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void toggled(bool checked)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
QString country() const
Definition: geolocation.h:124
QList< QStandardItem * > findItems(const QString &text, Qt::MatchFlags flags, int column) const const
void setRootModelIndex(const QModelIndex &index)
KIOCORE_EXPORT SimpleJob * mount(bool ro, const QByteArray &fstype, const QString &dev, const QString &point, JobFlags flags=DefaultFlags)
void deleteLater()
void rejected()
QString i18n(const char *text, const TYPE &arg...)
void setWindowFlags(Qt::WindowFlags type)
void setModel(QAbstractItemModel *model)
char * toString(const T &value)
const T & at(int i) const const
GeoLocation * geo()
Definition: kstarsdata.h:229
void setWindowTitle(const QString &)
void setView(QAbstractItemView *itemView)
int toInt(bool *ok, int base) const const
void setModelColumn(int visibleColumn)
PostalAddress address(const QVariant &location)
virtual int exec()
void setupUi(QWidget *widget)
QString name() const
Definition: geolocation.h:106
virtual void sort(int column, Qt::SortOrder order)
void GetAllScopes(QList< OAL::Scope * > &m_scopeList)
updates the scope list with all scopes from database List is cleared and then filled with content.
Definition: ksuserdb.cpp:1076
QString & remove(int position, int n)
QString province() const
Definition: geolocation.h:115
int row() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
ScriptableExtension * host() const
void setCurrentIndex(const QModelIndex &index)
int findText(const QString &text, Qt::MatchFlags flags) const const
int count() const const
void setObjectName(const QString &name)
QList< QHostAddress > allAddresses()
void appendRow(const QList< QStandardItem * > &items)
QByteArray toJson() const const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QList::iterator begin()
QModelIndex parent() const const
void addItem(const QString &text, const QVariant &userData)
virtual void setData(const QVariant &value, int role)
void setSelectable(bool selectable)
void setLayout(QLayout *layout)
QList::iterator end()
void appendRow(const QList< QStandardItem * > &items)
virtual void abort()=0
void sort(Qt::CaseSensitivity cs)
void accepted()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sun Aug 14 2022 04:13:59 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.