-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMainWindow.cpp
1858 lines (1696 loc) · 62.3 KB
/
MainWindow.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*!
* @author Ferris Prima Nugraha
* @author Josef Natanael
* @author Alvin Harjanto
* @version 1.0.0
* @date 2019
* @copyright GNU Public License.
* @mainpage The Photo Editor
* @section intro_sec Introduction
* This code is developed to implement version control and multi-user support to a standard photo editing software.
* @section compile_sec Compilation
* Here I will describe how to compile this code with Qt
* 1. Load .pro project file to QtCreator
* 2. Select kits (Tested with MinGW 5.10.0 and above)
* 3. Build and run
* @class MainWindow
* @brief Central hub for all functions and classes in this application.
*/
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QtWidgets>
#include <QRubberBand>
#include "FilterTransform/NonKernelBased/MagicWand.h"
#include <QJsonObject>
#include "WorkspaceArea.h"
#include "Palette/BasicControls.h"
#include "Palette/Brush.h"
#include "Palette/Histogram.h"
#include "Palette/ColorControls.h"
#include "Palette/Effects.h"
#include "FilterTransform/NonKernelBased/GrayscaleFilter.h"
#include "FilterTransform/NonKernelBased/InvertFilter.h"
#include "FilterTransform/NonKernelBased/HueFilter.h"
#include "FilterTransform/NonKernelBased/SaturationFilter.h"
#include "FilterTransform/NonKernelBased/TemperatureFilter.h"
#include "FilterTransform/NonKernelBased/TintFilter.h"
#include "FilterTransform/NonKernelBased/BrightnessFilter.h"
#include "FilterTransform/NonKernelBased/ContrastFilter.h"
#include "FilterTransform/NonKernelBased/ExposureFilter.h"
#include "FilterTransform/NonKernelBased/ClockwiseRotationTransform.h"
#include "FilterTransform/NonKernelBased/CounterClockwiseRotationTransform.h"
#include "FilterTransform/NonKernelBased/FlipHorizontalTransform.h"
#include "FilterTransform/NonKernelBased/FlipVerticalTransform.h"
#include "FilterTransform/KernelBased/GaussianBlurFilter.h"
#include "FilterTransform/KernelBased/MeanBlurFilter.h"
#include "FilterTransform/KernelBased/EmbossFilter.h"
#include "FilterTransform/KernelBased/EdgeDetectionFilter.h"
#include "FilterTransform/KernelBased/ImageInpainting.h"
#include "FilterTransform/KernelBased/ImageScissors.h"
#include "ServerRoom.h"
/**
* @brief Construct a new Main Window::MainWindow object.
*
* @param parent to be passed to QMainWindow(parent) constructor.
*/
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
/*
* Adds a toolbar on runtime, layed out vertically, aligned to the right
*/
// 1. Set the toolbar dimensions
ui->toolBar->setIconSize(QSize(55, 55));
ui->toolBar->setFixedHeight(60);
ui->toolBar->setContentsMargins(30, 0, 30, 0);
ui->toolBar->setStyleSheet("QToolBar{spacing:30px;}");
// 2. Setup the toolbar alignment
QWidget *spacerWidget = new QWidget(this);
spacerWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
spacerWidget->setVisible(true);
// 3. Adds actions to the toolbar
ui->toolBar->addWidget(spacerWidget);
ui->toolBar->addAction(ui->actionNew);
ui->toolBar->addSeparator();
ui->toolBar->addAction(ui->actionOpen);
ui->toolBar->addSeparator();
ui->toolBar->addAction(ui->actionSave);
ui->toolBar->addSeparator();
ui->toolBar->addAction(ui->actionCommit_Changes);
// Setup our actions shortcuts
ui->actionNew->setShortcuts(QKeySequence::New);
ui->actionOpen->setShortcuts(QKeySequence::Open);
ui->actionSave->setShortcuts(QKeySequence::Save);
ui->actionUndo->setShortcuts(QKeySequence::Undo);
ui->actionRedo->setShortcuts(QKeySequence::Redo);
ui->actionPrint->setShortcuts(QKeySequence::Print);
ui->actionExit->setShortcuts(QKeySequence::Quit);
///////////////////////////////////////////////////////////////////////////////////////////
// Spawns a graphicsView
graphicsView = new QGraphicsView(this);
// Spawns a WorkspaceArea
workspaceArea = new WorkspaceArea();
// Adds the workspaceArea into our graphicsView
graphicsView->setScene(workspaceArea);
ui->workspaceView->addWidget(graphicsView);
workspaceArea->setParent(graphicsView);
graphicsView->scene()->installEventFilter(this);
// Sets the graphicsViewBoundaries at launch, to the default width and height of the workspaceArea
resizeGraphicsViewBoundaries(WorkspaceArea::SCENE_WIDTH, WorkspaceArea::SCENE_HEIGHT);
///////////////////////////////////////////////////////////////////////////////////////////
// Create additional actions and menus
createActions();
createMenus();
// Setup zoom options
comboBox = new QComboBox(this);
comboBox->addItem("50%");
comboBox->addItem("100%");
comboBox->addItem("120%");
comboBox->addItem("Fit to screen");
comboBox->setCurrentText("100%");
ui->statusBar->addWidget(comboBox);
///////////////////////////////////////////////////////////////////////////////////////////
// Setup a treeWidget, which are the menus in our palette
ui->palette->setColumnCount(1);
histogram = new QTreeWidgetItem(ui->palette);
basicControls = new QTreeWidgetItem(ui->palette);
colorControls = new QTreeWidgetItem(ui->palette);
brushControls = new QTreeWidgetItem(ui->palette);
effects = new QTreeWidgetItem(ui->palette);
/*
* Fills in our treeWidget palette with all the widgets it has
*/
// 1. Adds a Histogram widget to the treeWidget palette
addRoot(histogram, "Histogram");
histo = new Histogram();
customAddChild(histogram, histo);
// 2. Adds a BasicControls widget to the treeWidget palette
addRoot(basicControls, "Basic Controls");
basics = new BasicControls();
customAddChild(basicControls, basics);
// 3. Adds a ColorControls widget to the treeWidget palette
addRoot(colorControls, "Color");
colors = new ColorControls();
customAddChild(colorControls, colors);
// 4. Adds a BrushControls widget to the treeWidget palette
addRoot(brushControls, "Brush");
brush = new Brush();
customAddChild(brushControls, brush);
// TODO 5. Adds an Effects widget to the treeWidget palette
addRoot(effects, "Effects");
effect = new Effects();
customAddChild(effects, effect);
///////////////////////////////////////////////////////////////////////////////////////////
// Setup all our signal and slots
reconnectConnection(); // Workspace related connection
connect(basics, &BasicControls::crossCursorChanged, this, &MainWindow::onCrossCursorChanged); // Cursor change connection
connect(basics, &BasicControls::applyTransformClicked, this, &MainWindow::applyFilterTransform); // Image transformation connection to basic controls
connect(colors, &ColorControls::applyColorFilterClicked, this, &MainWindow::applyFilterTransform); // Image filters connection to color controls
connect(effect, &Effects::applyEffectClicked, this, &MainWindow::applyFilterTransform); // Image effects connection to effects widget
connect(comboBox, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(onZoom(const QString &))); // Zoom level change connection
connect(colors, &ColorControls::applyColorFilterOnPreview, this, &MainWindow::applyFilterTransformOnPreview);
}
/**
* @brief Destroy the Main Window::MainWindow object.
*
*/
MainWindow::~MainWindow()
{
delete ui;
delete temporaryArea;
}
/**
* @brief Setup all connections related to the workspaceArea.
* @details This is need in order to reestablish connection of signal and slots to the a new workspaceArea, i.e. workspaceArea is recreated.
*/
void MainWindow::reconnectConnection()
{
connect(workspaceArea, &WorkspaceArea::imageDrawn, this, &MainWindow::onImageDrawn);
connect(workspaceArea, &WorkspaceArea::imageLoaded, histo, &Histogram::onImageLoaded);
connect(brush, &Brush::penColorChanged, workspaceArea, &WorkspaceArea::setPenColor);
connect(brush, &Brush::penWidthChanged, workspaceArea, &WorkspaceArea::setPenWidth);
connect(workspaceArea, &WorkspaceArea::imageCropped, this, &MainWindow::rerenderWorkspaceArea);
connect(basics, &BasicControls::resizeButtonClicked, workspaceArea, &WorkspaceArea::resizeImage);
connect(workspaceArea, &WorkspaceArea::imageResized, this, &MainWindow::rerenderWorkspaceArea);
connect(workspaceArea, &WorkspaceArea::sendResize, this, &MainWindow::onSendResize);
connect(workspaceArea, &WorkspaceArea::sendCrop, this, &MainWindow::onSendCrop);
connect(workspaceArea, &WorkspaceArea::sendCropWithMagicWand, this, &MainWindow::onSendCropWithMagicWand);
connect(workspaceArea, &WorkspaceArea::sendMoveScribble, this, &MainWindow::onSendMoveScribble);
connect(workspaceArea, &WorkspaceArea::sendReleaseScribble, this, &MainWindow::onSendReleaseScribble);
connect(workspaceArea, &WorkspaceArea::updateImagePreview, this, &MainWindow::onUpdateImagePreview);
connect(workspaceArea, &WorkspaceArea::commitChanges, this, &MainWindow::onCommitChanges);
}
/**
* @brief Reconstruct workspaceArea when changes are made to the image.
*
* @details Delete the current workspaceArea and recreates it with the new image dimensions,
* Then resetup our graphicsView.
*
* @param imageWidth reconstruct the workspaceArea with width imageWidth.
* @param imageHeight reconstruct the workspaceArea with height imageHeight.
*/
void MainWindow::reconstructWorkspaceArea(int imageWidth, int imageHeight)
{
delete workspaceArea;
workspaceArea = nullptr;
workspaceArea = new WorkspaceArea(imageWidth, imageHeight);
reconnectConnection();
// Re-Setup graphicsView.
graphicsView->setScene(workspaceArea);
ui->workspaceView->addWidget(graphicsView);
workspaceArea->setParent(graphicsView);
graphicsView->scene()->installEventFilter(this);
}
/**
* @brief Resize graphics view dimensions and boundaries, to fit the workspaceArea.
*
* @details Resize our graphicsView dimensions to a specified width and height.
* This is done usually after we reconstruct our workspaceArea.
*
* @param newWidth
* @param newHeight
*/
void MainWindow::resizeGraphicsViewBoundaries(int newWidth, int newHeight)
{
graphicsView->setFixedSize(newWidth, newHeight);
graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
graphicsView->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
graphicsView->setFocusPolicy(Qt::NoFocus);
graphicsView->setSceneRect(0, 0, newWidth - 2, newHeight - 2); // Allow for extra 2px boundaries, 1px on the left/top and 1px on the right/bottom.
}
/**
* @brief Adds a root into a parent QTreeWidgetItem, located in the palette QTreeWidget.
*
* @param parent QTreeWidgetItem to insert to.
* @param name Name of the root.
*/
void MainWindow::addRoot(QTreeWidgetItem *parent, QString name)
{
parent->setText(0, name);
ui->palette->addTopLevelItem(parent);
}
/**
* @brief Custom implementation of the addChild function of a QTreeWidgetItem
*
* @details Adds a child to a parent, the child is a QWidget.
*
* @param parent The parent QTreeWidgetItem to insert child to.
* @param widget The QWidget/child.
*/
void MainWindow::customAddChild(QTreeWidgetItem *parent, QWidget *widget)
{
QTreeWidgetItem *item = new QTreeWidgetItem();
parent->addChild(item);
ui->palette->setItemWidget(item, 0, widget);
}
/**
* @brief Spawns a "do you want to save" widget when closing the app.
*
* @param event "Save" or "Discard" event.
*/
void MainWindow::closeEvent(QCloseEvent *event)
{
if (maybeSave())
{
event->accept(); // If no changes have been made and the app closes
}
else
{
event->ignore(); // If there have been changes ignore the event
}
}
/**
* @brief Called when the user clicks Save As in the menu.
*
* @details This function will call the saveAsfile function to save the image with the right format.
* Default format is "jpg".
*/
void MainWindow::saveAs()
{
QAction *action = qobject_cast<QAction *>(sender()); // Represents the action of the user clicking.
QByteArray defaultFormat("jpg", 3); // Sets default format.
fileFormat = action->data().toByteArray(); // Stores the array of bytes of the users data.
if (fileFormat.isEmpty())
{
fileFormat = defaultFormat;
}
saveAsFile(fileFormat); // Pass fileFormat to be saved.
}
/**
* @brief Clears the workspaceArea.
*
* @details this will not invoke maybeSave(), nor commit changes to version control.
*/
void MainWindow::clearImage()
{
reconstructWorkspaceArea(workspaceArea->getImageWidth(), workspaceArea->getImageHeight());
workspaceArea->setModified(true);
update();
workspaceArea->setImageLoaded(false);
}
/**
* @brief Create menu actions for SaveAs and clearScreen.
*/
void MainWindow::createActions()
{
// Get a list of the supported file formats
// QImageWriter is used to write images to files
foreach (QByteArray format, QImageWriter::supportedImageFormats())
{
QString text = tr("%1...").arg(QString(format).toUpper());
QAction *action = new QAction(text, this); // Create an action for each file format
action->setData(format); // Set an action for each file format
saveAsActs.append(action); // Attach each file format option menu item to Save As
connect(action, SIGNAL(triggered()), this, SLOT(saveAs()));
}
// Create clear screen action and tie to MainWindow::clearImage()
clearScreenAct = new QAction(tr("&Clear Screen"), this);
clearScreenAct->setShortcut(tr("Ctrl+L"));
connect(clearScreenAct, SIGNAL(triggered()), this, SLOT(on_actionNew_triggered()));
}
/**
* @brief Generate image history/version control in the menu bar.
*/
void MainWindow::generateHistoryMenu()
{
// Clean image history menu
for (QMenu *_ : imageHistoryMenu)
{
delete imageHistoryMenu[0];
imageHistoryMenu.pop_front();
}
// Traverse the imageHistory (Version Control)
QLinkedList<VersionControl::MasterNode>::iterator it = imageHistory.masterBranch.begin();
// 1a. Traverse nodes in master branch
for (int masterNodeNumber = 0; it != imageHistory.masterBranch.end(); ++it, ++masterNodeNumber)
{
// 1b. Create a menu for each master node
QMenu *masterNodeMenu = new QMenu(it->changes, this);
// 2a. Traverse images in side branches
QLinkedList<VersionControl::SideNode>::iterator i = it->sideBranch.begin();
for (int sideNodeNumber = 0; i != it->sideBranch.end(); ++i, ++sideNodeNumber)
{
// 2b. Create an action for each image
QAction *action = new QAction(i->changes, masterNodeMenu);
masterNodeMenu->addAction(action);
connect(action, &QAction::triggered, this, [=]() { checkoutCommit(masterNodeNumber, sideNodeNumber, true); });
}
// 3. Add the menu to our menuHistory
ui->menuHistory->addMenu(masterNodeMenu);
imageHistoryMenu.append(masterNodeMenu);
}
}
/**
* @brief Checkout a commit. A commit is differentiated by their masterNodeNumber and sideNodeNumber.
*
* @param masterNodeNumber Which branch in the master branch.
* @param sideNodeNumber Which commit in the branch.
*/
void MainWindow::checkoutCommit(int masterNodeNumber, int sideNodeNumber, bool fromActionMenu)
{
this->masterNodeNumber = masterNodeNumber;
this->sideNodeNumber = sideNodeNumber;
// Go through the imageHistory (Version Control)
QLinkedList<VersionControl::MasterNode>::iterator it = imageHistory.masterBranch.begin();
// 1. Go through nodes in master branch
it += masterNodeNumber;
// 2. Go through images in the side branch
QLinkedList<VersionControl::SideNode>::iterator it2 = it->sideBranch.begin();
it2 += sideNodeNumber;
rerenderWorkspaceArea(it2->currentImage, it2->currentImage.width(), it2->currentImage.height());
if (fromActionMenu) {
sendVersion("checkoutCommit", masterNodeNumber, sideNodeNumber);
}
}
/**
* @brief Commit changes to the version control/image history.
*
* @param changedImage Changed image.
* @param changes Description of the commit.
*/
void MainWindow::commitChanges(QImage changedImage, QString changes)
{
if (masterNodeNumber == 0) // Commit changes to master branch.
{
imageHistory.commitChanges(changedImage, changes);
}
else // Commit changes to current branch (i.e. not master branch).
{
imageHistory.getMasterNodeIteratorAtIndex(masterNodeNumber)->commitChanges(changedImage, changes);
}
generateHistoryMenu(); // update history menu.
}
/**
* @brief Create additional menus for certain actions, i.e. clearScreen action.
*/
void MainWindow::createMenus()
{
// Create Save As option and the list of file types
foreach (QAction *action, saveAsActs)
ui->menuSave_As->addAction(action);
// Attach all actions to Options
optionMenu = new QMenu(tr("&Options"), this);
optionMenu->addAction(clearScreenAct);
menuBar()->addMenu(optionMenu);
}
/**
* @brief Prompt user to save the file when user tried to change image/quit application.
*
* @return true There has been changes/modifications.
* @return false There was no changes/modifications made.
*/
bool MainWindow::maybeSave()
{
// Check for changes since last save
if (workspaceArea->isModified())
{
QMessageBox::StandardButton ret;
ret = QMessageBox::warning(this, tr("Scribble"),
tr("The image has been modified.\n"
"Do you want to save your changes?"),
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
if (ret == QMessageBox::Save)
{
return saveAsFile("png");
}
else if (ret == QMessageBox::Cancel)
{
return false;
}
}
return true;
}
/**
* @brief Saves the current image with the specified fileFormat.
*
* @param fileFormat
* @return true Image successfully saved.
* @return false Image not saved.
*/
bool MainWindow::saveAsFile(const QByteArray &fileFormat)
{
QString initialPath = QDir::currentPath() + "/untitled." + fileFormat;
// Add the proper file formats and extensions
fileName = QFileDialog::getSaveFileName(this, tr("Save As"),
initialPath,
tr("%1 Files (*.%2);;All Files (*)")
.arg(QString::fromLatin1(fileFormat.toUpper()))
.arg(QString::fromLatin1(fileFormat)));
if (fileName.isEmpty())
{
return false;
}
else
{
bool saved = workspaceArea->saveImage(fileName, fileFormat.constData()); // Call for the file to be saved
if (saved)
{
fileSaved = true;
}
else
{
fileSaved = false;
}
return saved;
}
}
/**
* @brief Clears image from the workspaceArea;
*
* @details Note the difference between this function to clearImage();
* This function will prompt the user to save changes made to image, if there has been unsaved changes.
*/
void MainWindow::on_actionNew_triggered()
{
if (isConnected && !isHost) {
QMessageBox::information(this, QString("Not host"), QString("You are not host. Please disconnect to create new image."));
return;
}
if (maybeSave())
{
clearImage();
}
workspaceArea->setModified(false);
sendClearScreen();
}
/**
* @brief Opens a new image.
*
* @details Checks whether the current image has been modified, and prompt "do you want to save".
* Otherwise open an open file dialog to open the image.
*/
void MainWindow::on_actionOpen_triggered()
{
if (isConnected && !isHost) {
QMessageBox::information(this, QString("Not host"), QString("You are not host. Please disconnect to open your own image."));
return;
}
if (maybeSave())
{ // Check if changes have been made since last save
// Get the file to open from a dialog
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::currentPath());
QImage loadedImage;
if (!fileName.isEmpty())
{
if (!loadedImage.load(fileName))
{
return;
}
this->fileName = fileName;
fileSaved = false;
// Reads image dimensions
QImageReader reader(fileName);
QSize sizeOfImage = reader.size();
int imageHeight = sizeOfImage.height();
int imageWidth = sizeOfImage.width();
// Recontruct our graphicsView and workspaceArea, according to the new image dimensions
resetGraphicsViewScale();
reconstructWorkspaceArea(imageWidth, imageHeight);
// Updating data members that keeps image width and height
resizedImageWidth = imageWidth;
resizedImageHeight = imageHeight;
basics->setImageDimensions(imageWidth, imageHeight);
// Setup our workspace
workspaceArea->openImage(loadedImage, imageWidth, imageHeight);
resizeGraphicsViewBoundaries(imageWidth, imageHeight);
fitImageToScreen(imageWidth, imageHeight);
// Setup image preview, which is situated in color controls
colors->setImagePreview(workspaceArea->commitImageForPreview());
colors->resetSliders();
// Commit changes to version control.
commitChanges(loadedImage, "Original Image");
sendInitialImage();
}
}
}
/**
* @brief Handle file saves.
*
* @details This slot will try to save the file, instead of saveAs the file, if it was once saved.
*/
void MainWindow::on_actionSave_triggered()
{
if (fileSaved)
{
bool saved = workspaceArea->saveImage(fileName, fileFormat.constData()); // Call for the file to be saved
if (saved)
{
fileSaved = true;
}
else
{
fileSaved = false;
}
}
else
{
saveAs();
}
}
/**
* @brief Handle undo actions.
*
* @details Checkout a commit based on current checkout position (i.e. sideNodenumber and masterNodeNumber)
*/
void MainWindow::on_actionUndo_triggered()
{
undo();
sendVersion("undo");
}
/**
* @brief Undo implementation
*
* @details Checks the masterNodeNumber and sideNodeNumber to perform suitable checkoutCommit
*/
void MainWindow::undo()
{
// sideNodeNumber > 0 means we are in a sideBranch, sideBranchLength > 1 means we are also in a sideBranch
if (sideNodeNumber > 0 || imageHistory.getMasterNodeIteratorAtIndex(masterNodeNumber)->getBranchLength() > 1)
{
// Node starts at 0, +2 because we want to compare length and if the image is undoable
if (sideNodeNumber + 2 <= imageHistory.getMasterNodeIteratorAtIndex(masterNodeNumber)->getBranchLength())
checkoutCommit(masterNodeNumber, ++sideNodeNumber);
}
else if (masterNodeNumber + 2 <= imageHistory.getBranchLength())
{
checkoutCommit(++masterNodeNumber, 0);
}
}
/**
* @brief Handle redo actions.
*
* @details Checkout a commit based on current checkout position (i.e. sideNodenumber and masterNodeNumber)
*/
void MainWindow::on_actionRedo_triggered()
{
redo();
sendVersion("redo");
}
/**
* @brief Redo implementation
*
* @details Checks the masterNodeNumber and sideNodeNumber to perform suitable checkoutCommit
*/
void MainWindow::redo()
{
// Check if redo is available on current branch
if (sideNodeNumber > 0)
{
checkoutCommit(masterNodeNumber, --sideNodeNumber);
}
else if (masterNodeNumber > 0)
{
checkoutCommit(--masterNodeNumber, 0);
}
}
/**
* @brief Revert commit to last commit.
*/
void MainWindow::on_actionRevert_to_Last_Commit_triggered()
{
revertToLastCommit();
sendVersion("revertCommit");
}
/**
* @brief Revert commit to last commit implementation
*
* @details Checks the masterNodeNumber and sideNodeNumber
* and check for imageHistory
* whether it is possible to perform suitable checkoutCommit and reverseCommit
*/
void MainWindow::revertToLastCommit()
{
if (!imageHistory.getBranchLength())
return;
// sideNodeNumber > 0 means we are in a sideBranch, sideBranchLength > 1 means we are also in a sideBranch
if (sideNodeNumber > 0 || imageHistory.getMasterNodeIteratorAtIndex(masterNodeNumber)->getBranchLength() > 1)
{
// Node starts at 0, +2 because we want to compare length and if the image is revertible
if (sideNodeNumber + 2 <= imageHistory.getMasterNodeIteratorAtIndex(masterNodeNumber)->getBranchLength())
{
checkoutCommit(masterNodeNumber, sideNodeNumber + 1);
imageHistory.getMasterNodeIteratorAtIndex(masterNodeNumber)->reverseCommit();
--sideNodeNumber;
}
}
else if (masterNodeNumber + 2 <= imageHistory.getBranchLength())
{
checkoutCommit(masterNodeNumber + 1, 0);
imageHistory.reverseCommit();
--masterNodeNumber;
}
else
{
return;
}
generateHistoryMenu();
}
/**
* @brief Prints the workspace area.
*
* @details calls workspaceArea's print function.
*/
void MainWindow::on_actionPrint_triggered()
{
on_actionSave_triggered();
workspaceArea->print();
}
/**
* @brief Shows About us dialog when slot is triggered.
*/
void MainWindow::on_actionAbout_Us_triggered()
{
AboutUs aboutUs;
aboutUs.setModal(true);
aboutUs.exec();
}
/**
* @brief Updates image on image drawn/scribbled.
*
* @details commits changes to version control.
*/
void MainWindow::onImageDrawn()
{
QImage image = workspaceArea->commitImage();
commitChanges(image, "Brush");
}
/**
* @brief Filter mouse scroll event to not propagate to the whole mainwindow, instead only to the workspaceArea.
*
* @details Mouse scroll/wheel event will be handled by MainWindow::handleWheelEvent(QGraphicsSceneWheelEvent*).
*
* @param event
* @return true event is a GraphicsSceneWheel.
* @return false event is not a GraphicsSceneWheel.
*/
bool MainWindow::eventFilter(QObject *, QEvent *event)
{
if (event->type() == QEvent::GraphicsSceneWheel)
{
handleWheelEvent(static_cast<QGraphicsSceneWheelEvent *>(event));
// Don't propagate event to the whole mainwindow
event->accept();
return true;
}
return false;
}
/**
* @brief Zooms in/out the workspaceArea/graphicsView on mouse scroll/wheel event
*
* @param event Scene wheel event.
*/
void MainWindow::handleWheelEvent(QGraphicsSceneWheelEvent *event)
{
// Sets how the view should position during scene transformations
graphicsView->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
// Sets up the zooming properties,
// Saves the current screen size in a QRect for the upper bound of zooming
const double scaleFactor = 1.1;
const QRect screenSize = WindowHelper::screenFromWidget(qApp->desktop())->geometry();
const int upperBound = qMin(screenSize.width(), screenSize.height());
const int lowerBound = 100;
/*
* Register a scroll if the scroll is vertical, within 7 (or -7) degree
* with maximum workspaceArea dimensions equal to the upperbound
* and minimum workspaceArea dimensions to the lowerbound
*/
if (event->orientation() == Qt::Vertical)
{
if (event->delta() > 7 && resizedImageWidth < upperBound && resizedImageHeight < upperBound)
{
// Update resizedImage dimensions
graphicsView->scale(scaleFactor, scaleFactor);
resizeGraphicsViewBoundaries(static_cast<int>(resizedImageWidth * scaleFactor), static_cast<int>(resizedImageHeight * scaleFactor));
resizedImageWidth = resizedImageWidth * scaleFactor;
resizedImageHeight = resizedImageHeight * scaleFactor;
currentZoom *= scaleFactor;
}
else if (event->delta() < -7 && resizedImageWidth > lowerBound && resizedImageHeight > lowerBound)
{
graphicsView->scale(1.0 / scaleFactor, 1.0 / scaleFactor);
resizeGraphicsViewBoundaries(static_cast<int>(resizedImageWidth / scaleFactor), static_cast<int>(resizedImageHeight / scaleFactor));
currentZoom /= scaleFactor;
resizedImageWidth = resizedImageWidth * (1.0 / scaleFactor);
resizedImageHeight = resizedImageHeight * (1.0 / scaleFactor);
}
}
// Scrolls the contents of the view to ensure the item(workspaceArea) will always be fully visible in the view
graphicsView->centerOn(0, 0);
graphicsView->setAlignment(Qt::AlignTop | Qt::AlignLeft);
}
/**
* @brief Changes workspaceArea zoom level based on level.
*
* @param level The level of zoom desired.
*/
void MainWindow::onZoom(const QString &level)
{
if (level == "Fit to screen")
{
fitImageToScreen(static_cast<int>(resizedImageWidth), static_cast<int>(resizedImageHeight));
return;
}
// Neutralizes any zoom changes made before.
double originalFactor = 1.0 / currentZoom;
double a = resizedImageWidth * originalFactor;
double b = resizedImageHeight * originalFactor;
graphicsView->scale(a / resizedImageWidth, b / resizedImageHeight);
resizeGraphicsViewBoundaries(static_cast<int>(resizedImageWidth * originalFactor), static_cast<int>(resizedImageHeight * originalFactor));
resizedImageWidth *= originalFactor;
resizedImageHeight *= originalFactor;
graphicsView->setAlignment(Qt::AlignTop | Qt::AlignLeft);
double scaleFactor = 1.0;
if (level == "50%")
{
scaleFactor = 0.5;
currentZoom = 0.5;
}
else if (level == "100%")
{
scaleFactor = 1.0;
currentZoom = 1.0;
}
else if (level == "120%")
{
scaleFactor = 1.2;
currentZoom = 1.2;
}
// Setup scaling settings to graphicsView settings.
a = resizedImageWidth * scaleFactor;
b = resizedImageHeight * scaleFactor;
graphicsView->scale(a / resizedImageWidth, b / resizedImageHeight);
resizeGraphicsViewBoundaries(static_cast<int>(resizedImageWidth * scaleFactor), static_cast<int>(resizedImageHeight * scaleFactor));
resizedImageWidth *= scaleFactor;
resizedImageHeight *= scaleFactor;
graphicsView->setAlignment(Qt::AlignTop | Qt::AlignLeft);
}
/**
* @brief Updates workspaceArea's cursor.
*
* @details This slot receives basic controls' cursor change signals.
* Drawing/cropping/cutting an image will be signaled by the change of cursor state.
*
* @param cursor a WorkspaceArea::CursorMode cursor.
* @param data Any integer data that is needed for certain cursor, e.g. Magic Wand threshold.
*/
void MainWindow::onCrossCursorChanged(WorkspaceArea::CursorMode cursor, int data)
{
graphicsView->setCursor(Qt::CrossCursor);
switch (cursor)
{
case WorkspaceArea::CursorMode::RECTANGLECROP:
workspaceArea->setCursorMode(WorkspaceArea::CursorMode::RECTANGLECROP);
break;
case WorkspaceArea::CursorMode::MAGICWAND:
workspaceArea->setCursorMode(WorkspaceArea::CursorMode::MAGICWAND);
workspaceArea->setMagicWandThreshold(data);
break;
case WorkspaceArea::CursorMode::SCRIBBLE:
graphicsView->setCursor(Qt::ArrowCursor);
workspaceArea->setCursorMode(WorkspaceArea::CursorMode::SCRIBBLE);
break;
}
}
/**
* @brief Fits the image to screen.
*
* @details If any of the currentImage dimensions is larger than the screen dimensions, we would like to scale down.
* Else if any of the currentImage dimensions is smaller than the screen dimensions, we would like to scale up.
* Otherwise, both dimensions are equal, we can leave the function.
*
* @param currentImageWidth
* @param currentImageHeight
*/
void MainWindow::fitImageToScreen(int currentImageWidth, int currentImageHeight)
{
const QRect screenRect = WindowHelper::screenFromWidget(qApp->desktop())->geometry();
int screenWidth = screenRect.width();
int screenHeight = screenRect.height();
double ratio;
if (currentImageWidth > screenWidth || currentImageHeight > screenHeight)
{
ratio = qMax(static_cast<double>(screenWidth) / currentImageWidth, static_cast<double>(screenHeight) / currentImageHeight) * 0.5;
}
else if (currentImageWidth < screenWidth || currentImageHeight < screenHeight)
{
ratio = qMin(static_cast<double>(screenWidth) / currentImageWidth, static_cast<double>(screenHeight) / currentImageHeight) * 0.5;
}
else
{
return;
}
graphicsView->scale(ratio, ratio);
resizeGraphicsViewBoundaries(static_cast<int>(resizedImageWidth * ratio), static_cast<int>(resizedImageHeight * ratio));
resizedImageWidth *= ratio;
resizedImageHeight *= ratio;
currentZoom *= ratio;
graphicsView->setAlignment(Qt::AlignTop | Qt::AlignLeft);
comboBox->setCurrentText("Fit to screen");
}
/**
* @brief Rerenders the workspace area.
*
* @details Mechanism to view the cropped/filtered/transformed image:
* 1. store current workspaceArea to a temporaryArea
* 2. create a new workspaceArea to hold the new cropped image, and set the graphicsView to contain this new workspaceArea
* 3. the temporaryArea will then be destroyed when a) destroyed by the destructor, or b) there is a new signal to crop the image.
* We cannot destroy/reconstruct the workspaceArea in this slot, as this slot is connected (signal-slot connection) to the caller signal in the workspaceArea.
* @param image Image to be rendered.
* @param imageWidth Width of the image.
* @param imageHeight Height of the image.
*/
void MainWindow::rerenderWorkspaceArea(const QImage &image, int imageWidth, int imageHeight)
{
resetGraphicsViewScale();
// Removes previous temporaryArea. If temporaryArea is not nullptr,
// this means temporaryArea holds the previous workspaceArea.
delete temporaryArea;
// Let the temporaryArea hold our current workspaceArea
temporaryArea = workspaceArea;
// Create a new workspaceArea to hold the newly cropped image, while setting up all the connections needed.
workspaceArea = new WorkspaceArea(imageWidth, imageHeight);
reconnectConnection();
// Contain the new workspaceArea into our graphicsView
graphicsView->setScene(workspaceArea);
ui->workspaceView->addWidget(graphicsView);
workspaceArea->setParent(graphicsView);
graphicsView->scene()->installEventFilter(this);
// Update our data members
resizedImageWidth = imageWidth;
resizedImageHeight = imageHeight;
// Actually open the cropped image
workspaceArea->openImage(image, imageWidth, imageHeight);
resizeGraphicsViewBoundaries(imageWidth, imageHeight);
fitImageToScreen(imageWidth, imageHeight);
// Reset crop buttons state (make buttons pressable after crop is finished)
basics->on_cancelCutoutPushButton_clicked();
basics->setImageDimensions(imageWidth, imageHeight);
colors->setImagePreview(workspaceArea->commitImageForPreview());
colors->resetSliders();
onUpdateImagePreview();
}