| 1 /* |
|
| 2 * LDForge: LDraw parts authoring CAD |
|
| 3 * Copyright (C) 2013, 2014 Santeri Piippo |
|
| 4 * |
|
| 5 * This program is free software: you can redistribute it and/or modify |
|
| 6 * it under the terms of the GNU General Public License as published by |
|
| 7 * the Free Software Foundation, either version 3 of the License, or |
|
| 8 * (at your option) any later version. |
|
| 9 * |
|
| 10 * This program is distributed in the hope that it will be useful, |
|
| 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 13 * GNU General Public License for more details. |
|
| 14 * |
|
| 15 * You should have received a copy of the GNU General Public License |
|
| 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
| 17 */ |
|
| 18 |
|
| 19 #include <QGridLayout> |
|
| 20 #include <QMessageBox> |
|
| 21 #include <QEvent> |
|
| 22 #include <QContextMenuEvent> |
|
| 23 #include <QMenuBar> |
|
| 24 #include <QStatusBar> |
|
| 25 #include <QSplitter> |
|
| 26 #include <QListWidget> |
|
| 27 #include <QToolButton> |
|
| 28 #include <QComboBox> |
|
| 29 #include <QDialogButtonBox> |
|
| 30 #include <QToolBar> |
|
| 31 #include <QProgressBar> |
|
| 32 #include <QLabel> |
|
| 33 #include <QFileDialog> |
|
| 34 #include <QPushButton> |
|
| 35 #include <QCoreApplication> |
|
| 36 #include <QTimer> |
|
| 37 #include <QMetaMethod> |
|
| 38 #include "Main.h" |
|
| 39 #include "GLRenderer.h" |
|
| 40 #include "MainWindow.h" |
|
| 41 #include "Document.h" |
|
| 42 #include "Configuration.h" |
|
| 43 #include "Misc.h" |
|
| 44 #include "Colors.h" |
|
| 45 #include "EditHistory.h" |
|
| 46 #include "Widgets.h" |
|
| 47 #include "AddObjectDialog.h" |
|
| 48 #include "MessageLog.h" |
|
| 49 #include "Configuration.h" |
|
| 50 #include "ui_ldforge.h" |
|
| 51 |
|
| 52 static bool g_isSelectionLocked = false; |
|
| 53 |
|
| 54 cfg (Bool, lv_colorize, true); |
|
| 55 cfg (String, gui_colortoolbar, "16:24:|:4:25:14:27:2:3:11:1:22:|:0:72:71:15"); |
|
| 56 cfg (Bool, gui_implicitfiles, false); |
|
| 57 extern_cfg (List, io_recentfiles); |
|
| 58 extern_cfg (Bool, gl_axes); |
|
| 59 extern_cfg (String, gl_maincolor); |
|
| 60 extern_cfg (Float, gl_maincolor_alpha); |
|
| 61 extern_cfg (Bool, gl_wireframe); |
|
| 62 extern_cfg (Bool, gl_colorbfc); |
|
| 63 extern_cfg (Bool, gl_drawangles); |
|
| 64 |
|
| 65 // ============================================================================= |
|
| 66 // |
|
| 67 MainWindow::MainWindow (QWidget* parent, Qt::WindowFlags flags) : |
|
| 68 QMainWindow (parent, flags) |
|
| 69 { |
|
| 70 g_win = this; |
|
| 71 ui = new Ui_LDForgeUI; |
|
| 72 ui->setupUi (this); |
|
| 73 m_updatingTabs = false; |
|
| 74 m_renderer = new GLRenderer (this); |
|
| 75 m_tabs = new QTabBar; |
|
| 76 ui->verticalLayout->insertWidget (0, m_tabs); |
|
| 77 |
|
| 78 // Stuff the renderer into its frame |
|
| 79 QVBoxLayout* rendererLayout = new QVBoxLayout (ui->rendererFrame); |
|
| 80 rendererLayout->addWidget (R()); |
|
| 81 |
|
| 82 connect (ui->objectList, SIGNAL (itemSelectionChanged()), this, SLOT (slot_selectionChanged())); |
|
| 83 connect (ui->objectList, SIGNAL (itemDoubleClicked (QListWidgetItem*)), this, SLOT (slot_editObject (QListWidgetItem*))); |
|
| 84 connect (m_tabs, SIGNAL (currentChanged(int)), this, SLOT (changeCurrentFile())); |
|
| 85 |
|
| 86 // Init message log manager |
|
| 87 m_msglog = new MessageManager; |
|
| 88 m_msglog->setRenderer (R()); |
|
| 89 m_renderer->setMessageLog (m_msglog); |
|
| 90 m_quickColors = quickColorsFromConfig(); |
|
| 91 slot_selectionChanged(); |
|
| 92 setStatusBar (new QStatusBar); |
|
| 93 |
|
| 94 // Make certain actions checkable |
|
| 95 ui->actionAxes->setChecked (gl_axes); |
|
| 96 ui->actionWireframe->setChecked (gl_wireframe); |
|
| 97 ui->actionBFCView->setChecked (gl_colorbfc); |
|
| 98 updateGridToolBar(); |
|
| 99 updateEditModeActions(); |
|
| 100 updateRecentFilesMenu(); |
|
| 101 updateColorToolbar(); |
|
| 102 updateTitle(); |
|
| 103 updateActionShortcuts(); |
|
| 104 |
|
| 105 setMinimumSize (300, 200); |
|
| 106 |
|
| 107 connect (qApp, SIGNAL (aboutToQuit()), this, SLOT (slot_lastSecondCleanup())); |
|
| 108 |
|
| 109 // Connect all actions |
|
| 110 for (QAction* act : findChildren<QAction*>()) |
|
| 111 if (!act->objectName().isEmpty()) |
|
| 112 connect (act, SIGNAL (triggered()), this, SLOT (slot_action())); |
|
| 113 } |
|
| 114 |
|
| 115 // ============================================================================= |
|
| 116 // |
|
| 117 KeySequenceConfig* MainWindow::shortcutForAction (QAction* action) |
|
| 118 { |
|
| 119 QString keycfgname = format ("key_%1", action->objectName()); |
|
| 120 return KeySequenceConfig::getByName (keycfgname); |
|
| 121 } |
|
| 122 |
|
| 123 // ============================================================================= |
|
| 124 // |
|
| 125 void MainWindow::updateActionShortcuts() |
|
| 126 { |
|
| 127 for (QAction* act : findChildren<QAction*>()) |
|
| 128 { |
|
| 129 KeySequenceConfig* cfg = shortcutForAction (act); |
|
| 130 |
|
| 131 if (cfg) |
|
| 132 act->setShortcut (cfg->getValue()); |
|
| 133 } |
|
| 134 } |
|
| 135 |
|
| 136 // ============================================================================= |
|
| 137 // |
|
| 138 void MainWindow::slot_action() |
|
| 139 { |
|
| 140 // Get the name of the sender object and use it to compose the slot name, |
|
| 141 // then invoke this slot to call the action. |
|
| 142 QMetaObject::invokeMethod (this, |
|
| 143 qPrintable (format ("slot_%1", sender()->objectName())), Qt::DirectConnection); |
|
| 144 endAction(); |
|
| 145 } |
|
| 146 |
|
| 147 // ============================================================================= |
|
| 148 // |
|
| 149 void MainWindow::endAction() |
|
| 150 { |
|
| 151 // Add a step in the history now. |
|
| 152 getCurrentDocument()->addHistoryStep(); |
|
| 153 |
|
| 154 // Update the list item of the current file - we may need to draw an icon |
|
| 155 // now that marks it as having unsaved changes. |
|
| 156 updateDocumentListItem (getCurrentDocument()); |
|
| 157 } |
|
| 158 |
|
| 159 // ============================================================================= |
|
| 160 // |
|
| 161 void MainWindow::slot_lastSecondCleanup() |
|
| 162 { |
|
| 163 delete m_renderer; |
|
| 164 delete ui; |
|
| 165 } |
|
| 166 |
|
| 167 // ============================================================================= |
|
| 168 // |
|
| 169 void MainWindow::updateRecentFilesMenu() |
|
| 170 { |
|
| 171 // First, clear any items in the recent files menu |
|
| 172 for (QAction * recent : m_recentFiles) |
|
| 173 delete recent; |
|
| 174 |
|
| 175 m_recentFiles.clear(); |
|
| 176 |
|
| 177 QAction* first = null; |
|
| 178 |
|
| 179 for (const QVariant& it : io_recentfiles) |
|
| 180 { |
|
| 181 QString file = it.toString(); |
|
| 182 QAction* recent = new QAction (getIcon ("open-recent"), file, this); |
|
| 183 |
|
| 184 connect (recent, SIGNAL (triggered()), this, SLOT (slot_recentFile())); |
|
| 185 ui->menuOpenRecent->insertAction (first, recent); |
|
| 186 m_recentFiles << recent; |
|
| 187 first = recent; |
|
| 188 } |
|
| 189 } |
|
| 190 |
|
| 191 // ============================================================================= |
|
| 192 // |
|
| 193 QList<LDQuickColor> quickColorsFromConfig() |
|
| 194 { |
|
| 195 QList<LDQuickColor> colors; |
|
| 196 |
|
| 197 for (QString colorname : gui_colortoolbar.split (":")) |
|
| 198 { |
|
| 199 if (colorname == "|") |
|
| 200 colors << LDQuickColor::getSeparator(); |
|
| 201 else |
|
| 202 { |
|
| 203 LDColor* col = getColor (colorname.toLong()); |
|
| 204 |
|
| 205 if (col != null) |
|
| 206 colors << LDQuickColor (col, null); |
|
| 207 } |
|
| 208 } |
|
| 209 |
|
| 210 return colors; |
|
| 211 } |
|
| 212 |
|
| 213 // ============================================================================= |
|
| 214 // |
|
| 215 void MainWindow::updateColorToolbar() |
|
| 216 { |
|
| 217 m_colorButtons.clear(); |
|
| 218 ui->colorToolbar->clear(); |
|
| 219 |
|
| 220 for (LDQuickColor& entry : m_quickColors) |
|
| 221 { |
|
| 222 if (entry.isSeparator()) |
|
| 223 ui->colorToolbar->addSeparator(); |
|
| 224 else |
|
| 225 { |
|
| 226 QToolButton* colorButton = new QToolButton; |
|
| 227 colorButton->setIcon (makeColorIcon (entry.color(), 22)); |
|
| 228 colorButton->setIconSize (QSize (22, 22)); |
|
| 229 colorButton->setToolTip (entry.color()->name); |
|
| 230 |
|
| 231 connect (colorButton, SIGNAL (clicked()), this, SLOT (slot_quickColor())); |
|
| 232 ui->colorToolbar->addWidget (colorButton); |
|
| 233 m_colorButtons << colorButton; |
|
| 234 |
|
| 235 entry.setToolButton (colorButton); |
|
| 236 } |
|
| 237 } |
|
| 238 |
|
| 239 updateGridToolBar(); |
|
| 240 } |
|
| 241 |
|
| 242 // ============================================================================= |
|
| 243 // |
|
| 244 void MainWindow::updateGridToolBar() |
|
| 245 { |
|
| 246 // Ensure that the current grid - and only the current grid - is selected. |
|
| 247 ui->actionGridCoarse->setChecked (grid == Grid::Coarse); |
|
| 248 ui->actionGridMedium->setChecked (grid == Grid::Medium); |
|
| 249 ui->actionGridFine->setChecked (grid == Grid::Fine); |
|
| 250 } |
|
| 251 |
|
| 252 // ============================================================================= |
|
| 253 // |
|
| 254 void MainWindow::updateTitle() |
|
| 255 { |
|
| 256 QString title = format (APPNAME " %1", fullVersionString()); |
|
| 257 |
|
| 258 // Append our current file if we have one |
|
| 259 if (getCurrentDocument()) |
|
| 260 { |
|
| 261 if (getCurrentDocument()->name().length() > 0) |
|
| 262 title += format (": %1", basename (getCurrentDocument()->name())); |
|
| 263 else |
|
| 264 title += format (": <anonymous>"); |
|
| 265 |
|
| 266 if (getCurrentDocument()->getObjectCount() > 0 && |
|
| 267 getCurrentDocument()->getObject (0)->type() == LDObject::EComment) |
|
| 268 { |
|
| 269 // Append title |
|
| 270 LDComment* comm = static_cast<LDComment*> (getCurrentDocument()->getObject (0)); |
|
| 271 title += format (": %1", comm->text()); |
|
| 272 } |
|
| 273 |
|
| 274 if (getCurrentDocument()->hasUnsavedChanges()) |
|
| 275 title += '*'; |
|
| 276 } |
|
| 277 |
|
| 278 #ifdef DEBUG |
|
| 279 title += " [debug build]"; |
|
| 280 #elif BUILD_ID != BUILD_RELEASE |
|
| 281 title += " [pre-release build]"; |
|
| 282 #endif // DEBUG |
|
| 283 |
|
| 284 #ifdef COMPILE_DATE |
|
| 285 title += " (built " COMPILE_DATE ")"; |
|
| 286 #endif // COMPILE_DATE |
|
| 287 |
|
| 288 setWindowTitle (title); |
|
| 289 } |
|
| 290 |
|
| 291 // ============================================================================= |
|
| 292 // |
|
| 293 int MainWindow::deleteSelection() |
|
| 294 { |
|
| 295 if (selection().isEmpty()) |
|
| 296 return 0; |
|
| 297 |
|
| 298 LDObjectList selCopy = selection(); |
|
| 299 |
|
| 300 // Delete the objects that were being selected |
|
| 301 for (LDObject* obj : selCopy) |
|
| 302 obj->destroy(); |
|
| 303 |
|
| 304 refresh(); |
|
| 305 return selCopy.size(); |
|
| 306 } |
|
| 307 |
|
| 308 // ============================================================================= |
|
| 309 // |
|
| 310 void MainWindow::buildObjList() |
|
| 311 { |
|
| 312 if (!getCurrentDocument()) |
|
| 313 return; |
|
| 314 |
|
| 315 // Lock the selection while we do this so that refreshing the object list |
|
| 316 // doesn't trigger selection updating so that the selection doesn't get lost |
|
| 317 // while this is done. |
|
| 318 g_isSelectionLocked = true; |
|
| 319 |
|
| 320 for (int i = 0; i < ui->objectList->count(); ++i) |
|
| 321 delete ui->objectList->item (i); |
|
| 322 |
|
| 323 ui->objectList->clear(); |
|
| 324 |
|
| 325 for (LDObject* obj : getCurrentDocument()->objects()) |
|
| 326 { |
|
| 327 QString descr; |
|
| 328 |
|
| 329 switch (obj->type()) |
|
| 330 { |
|
| 331 case LDObject::EComment: |
|
| 332 { |
|
| 333 descr = static_cast<LDComment*> (obj)->text(); |
|
| 334 |
|
| 335 // Remove leading whitespace |
|
| 336 while (descr[0] == ' ') |
|
| 337 descr.remove (0, 1); |
|
| 338 |
|
| 339 break; |
|
| 340 } |
|
| 341 |
|
| 342 case LDObject::EEmpty: |
|
| 343 break; // leave it empty |
|
| 344 |
|
| 345 case LDObject::ELine: |
|
| 346 case LDObject::ETriangle: |
|
| 347 case LDObject::EQuad: |
|
| 348 case LDObject::ECondLine: |
|
| 349 { |
|
| 350 for (int i = 0; i < obj->vertices(); ++i) |
|
| 351 { |
|
| 352 if (i != 0) |
|
| 353 descr += ", "; |
|
| 354 |
|
| 355 descr += obj->vertex (i).toString (true); |
|
| 356 } |
|
| 357 break; |
|
| 358 } |
|
| 359 |
|
| 360 case LDObject::EError: |
|
| 361 { |
|
| 362 descr = format ("ERROR: %1", obj->asText()); |
|
| 363 break; |
|
| 364 } |
|
| 365 |
|
| 366 case LDObject::EVertex: |
|
| 367 { |
|
| 368 descr = static_cast<LDVertex*> (obj)->pos.toString (true); |
|
| 369 break; |
|
| 370 } |
|
| 371 |
|
| 372 case LDObject::ESubfile: |
|
| 373 { |
|
| 374 LDSubfile* ref = static_cast<LDSubfile*> (obj); |
|
| 375 |
|
| 376 descr = format ("%1 %2, (", ref->fileInfo()->getDisplayName(), ref->position().toString (true)); |
|
| 377 |
|
| 378 for (int i = 0; i < 9; ++i) |
|
| 379 descr += format ("%1%2", ref->transform()[i], (i != 8) ? " " : ""); |
|
| 380 |
|
| 381 descr += ')'; |
|
| 382 break; |
|
| 383 } |
|
| 384 |
|
| 385 case LDObject::EBFC: |
|
| 386 { |
|
| 387 descr = LDBFC::k_statementStrings[static_cast<LDBFC*> (obj)->statement()]; |
|
| 388 break; |
|
| 389 } |
|
| 390 |
|
| 391 case LDObject::EOverlay: |
|
| 392 { |
|
| 393 LDOverlay* ovl = static_cast<LDOverlay*> (obj); |
|
| 394 descr = format ("[%1] %2 (%3, %4), %5 x %6", g_CameraNames[ovl->camera()], |
|
| 395 basename (ovl->fileName()), ovl->x(), ovl->y(), |
|
| 396 ovl->width(), ovl->height()); |
|
| 397 break; |
|
| 398 } |
|
| 399 |
|
| 400 default: |
|
| 401 { |
|
| 402 descr = obj->typeName(); |
|
| 403 break; |
|
| 404 } |
|
| 405 } |
|
| 406 |
|
| 407 QListWidgetItem* item = new QListWidgetItem (descr); |
|
| 408 item->setIcon (getIcon (obj->typeName())); |
|
| 409 |
|
| 410 // Use italic font if hidden |
|
| 411 if (obj->isHidden()) |
|
| 412 { |
|
| 413 QFont font = item->font(); |
|
| 414 font.setItalic (true); |
|
| 415 item->setFont (font); |
|
| 416 } |
|
| 417 |
|
| 418 // Color gibberish orange on red so it stands out. |
|
| 419 if (obj->type() == LDObject::EError) |
|
| 420 { |
|
| 421 item->setBackground (QColor ("#AA0000")); |
|
| 422 item->setForeground (QColor ("#FFAA00")); |
|
| 423 } |
|
| 424 elif (lv_colorize && obj->isColored() && obj->color() != maincolor && obj->color() != edgecolor) |
|
| 425 { |
|
| 426 // If the object isn't in the main or edge color, draw this |
|
| 427 // list entry in said color. |
|
| 428 LDColor* col = getColor (obj->color()); |
|
| 429 |
|
| 430 if (col) |
|
| 431 item->setForeground (col->faceColor); |
|
| 432 } |
|
| 433 |
|
| 434 obj->qObjListEntry = item; |
|
| 435 ui->objectList->insertItem (ui->objectList->count(), item); |
|
| 436 } |
|
| 437 |
|
| 438 g_isSelectionLocked = false; |
|
| 439 updateSelection(); |
|
| 440 scrollToSelection(); |
|
| 441 } |
|
| 442 |
|
| 443 // ============================================================================= |
|
| 444 // |
|
| 445 void MainWindow::scrollToSelection() |
|
| 446 { |
|
| 447 if (selection().isEmpty()) |
|
| 448 return; |
|
| 449 |
|
| 450 LDObject* obj = selection().last(); |
|
| 451 ui->objectList->scrollToItem (obj->qObjListEntry); |
|
| 452 } |
|
| 453 |
|
| 454 // ============================================================================= |
|
| 455 // |
|
| 456 void MainWindow::slot_selectionChanged() |
|
| 457 { |
|
| 458 if (g_isSelectionLocked == true || getCurrentDocument() == null) |
|
| 459 return; |
|
| 460 |
|
| 461 // Update the shared selection array, though don't do this if this was |
|
| 462 // called during GL picking, in which case the GL renderer takes care |
|
| 463 // of the selection. |
|
| 464 if (m_renderer->isPicking()) |
|
| 465 return; |
|
| 466 |
|
| 467 LDObjectList priorSelection = selection(); |
|
| 468 |
|
| 469 // Get the objects from the object list selection |
|
| 470 getCurrentDocument()->clearSelection(); |
|
| 471 const QList<QListWidgetItem*> items = ui->objectList->selectedItems(); |
|
| 472 |
|
| 473 for (LDObject* obj : getCurrentDocument()->objects()) |
|
| 474 { |
|
| 475 for (QListWidgetItem* item : items) |
|
| 476 { |
|
| 477 if (item == obj->qObjListEntry) |
|
| 478 { |
|
| 479 obj->select(); |
|
| 480 break; |
|
| 481 } |
|
| 482 } |
|
| 483 } |
|
| 484 |
|
| 485 // Update the GL renderer |
|
| 486 LDObjectList compound = priorSelection + selection(); |
|
| 487 removeDuplicates (compound); |
|
| 488 |
|
| 489 for (LDObject* obj : compound) |
|
| 490 m_renderer->compileObject (obj); |
|
| 491 |
|
| 492 m_renderer->update(); |
|
| 493 } |
|
| 494 |
|
| 495 // ============================================================================= |
|
| 496 // |
|
| 497 void MainWindow::slot_recentFile() |
|
| 498 { |
|
| 499 QAction* qAct = static_cast<QAction*> (sender()); |
|
| 500 openMainFile (qAct->text()); |
|
| 501 } |
|
| 502 |
|
| 503 // ============================================================================= |
|
| 504 // |
|
| 505 void MainWindow::slot_quickColor() |
|
| 506 { |
|
| 507 QToolButton* button = static_cast<QToolButton*> (sender()); |
|
| 508 LDColor* col = null; |
|
| 509 |
|
| 510 for (const LDQuickColor& entry : m_quickColors) |
|
| 511 { |
|
| 512 if (entry.toolButton() == button) |
|
| 513 { |
|
| 514 col = entry.color(); |
|
| 515 break; |
|
| 516 } |
|
| 517 } |
|
| 518 |
|
| 519 if (col == null) |
|
| 520 return; |
|
| 521 |
|
| 522 int newColor = col->index; |
|
| 523 |
|
| 524 for (LDObject* obj : selection()) |
|
| 525 { |
|
| 526 if (obj->isColored() == false) |
|
| 527 continue; // uncolored object |
|
| 528 |
|
| 529 obj->setColor (newColor); |
|
| 530 R()->compileObject (obj); |
|
| 531 } |
|
| 532 |
|
| 533 endAction(); |
|
| 534 refresh(); |
|
| 535 } |
|
| 536 |
|
| 537 // ============================================================================= |
|
| 538 // |
|
| 539 int MainWindow::getInsertionPoint() |
|
| 540 { |
|
| 541 // If we have a selection, put the item after it. |
|
| 542 if (!selection().isEmpty()) |
|
| 543 return selection().last()->lineNumber() + 1; |
|
| 544 |
|
| 545 // Otherwise place the object at the end. |
|
| 546 return getCurrentDocument()->getObjectCount(); |
|
| 547 } |
|
| 548 |
|
| 549 // ============================================================================= |
|
| 550 // |
|
| 551 void MainWindow::doFullRefresh() |
|
| 552 { |
|
| 553 buildObjList(); |
|
| 554 m_renderer->hardRefresh(); |
|
| 555 } |
|
| 556 |
|
| 557 // ============================================================================= |
|
| 558 // |
|
| 559 void MainWindow::refresh() |
|
| 560 { |
|
| 561 buildObjList(); |
|
| 562 m_renderer->update(); |
|
| 563 } |
|
| 564 |
|
| 565 // ============================================================================= |
|
| 566 // |
|
| 567 void MainWindow::updateSelection() |
|
| 568 { |
|
| 569 g_isSelectionLocked = true; |
|
| 570 |
|
| 571 for (LDObject* obj : getCurrentDocument()->objects()) |
|
| 572 obj->setSelected (false); |
|
| 573 |
|
| 574 ui->objectList->clearSelection(); |
|
| 575 |
|
| 576 for (LDObject* obj : selection()) |
|
| 577 { |
|
| 578 if (obj->qObjListEntry == null) |
|
| 579 continue; |
|
| 580 |
|
| 581 obj->qObjListEntry->setSelected (true); |
|
| 582 obj->setSelected (true); |
|
| 583 } |
|
| 584 |
|
| 585 g_isSelectionLocked = false; |
|
| 586 slot_selectionChanged(); |
|
| 587 } |
|
| 588 |
|
| 589 // ============================================================================= |
|
| 590 // |
|
| 591 int MainWindow::getSelectedColor() |
|
| 592 { |
|
| 593 int result = -1; |
|
| 594 |
|
| 595 for (LDObject* obj : selection()) |
|
| 596 { |
|
| 597 if (obj->isColored() == false) |
|
| 598 continue; // doesn't use color |
|
| 599 |
|
| 600 if (result != -1 && obj->color() != result) |
|
| 601 return -1; // No consensus in object color |
|
| 602 |
|
| 603 if (result == -1) |
|
| 604 result = obj->color(); |
|
| 605 } |
|
| 606 |
|
| 607 return result; |
|
| 608 } |
|
| 609 |
|
| 610 // ============================================================================= |
|
| 611 // |
|
| 612 LDObject::Type MainWindow::getUniformSelectedType() |
|
| 613 { |
|
| 614 LDObject::Type result = LDObject::EUnidentified; |
|
| 615 |
|
| 616 for (LDObject* obj : selection()) |
|
| 617 { |
|
| 618 if (result != LDObject::EUnidentified && obj->color() != result) |
|
| 619 return LDObject::EUnidentified; |
|
| 620 |
|
| 621 if (result == LDObject::EUnidentified) |
|
| 622 result = obj->type(); |
|
| 623 } |
|
| 624 |
|
| 625 return result; |
|
| 626 } |
|
| 627 |
|
| 628 // ============================================================================= |
|
| 629 // |
|
| 630 void MainWindow::closeEvent (QCloseEvent* ev) |
|
| 631 { |
|
| 632 // Check whether it's safe to close all files. |
|
| 633 if (!safeToCloseAll()) |
|
| 634 { |
|
| 635 ev->ignore(); |
|
| 636 return; |
|
| 637 } |
|
| 638 |
|
| 639 // Save the configuration before leaving so that, for instance, grid choice |
|
| 640 // is preserved across instances. |
|
| 641 Config::save(); |
|
| 642 |
|
| 643 ev->accept(); |
|
| 644 } |
|
| 645 |
|
| 646 // ============================================================================= |
|
| 647 // |
|
| 648 void MainWindow::spawnContextMenu (const QPoint pos) |
|
| 649 { |
|
| 650 const bool single = (selection().size() == 1); |
|
| 651 LDObject* singleObj = (single) ? selection()[0] : null; |
|
| 652 |
|
| 653 QMenu* contextMenu = new QMenu; |
|
| 654 |
|
| 655 if (single && singleObj->type() != LDObject::EEmpty) |
|
| 656 { |
|
| 657 contextMenu->addAction (ui->actionEdit); |
|
| 658 contextMenu->addSeparator(); |
|
| 659 } |
|
| 660 |
|
| 661 contextMenu->addAction (ui->actionCut); |
|
| 662 contextMenu->addAction (ui->actionCopy); |
|
| 663 contextMenu->addAction (ui->actionPaste); |
|
| 664 contextMenu->addAction (ui->actionDelete); |
|
| 665 contextMenu->addSeparator(); |
|
| 666 contextMenu->addAction (ui->actionSetColor); |
|
| 667 |
|
| 668 if (single) |
|
| 669 contextMenu->addAction (ui->actionEditRaw); |
|
| 670 |
|
| 671 contextMenu->addAction (ui->actionBorders); |
|
| 672 contextMenu->addAction (ui->actionSetOverlay); |
|
| 673 contextMenu->addAction (ui->actionClearOverlay); |
|
| 674 contextMenu->addAction (ui->actionModeSelect); |
|
| 675 contextMenu->addAction (ui->actionModeDraw); |
|
| 676 contextMenu->addAction (ui->actionModeCircle); |
|
| 677 |
|
| 678 if (selection().size() > 0) |
|
| 679 { |
|
| 680 contextMenu->addSeparator(); |
|
| 681 contextMenu->addAction (ui->actionSubfileSelection); |
|
| 682 } |
|
| 683 |
|
| 684 if (R()->camera() != GL::EFreeCamera) |
|
| 685 { |
|
| 686 contextMenu->addSeparator(); |
|
| 687 contextMenu->addAction (ui->actionSetDrawDepth); |
|
| 688 } |
|
| 689 |
|
| 690 contextMenu->exec (pos); |
|
| 691 } |
|
| 692 |
|
| 693 // ============================================================================= |
|
| 694 // |
|
| 695 void MainWindow::deleteByColor (int colnum) |
|
| 696 { |
|
| 697 LDObjectList objs; |
|
| 698 |
|
| 699 for (LDObject* obj : getCurrentDocument()->objects()) |
|
| 700 { |
|
| 701 if (!obj->isColored() || obj->color() != colnum) |
|
| 702 continue; |
|
| 703 |
|
| 704 objs << obj; |
|
| 705 } |
|
| 706 |
|
| 707 for (LDObject* obj : objs) |
|
| 708 obj->destroy(); |
|
| 709 } |
|
| 710 |
|
| 711 // ============================================================================= |
|
| 712 // |
|
| 713 void MainWindow::updateEditModeActions() |
|
| 714 { |
|
| 715 const EditMode mode = R()->editMode(); |
|
| 716 ui->actionModeSelect->setChecked (mode == ESelectMode); |
|
| 717 ui->actionModeDraw->setChecked (mode == EDrawMode); |
|
| 718 ui->actionModeCircle->setChecked (mode == ECircleMode); |
|
| 719 } |
|
| 720 |
|
| 721 // ============================================================================= |
|
| 722 // |
|
| 723 void MainWindow::slot_editObject (QListWidgetItem* listitem) |
|
| 724 { |
|
| 725 LDObject* obj = null; |
|
| 726 |
|
| 727 for (LDObject* it : getCurrentDocument()->objects()) |
|
| 728 { |
|
| 729 if (it->qObjListEntry == listitem) |
|
| 730 { |
|
| 731 obj = it; |
|
| 732 break; |
|
| 733 } |
|
| 734 } |
|
| 735 |
|
| 736 AddObjectDialog::staticDialog (obj->type(), obj); |
|
| 737 } |
|
| 738 |
|
| 739 // ============================================================================= |
|
| 740 // |
|
| 741 bool MainWindow::save (LDDocument* doc, bool saveAs) |
|
| 742 { |
|
| 743 QString path = doc->fullPath(); |
|
| 744 |
|
| 745 if (saveAs || path.isEmpty()) |
|
| 746 { |
|
| 747 QString name = doc->defaultName(); |
|
| 748 |
|
| 749 if (!doc->fullPath().isEmpty()) |
|
| 750 name = doc->fullPath(); |
|
| 751 elif (!doc->name().isEmpty()) |
|
| 752 name = doc->name(); |
|
| 753 |
|
| 754 name.replace ("\\", "/"); |
|
| 755 path = QFileDialog::getSaveFileName (g_win, tr ("Save As"), |
|
| 756 name, tr ("LDraw files (*.dat *.ldr)")); |
|
| 757 |
|
| 758 if (path.isEmpty()) |
|
| 759 { |
|
| 760 // User didn't give a file name, abort. |
|
| 761 return false; |
|
| 762 } |
|
| 763 } |
|
| 764 |
|
| 765 if (doc->save (path)) |
|
| 766 { |
|
| 767 if (doc == getCurrentDocument()) |
|
| 768 updateTitle(); |
|
| 769 |
|
| 770 print ("Saved to %1.", path); |
|
| 771 |
|
| 772 // Add it to recent files |
|
| 773 addRecentFile (path); |
|
| 774 return true; |
|
| 775 } |
|
| 776 |
|
| 777 QString message = format (tr ("Failed to save to %1: %2"), path, strerror (errno)); |
|
| 778 |
|
| 779 // Tell the user the save failed, and give the option for saving as with it. |
|
| 780 QMessageBox dlg (QMessageBox::Critical, tr ("Save Failure"), message, QMessageBox::Close, g_win); |
|
| 781 |
|
| 782 // Add a save-as button |
|
| 783 QPushButton* saveAsBtn = new QPushButton (tr ("Save As")); |
|
| 784 saveAsBtn->setIcon (getIcon ("file-save-as")); |
|
| 785 dlg.addButton (saveAsBtn, QMessageBox::ActionRole); |
|
| 786 dlg.setDefaultButton (QMessageBox::Close); |
|
| 787 dlg.exec(); |
|
| 788 |
|
| 789 if (dlg.clickedButton() == saveAsBtn) |
|
| 790 return save (doc, true); // yay recursion! |
|
| 791 |
|
| 792 return false; |
|
| 793 } |
|
| 794 |
|
| 795 void MainWindow::addMessage (QString msg) |
|
| 796 { |
|
| 797 m_msglog->addLine (msg); |
|
| 798 } |
|
| 799 |
|
| 800 // ============================================================================ |
|
| 801 void ObjectList::contextMenuEvent (QContextMenuEvent* ev) |
|
| 802 { |
|
| 803 g_win->spawnContextMenu (ev->globalPos()); |
|
| 804 } |
|
| 805 |
|
| 806 // ============================================================================= |
|
| 807 // |
|
| 808 QPixmap getIcon (QString iconName) |
|
| 809 { |
|
| 810 return (QPixmap (format (":/icons/%1.png", iconName))); |
|
| 811 } |
|
| 812 |
|
| 813 // ============================================================================= |
|
| 814 // |
|
| 815 bool confirm (const QString& message) |
|
| 816 { |
|
| 817 return confirm (MainWindow::tr ("Confirm"), message); |
|
| 818 } |
|
| 819 |
|
| 820 // ============================================================================= |
|
| 821 // |
|
| 822 bool confirm (const QString& title, const QString& message) |
|
| 823 { |
|
| 824 return QMessageBox::question (g_win, title, message, |
|
| 825 (QMessageBox::Yes | QMessageBox::No), QMessageBox::No) == QMessageBox::Yes; |
|
| 826 } |
|
| 827 |
|
| 828 // ============================================================================= |
|
| 829 // |
|
| 830 void critical (const QString& message) |
|
| 831 { |
|
| 832 QMessageBox::critical (g_win, MainWindow::tr ("Error"), message, |
|
| 833 (QMessageBox::Close), QMessageBox::Close); |
|
| 834 } |
|
| 835 |
|
| 836 // ============================================================================= |
|
| 837 // |
|
| 838 QIcon makeColorIcon (LDColor* colinfo, const int size) |
|
| 839 { |
|
| 840 // Create an image object and link a painter to it. |
|
| 841 QImage img (size, size, QImage::Format_ARGB32); |
|
| 842 QPainter paint (&img); |
|
| 843 QColor col = colinfo->faceColor; |
|
| 844 |
|
| 845 if (colinfo->index == maincolor) |
|
| 846 { |
|
| 847 // Use the user preferences for main color here |
|
| 848 col = gl_maincolor; |
|
| 849 col.setAlphaF (gl_maincolor_alpha); |
|
| 850 } |
|
| 851 |
|
| 852 // Paint the icon border |
|
| 853 paint.fillRect (QRect (0, 0, size, size), colinfo->edgeColor); |
|
| 854 |
|
| 855 // Paint the checkerboard background, visible with translucent icons |
|
| 856 paint.drawPixmap (QRect (1, 1, size - 2, size - 2), getIcon ("checkerboard"), QRect (0, 0, 8, 8)); |
|
| 857 |
|
| 858 // Paint the color above the checkerboard |
|
| 859 paint.fillRect (QRect (1, 1, size - 2, size - 2), col); |
|
| 860 return QIcon (QPixmap::fromImage (img)); |
|
| 861 } |
|
| 862 |
|
| 863 // ============================================================================= |
|
| 864 // |
|
| 865 void makeColorComboBox (QComboBox* box) |
|
| 866 { |
|
| 867 std::map<int, int> counts; |
|
| 868 |
|
| 869 for (LDObject* obj : getCurrentDocument()->objects()) |
|
| 870 { |
|
| 871 if (!obj->isColored()) |
|
| 872 continue; |
|
| 873 |
|
| 874 if (counts.find (obj->color()) == counts.end()) |
|
| 875 counts[obj->color()] = 1; |
|
| 876 else |
|
| 877 counts[obj->color()]++; |
|
| 878 } |
|
| 879 |
|
| 880 box->clear(); |
|
| 881 int row = 0; |
|
| 882 |
|
| 883 for (const auto& pair : counts) |
|
| 884 { |
|
| 885 LDColor* col = getColor (pair.first); |
|
| 886 assert (col != null); |
|
| 887 |
|
| 888 QIcon ico = makeColorIcon (col, 16); |
|
| 889 box->addItem (ico, format ("[%1] %2 (%3 object%4)", |
|
| 890 pair.first, col->name, pair.second, plural (pair.second))); |
|
| 891 box->setItemData (row, pair.first); |
|
| 892 |
|
| 893 ++row; |
|
| 894 } |
|
| 895 } |
|
| 896 |
|
| 897 // ============================================================================= |
|
| 898 // |
|
| 899 void MainWindow::updateDocumentList() |
|
| 900 { |
|
| 901 m_updatingTabs = true; |
|
| 902 |
|
| 903 while (m_tabs->count() > 0) |
|
| 904 m_tabs->removeTab (0); |
|
| 905 |
|
| 906 for (LDDocument* f : g_loadedFiles) |
|
| 907 { |
|
| 908 // Don't list implicit files unless explicitly desired. |
|
| 909 if (f->isImplicit() && !gui_implicitfiles) |
|
| 910 continue; |
|
| 911 |
|
| 912 // Add an item to the list for this file and store the tab index |
|
| 913 // in the document so we can find documents by tab index. |
|
| 914 f->setTabIndex (m_tabs->addTab ("")); |
|
| 915 updateDocumentListItem (f); |
|
| 916 } |
|
| 917 |
|
| 918 m_updatingTabs = false; |
|
| 919 } |
|
| 920 |
|
| 921 // ============================================================================= |
|
| 922 // |
|
| 923 void MainWindow::updateDocumentListItem (LDDocument* doc) |
|
| 924 { |
|
| 925 bool oldUpdatingTabs = m_updatingTabs; |
|
| 926 m_updatingTabs = true; |
|
| 927 |
|
| 928 if (doc->tabIndex() == -1) |
|
| 929 { |
|
| 930 // We don't have a list item for this file, so the list either doesn't |
|
| 931 // exist yet or is out of date. Build the list now. |
|
| 932 updateDocumentList(); |
|
| 933 return; |
|
| 934 } |
|
| 935 |
|
| 936 // If this is the current file, it also needs to be the selected item on |
|
| 937 // the list. |
|
| 938 if (doc == getCurrentDocument()) |
|
| 939 m_tabs->setCurrentIndex (doc->tabIndex()); |
|
| 940 |
|
| 941 m_tabs->setTabText (doc->tabIndex(), doc->getDisplayName()); |
|
| 942 |
|
| 943 // If the document.has unsaved changes, draw a little icon next to it to mark that. |
|
| 944 m_tabs->setTabIcon (doc->tabIndex(), doc->hasUnsavedChanges() ? getIcon ("file-save") : QIcon()); |
|
| 945 m_updatingTabs = oldUpdatingTabs; |
|
| 946 } |
|
| 947 |
|
| 948 // ============================================================================= |
|
| 949 // |
|
| 950 // A file is selected from the list of files on the left of the screen. Find out |
|
| 951 // which file was picked and change to it. |
|
| 952 // |
|
| 953 void MainWindow::changeCurrentFile() |
|
| 954 { |
|
| 955 if (m_updatingTabs) |
|
| 956 return; |
|
| 957 |
|
| 958 LDDocument* f = null; |
|
| 959 int tabIndex = m_tabs->currentIndex(); |
|
| 960 |
|
| 961 // Find the file pointer of the item that was selected. |
|
| 962 for (LDDocument* it : g_loadedFiles) |
|
| 963 { |
|
| 964 if (it->tabIndex() == tabIndex) |
|
| 965 { |
|
| 966 f = it; |
|
| 967 break; |
|
| 968 } |
|
| 969 } |
|
| 970 |
|
| 971 // If we picked the same file we're currently on, we don't need to do |
|
| 972 // anything. |
|
| 973 if (f == null || f == getCurrentDocument()) |
|
| 974 return; |
|
| 975 |
|
| 976 LDDocument::setCurrent (f); |
|
| 977 } |
|
| 978 |
|
| 979 // ============================================================================= |
|
| 980 // |
|
| 981 void MainWindow::refreshObjectList() |
|
| 982 { |
|
| 983 #if 0 |
|
| 984 ui->objectList->clear(); |
|
| 985 LDDocument* f = getCurrentDocument(); |
|
| 986 |
|
| 987 for (LDObject* obj : *f) |
|
| 988 ui->objectList->addItem (obj->qObjListEntry); |
|
| 989 |
|
| 990 #endif |
|
| 991 |
|
| 992 buildObjList(); |
|
| 993 } |
|
| 994 |
|
| 995 // ============================================================================= |
|
| 996 // |
|
| 997 void MainWindow::updateActions() |
|
| 998 { |
|
| 999 History* his = getCurrentDocument()->history(); |
|
| 1000 int pos = his->position(); |
|
| 1001 ui->actionUndo->setEnabled (pos != -1); |
|
| 1002 ui->actionRedo->setEnabled (pos < (long) his->getSize() - 1); |
|
| 1003 ui->actionAxes->setChecked (gl_axes); |
|
| 1004 ui->actionBFCView->setChecked (gl_colorbfc); |
|
| 1005 ui->actionDrawAngles->setChecked (gl_drawangles); |
|
| 1006 } |
|
| 1007 |
|
| 1008 // ============================================================================= |
|
| 1009 // |
|
| 1010 QImage imageFromScreencap (uchar* data, int w, int h) |
|
| 1011 { |
|
| 1012 // GL and Qt formats have R and B swapped. Also, GL flips Y - correct it as well. |
|
| 1013 return QImage (data, w, h, QImage::Format_ARGB32).rgbSwapped().mirrored(); |
|
| 1014 } |
|
| 1015 |
|
| 1016 // ============================================================================= |
|
| 1017 // |
|
| 1018 LDQuickColor::LDQuickColor (LDColor* color, QToolButton* toolButton) : |
|
| 1019 m_color (color), |
|
| 1020 m_toolButton (toolButton) {} |
|
| 1021 |
|
| 1022 // ============================================================================= |
|
| 1023 // |
|
| 1024 LDQuickColor LDQuickColor::getSeparator() |
|
| 1025 { |
|
| 1026 return LDQuickColor (null, null); |
|
| 1027 } |
|
| 1028 |
|
| 1029 // ============================================================================= |
|
| 1030 // |
|
| 1031 bool LDQuickColor::isSeparator() const |
|
| 1032 { |
|
| 1033 return color() == null; |
|
| 1034 } |
|