File indexing completed on 2024-05-05 05:50:51
0001 /* Atelier KDE Printer Host for 3D Printing 0002 Copyright (C) <2016> 0003 Author: Lays Rodrigues - lays.rodrigues@kde.org 0004 Chris Rizzitello - rizzitello@kde.org 0005 0006 This program is free software: you can redistribute it and/or modify 0007 it under the terms of the GNU General Public License as published by 0008 the Free Software Foundation, either version 3 of the License, or 0009 (at your option) any later version. 0010 0011 This program is distributed in the hope that it will be useful, 0012 but WITHOUT ANY WARRANTY; without even the implied warranty of 0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0014 GNU General Public License for more details. 0015 0016 You should have received a copy of the GNU General Public License 0017 along with this program. If not, see <http://www.gnu.org/licenses/>. 0018 */ 0019 #include "mainwindow.h" 0020 #include "dialogs/choosefiledialog.h" 0021 #include "dialogs/profilesdialog.h" 0022 #include "widgets/3dview/viewer3d.h" 0023 #include "widgets/atcoreinstancewidget.h" 0024 #include "widgets/videomonitorwidget.h" 0025 #include "widgets/welcomewidget.h" 0026 #include <KActionCollection> 0027 #include <KLocalizedString> 0028 #include <KStandardAction> 0029 #include <KXMLGUIFactory> 0030 #include <MachineInfo> 0031 #include <QFileDialog> 0032 #include <QHBoxLayout> 0033 #include <QSplitter> 0034 #include <QToolButton> 0035 #include <memory> 0036 0037 MainWindow::MainWindow(QWidget *parent) 0038 : KXmlGuiWindow(parent) 0039 , m_currInstance(0) 0040 , m_theme(getTheme()) 0041 , m_instances(new QTabWidget(this)) 0042 { 0043 initWidgets(); 0044 setupActions(); 0045 setAcceptDrops(true); 0046 0047 connect(m_instances, &QTabWidget::tabCloseRequested, this, [this](int index) { 0048 auto tempWidget = qobject_cast<AtCoreInstanceWidget *>(m_instances->widget(index)); 0049 if (tempWidget->isPrinting() && !askToClose()) { 0050 return; 0051 } 0052 0053 tempWidget->disconnect(); 0054 tempWidget->close(); 0055 m_instances->removeTab(index); 0056 0057 if (m_instances->count() == 1) { 0058 m_instances->setTabsClosable(false); 0059 m_instances->setMovable(false); 0060 } 0061 }); 0062 } 0063 0064 void MainWindow::closeEvent(QCloseEvent *event) 0065 { 0066 if (!askToSave(m_gcodeEditor->modifiedFiles())) { 0067 event->ignore(); 0068 } 0069 0070 bool closePrompt = false; 0071 for (int i = 0; i < m_instances->count(); i++) { 0072 auto instance = qobject_cast<AtCoreInstanceWidget *>(m_instances->widget(i)); 0073 if (instance->isPrinting()) { 0074 closePrompt = true; 0075 break; 0076 } 0077 } 0078 if (closePrompt) { 0079 if (askToClose()) { 0080 event->accept(); 0081 } else { 0082 event->ignore(); 0083 } 0084 } 0085 } 0086 0087 void MainWindow::dragEnterEvent(QDragEnterEvent *event) 0088 { 0089 event->accept(); 0090 } 0091 0092 void MainWindow::dropEvent(QDropEvent *event) 0093 { 0094 const QMimeData *mimeData = event->mimeData(); 0095 if (mimeData->hasUrls()) { 0096 processDropEvent(mimeData->urls()); 0097 } 0098 } 0099 0100 void MainWindow::processDropEvent(const QList<QUrl> &fileList) 0101 { 0102 for (const auto &url : fileList) { 0103 // Loop thru the urls and only load ones ending our "supported" formats 0104 QString ext = url.toLocalFile().split('.').last(); 0105 if (ext.contains("gcode", Qt::CaseInsensitive) || ext.contains("gco", Qt::CaseInsensitive)) { 0106 loadFile(url); 0107 } 0108 } 0109 } 0110 0111 void MainWindow::initWidgets() 0112 { 0113 setupLateralArea(); 0114 newAtCoreInstance(); 0115 // View: 0116 // Sidebar, Sidebar Controls, Printer Tabs. 0117 // Sidebar Controls and Printer Tabs can be resized, Sidebar can't. 0118 auto splitter = new QSplitter(this); 0119 splitter->addWidget(m_lateral.m_stack); 0120 splitter->addWidget(m_instances); 0121 0122 auto addTabBtn = new QToolButton(this); 0123 addTabBtn->setIconSize(QSize(fontMetrics().lineSpacing(), fontMetrics().lineSpacing())); 0124 addTabBtn->setIcon(QIcon::fromTheme("list-add", QIcon(QString(":/%1/addTab").arg(m_theme)))); 0125 addTabBtn->setToolTip(i18n("Create new instance")); 0126 addTabBtn->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_T)); 0127 connect(addTabBtn, &QToolButton::clicked, this, &MainWindow::newAtCoreInstance); 0128 m_instances->setCornerWidget(addTabBtn, Qt::TopLeftCorner); 0129 0130 auto *centralLayout = new QHBoxLayout(); 0131 centralLayout->addWidget(m_lateral.m_toolBar); 0132 centralLayout->addWidget(splitter); 0133 0134 auto *centralWidget = new QWidget(this); 0135 centralWidget->setLayout(centralLayout); 0136 setCentralWidget(centralWidget); 0137 } 0138 0139 void MainWindow::newAtCoreInstance() 0140 { 0141 auto newInstanceWidget = new AtCoreInstanceWidget(this); 0142 QString name = QString::number(m_instances->addTab(newInstanceWidget, i18n("Connect a printer"))); 0143 newInstanceWidget->setObjectName(name); 0144 newInstanceWidget->setFileCount(m_openFiles.size()); 0145 connect(MachineInfo::instance(), &MachineInfo::profilesChanged, newInstanceWidget, &AtCoreInstanceWidget::updateProfileData); 0146 connect(newInstanceWidget, &AtCoreInstanceWidget::requestProfileDialog, this, [this] { 0147 std::unique_ptr<ProfilesDialog> pd(new ProfilesDialog(this)); 0148 pd->exec(); 0149 emit(profilesChanged()); 0150 }); 0151 0152 connect(newInstanceWidget, &AtCoreInstanceWidget::requestFileChooser, this, [newInstanceWidget, this] { 0153 QUrl file; 0154 switch (m_openFiles.size()) { 0155 case 0: 0156 QMessageBox::warning(this, i18n("Error"), i18n("There's no GCode file open.\nPlease select a file and try again."), QMessageBox::Ok); 0157 break; 0158 case 1: 0159 file = m_openFiles.at(0); 0160 break; 0161 default: 0162 ChooseFileDialog dialog(this, m_openFiles); 0163 if (dialog.exec() != QDialog::Accepted) { 0164 return; 0165 } 0166 file = dialog.choosenFile(); 0167 break; 0168 } 0169 if (m_gcodeEditor->modifiedFiles().contains(file)) { 0170 int result = QMessageBox::question(this, 0171 i18n("Document Modified"), 0172 i18n("%1 \nContains unsaved changes that will not be in the print.\nWould you like to save them before printing?", file.toLocalFile()), 0173 QMessageBox::Save, 0174 QMessageBox::Cancel, 0175 QMessageBox::Ignore); 0176 if (result == QMessageBox::Cancel) { 0177 return; 0178 } 0179 if (result == QMessageBox::Save) { 0180 m_gcodeEditor->saveFile(file); 0181 } 0182 } 0183 newInstanceWidget->printFile(file); 0184 }); 0185 0186 connect(newInstanceWidget, &AtCoreInstanceWidget::bedSizeChanged, this, [this](const QSize &newSize) { 0187 if (m_currInstance == m_instances->currentIndex()) { 0188 updateBedSize(newSize); 0189 } 0190 }); 0191 0192 connect(newInstanceWidget, &AtCoreInstanceWidget::connectionChanged, this, [this, newInstanceWidget](const QString &newName) { m_instances->setTabText(m_instances->indexOf(newInstanceWidget), newName); }); 0193 0194 if (m_instances->count() > 1) { 0195 m_instances->setTabsClosable(true); 0196 m_instances->setMovable(true); 0197 m_instances->setCurrentIndex(m_instances->count() - 1); 0198 } 0199 } 0200 // Move to LateralArea. 0201 void MainWindow::setupLateralArea() 0202 { 0203 m_lateral.m_toolBar = new QWidget(this); 0204 m_lateral.m_stack = new QStackedWidget(this); 0205 auto buttonLayout = new QVBoxLayout(); 0206 0207 auto setupButton = [this, buttonLayout](const QString &key, const QString &text, const QIcon &icon, QWidget *w) { 0208 auto *btn = new QPushButton(m_lateral.m_toolBar); 0209 btn->setToolTip(text); 0210 btn->setAutoExclusive(true); 0211 btn->setCheckable(true); 0212 // Check the top most widget, so users see its selected at startup time. 0213 btn->setChecked(key == QStringLiteral("welcome")); 0214 btn->setIcon(icon); 0215 // Set an iconSize based on the DPI. 0216 // 96 was considered to be the "standard" DPI for years. 0217 // Hi-dpi monitors have a higher DPI; 150+ 0218 // Tiny or old screen could have a lower DPI. 0219 // Start our iconSize at 16 so with a low DPI we get a sane iconsize. 0220 // Use 72 to better scale for less dense Hi-Dpi screens 0221 int iconSize = 16 + ((logicalDpiX() / 72) * 16); 0222 btn->setIconSize(QSize(iconSize, iconSize)); 0223 btn->setFixedSize(btn->iconSize()); 0224 btn->setFlat(true); 0225 m_lateral.m_stack->addWidget(w); 0226 m_lateral.m_map[key] = {btn, w}; 0227 buttonLayout->addWidget(btn); 0228 0229 connect(btn, &QPushButton::clicked, this, [this, w, btn] { 0230 if (m_lateral.m_stack->currentWidget() == w) { 0231 m_lateral.m_stack->setHidden(m_lateral.m_stack->isVisible()); 0232 if (m_lateral.m_stack->isHidden()) { 0233 btn->setCheckable(false); 0234 btn->setCheckable(true); 0235 } 0236 } else { 0237 m_lateral.m_stack->setHidden(false); 0238 m_lateral.m_stack->setCurrentWidget(w); 0239 } 0240 toggleGCodeActions(); 0241 }); 0242 }; 0243 0244 m_gcodeEditor = new GCodeEditorWidget(this); 0245 connect(m_gcodeEditor, &GCodeEditorWidget::updateClientFactory, this, &MainWindow::updateClientFactory); 0246 connect(m_gcodeEditor, &GCodeEditorWidget::droppedUrls, this, &MainWindow::processDropEvent); 0247 connect(m_gcodeEditor, &GCodeEditorWidget::fileClosed, this, [this](const QUrl &file) { m_openFiles.removeAll(file); }); 0248 0249 auto *viewer3D = new Viewer3D(this); 0250 connect(viewer3D, &Viewer3D::droppedUrls, this, &MainWindow::processDropEvent); 0251 // Connect for bed size 0252 connect(m_instances, &QTabWidget::currentChanged, this, [this](int index) { 0253 m_currInstance = index; 0254 auto tempWidget = qobject_cast<AtCoreInstanceWidget *>(m_instances->widget(index)); 0255 updateBedSize(tempWidget->bedSize()); 0256 }); 0257 0258 connect(m_gcodeEditor, &GCodeEditorWidget::currentFileChanged, this, [viewer3D](const QUrl &url) { viewer3D->drawModel(url.toLocalFile()); }); 0259 0260 setupButton("welcome", i18n("Welcome"), QIcon::fromTheme("go-home", QIcon(QString(":/%1/home").arg(m_theme))), new WelcomeWidget(this)); 0261 setupButton("3d", i18n("3D"), QIcon::fromTheme("draw-cuboid", QIcon(QString(":/%1/3d").arg(m_theme))), viewer3D); 0262 setupButton("gcode", i18n("GCode"), QIcon::fromTheme("accessories-text-editor", QIcon(":/icon/edit")), m_gcodeEditor); 0263 setupButton("video", i18n("Video"), QIcon::fromTheme("camera-web", QIcon(":/icon/video")), new VideoMonitorWidget(this)); 0264 buttonLayout->addStretch(); 0265 m_lateral.m_toolBar->setLayout(buttonLayout); 0266 } 0267 0268 void MainWindow::setupActions() 0269 { 0270 // Actions for the Toolbar 0271 QAction *action; 0272 action = actionCollection()->addAction(QStringLiteral("open")); 0273 action->setIcon(QIcon::fromTheme("document-open", QIcon(QString(":/%1/open").arg(m_theme)))); 0274 0275 action->setText(i18n("&Open")); 0276 actionCollection()->setDefaultShortcut(action, QKeySequence::Open); 0277 connect(action, &QAction::triggered, this, &MainWindow::openActionTriggered); 0278 0279 action = actionCollection()->addAction(QStringLiteral("new_instance")); 0280 action->setIcon(QIcon::fromTheme("list-add", QIcon(QString(":/%1/addTab").arg(m_theme)))); 0281 0282 action->setText(i18n("&New Connection")); 0283 actionCollection()->setDefaultShortcut(action, QKeySequence::AddTab); 0284 connect(action, &QAction::triggered, this, &MainWindow::newAtCoreInstance); 0285 0286 action = actionCollection()->addAction(QStringLiteral("profiles")); 0287 action->setIcon(QIcon::fromTheme("document-properties", QIcon(QString(":/%1/configure").arg(m_theme)))); 0288 0289 action->setText(i18n("&Profiles")); 0290 connect(action, &QAction::triggered, this, [this] { 0291 std::unique_ptr<ProfilesDialog> pd(new ProfilesDialog); 0292 pd->exec(); 0293 emit(profilesChanged()); 0294 }); 0295 0296 action = actionCollection()->addAction(QStringLiteral("quit")); 0297 action->setIcon(QIcon::fromTheme("application-exit", QIcon(":/icon/exit"))); 0298 0299 action->setText(i18n("&Quit")); 0300 actionCollection()->setDefaultShortcut(action, QKeySequence::Quit); 0301 connect(action, &QAction::triggered, this, &MainWindow::close); 0302 0303 setupGUI(Default, "atelierui"); 0304 } 0305 0306 void MainWindow::openActionTriggered() 0307 { 0308 QList<QUrl> fileList = QFileDialog::getOpenFileUrls(this, i18n("Open GCode"), QUrl::fromLocalFile(QDir::homePath()), i18n("GCode(*.gco *.gcode);;All Files(*.*)")); 0309 for (const auto &url : fileList) { 0310 loadFile(url); 0311 } 0312 } 0313 0314 void MainWindow::loadFile(const QUrl &fileName) 0315 { 0316 if (!fileName.isEmpty()) { 0317 m_lateral.get<GCodeEditorWidget>("gcode")->loadFile(fileName); 0318 m_lateral.get<Viewer3D>("3d")->drawModel(fileName.toLocalFile()); 0319 // Make 3dview focused when opening a file 0320 if (m_openFiles.isEmpty() && m_lateral.m_stack->currentWidget() == m_lateral.get<WelcomeWidget>("welcome")) { 0321 m_lateral.getButton<QPushButton>("3d")->setChecked(true); 0322 m_lateral.m_stack->setCurrentWidget(m_lateral.get<Viewer3D>("3d")); 0323 } 0324 0325 const int tabs = m_instances->count(); 0326 if (!m_openFiles.contains(fileName)) { 0327 m_openFiles.append(fileName); 0328 } 0329 0330 for (int i = 0; i < tabs; ++i) { 0331 auto instance = qobject_cast<AtCoreInstanceWidget *>(m_instances->widget(i)); 0332 instance->setFileCount(m_openFiles.size()); 0333 } 0334 } 0335 } 0336 0337 QString MainWindow::getTheme() 0338 { 0339 return palette().text().color().value() >= QColor(Qt::lightGray).value() ? QString("dark") : QString("light"); 0340 } 0341 0342 bool MainWindow::askToClose() 0343 { 0344 bool rtn = false; 0345 int result = QMessageBox::question(this, i18n("Printing"), i18n("Currently printing! \nAre you sure you want to close?"), QMessageBox::Close, QMessageBox::Cancel); 0346 0347 switch (result) { 0348 case QMessageBox::Close: 0349 rtn = true; 0350 break; 0351 default: 0352 break; 0353 } 0354 return rtn; 0355 } 0356 0357 void MainWindow::toggleGCodeActions() 0358 { 0359 if (m_lateral.m_stack->currentWidget() == m_lateral.m_map["gcode"].second && m_lateral.m_stack->isVisible()) { 0360 if (m_currEditorView) { 0361 guiFactory()->addClient(m_currEditorView); 0362 } 0363 } else { 0364 guiFactory()->removeClient(m_currEditorView); 0365 } 0366 } 0367 0368 void MainWindow::updateClientFactory(KTextEditor::View *view) 0369 { 0370 if (m_lateral.m_stack->currentWidget() == m_lateral.m_map["gcode"].second) { 0371 if (m_currEditorView) { 0372 guiFactory()->removeClient(m_currEditorView); 0373 } 0374 if (view) { 0375 guiFactory()->addClient(view); 0376 } 0377 } 0378 m_currEditorView = view; 0379 } 0380 0381 bool MainWindow::askToSave(const QVector<QUrl> &fileList) 0382 { 0383 if (fileList.isEmpty()) { 0384 return true; 0385 } 0386 QSize iconSize = QSize(fontMetrics().lineSpacing(), fontMetrics().lineSpacing()); 0387 auto dialog = new QDialog(this); 0388 const int padding = 30; 0389 auto listWidget = new QListWidget(dialog); 0390 listWidget->setMinimumWidth(fontMetrics().height() / 2 * padding); 0391 for (const auto &url : fileList) { 0392 listWidget->addItem(url.toLocalFile() + " [*]"); 0393 } 0394 0395 auto hLayout = new QHBoxLayout(); 0396 auto saveBtn = new QPushButton(QIcon::fromTheme("document-save", QIcon(QStringLiteral(":/%1/save").arg(m_theme))), i18n("Save Selected"), dialog); 0397 saveBtn->setIconSize(iconSize); 0398 saveBtn->setEnabled(false); 0399 0400 connect(listWidget, &QListWidget::currentRowChanged, this, [saveBtn](const int currentRow) { saveBtn->setEnabled(currentRow >= 0); }); 0401 0402 connect(saveBtn, &QPushButton::clicked, this, [this, listWidget, &fileList, dialog] { 0403 if (!m_gcodeEditor->saveFile(fileList.at(listWidget->currentRow()))) { 0404 QMessageBox::information(this, i18n("Save Failed"), i18n("Failed to save file: %1", fileList.at(listWidget->currentRow()).toLocalFile())); 0405 } else { 0406 listWidget->item(listWidget->currentRow())->setText(listWidget->item(listWidget->currentRow())->text().remove(" [*]")); 0407 for (int i = 0; i < listWidget->count(); i++) { 0408 if (listWidget->item(i)->text().endsWith(" [*]")) { 0409 return; 0410 } 0411 } 0412 dialog->accept(); 0413 } 0414 }); 0415 hLayout->addWidget(saveBtn); 0416 0417 auto saveAllBtn = new QPushButton(QIcon::fromTheme("document-save-all", QIcon(QStringLiteral(":/%1/saveAll").arg(m_theme))), i18n("Save All"), dialog); 0418 saveAllBtn->setIconSize(iconSize); 0419 connect(saveAllBtn, &QPushButton::clicked, this, [this, listWidget, &fileList, dialog] { 0420 for (int i = 0; i < listWidget->count(); i++) { 0421 if (!m_gcodeEditor->saveFile(fileList.at(i))) { 0422 QMessageBox::information(this, i18n("Save Failed"), i18n("Failed to save file: %1", fileList.at(i).toLocalFile())); 0423 dialog->reject(); 0424 } else { 0425 listWidget->item(i)->setText(listWidget->item(i)->text().remove(" [*]")); 0426 } 0427 } 0428 dialog->accept(); 0429 }); 0430 hLayout->addWidget(saveAllBtn); 0431 0432 auto cancelBtn = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(QStringLiteral(":/%1/cancel").arg(m_theme))), i18n("Cancel"), dialog); 0433 cancelBtn->setIconSize(iconSize); 0434 cancelBtn->setDefault(true); 0435 connect(cancelBtn, &QPushButton::clicked, this, [dialog] { dialog->reject(); }); 0436 hLayout->addWidget(cancelBtn); 0437 0438 auto ignoreBtn = new QPushButton(QIcon::fromTheme("edit-delete", style()->standardIcon(QStyle::SP_TrashIcon)), i18n("Discard Changes"), dialog); 0439 ignoreBtn->setIconSize(iconSize); 0440 connect(ignoreBtn, &QPushButton::clicked, this, [dialog] { dialog->accept(); }); 0441 hLayout->addWidget(ignoreBtn); 0442 0443 auto layout = new QVBoxLayout; 0444 auto label = new QLabel(i18n("Files with Unsaved Changes."), dialog); 0445 layout->addWidget(label); 0446 layout->addWidget(listWidget); 0447 layout->addItem(hLayout); 0448 dialog->setLayout(layout); 0449 0450 return dialog->exec(); 0451 } 0452 0453 void MainWindow::updateBedSize(const QSize &newSize) 0454 { 0455 m_lateral.get<Viewer3D>("3d")->setBedSize(newSize); 0456 }