Commit e114d95e authored by ahb's avatar ahb

Integrate cvv module: Interactive Visual Debugging of Computer Vision applications

parent 47131fd1
.. _Table-Of-Content-CVV:
*cvv* module. GUI for Interactive Visual Debugging
--------------------------------------------------
Here you will learn how to use the cvv module to ease programming computer vision software through visual debugging aids.
.. include:: ../../definitions/tocDefinitions.rst
+
.. tabularcolumns:: m{100pt} m{300pt}
.. cssclass:: toctableopencv
=============== ======================================================
|cvvIntro| *Title:* :ref:`Visual_Debugging_Introduction`
*Compatibility:* > OpenCV 2.4.8
*Author:* |Author_Bihlmaier|
We will learn how to debug our applications in a visual and interactive way.
=============== ======================================================
.. |cvvIntro| image:: images/Visual_Debugging_Introduction_Tutorial_Cover.jpg
:height: 90pt
:width: 90pt
.. raw:: latex
\pagebreak
.. toctree::
:hidden:
../visual_debugging_introduction/visual_debugging_introduction
---
AccessModifierOffset: -2
ConstructorInitializerIndentWidth: 4
AlignEscapedNewlinesLeft: false
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakTemplateDeclarations: false
AlwaysBreakBeforeMultilineStrings: false
BreakBeforeBinaryOperators: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BinPackParameters: true
ColumnLimit: 80
ConstructorInitializerAllOnOneLineOrOnePerLine: false
DerivePointerBinding: false
ExperimentalAutoDetectBinPacking: false
IndentCaseLabels: false
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 60
PenaltyBreakString: 1000
PenaltyBreakFirstLessLess: 120
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerBindsToType: false
SpacesBeforeTrailingComments: 1
Cpp11BracedListStyle: false
Standard: Cpp11
IndentWidth: 8
TabWidth: 8
UseTab: ForIndentation
BreakBeforeBraces: Allman
IndentFunctionDeclarationAfterType: false
SpacesInParentheses: false
SpacesInAngles: false
SpaceInEmptyParentheses: false
SpacesInCStyleCastParentheses: false
SpaceAfterControlStatementKeyword: true
SpaceBeforeAssignmentOperators: true
ContinuationIndentWidth: 4
...
build/
CMakeLists.txt.user
.ycm_extra_conf.py
.ycm_extra_conf.pyc
test.sh
release.sh
*.swp
*.swo
src/dbg/dbg.hpp
*~
if(NOT HAVE_QT5)
ocv_module_disable(cvv)
return()
endif()
# we need C++11 and want warnings:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wextra -pedantic")
ocv_warnings_disable(CMAKE_CXX_FLAGS -Wshadow)
# Qt5
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
foreach(dt5_dep Core Gui Widgets)
add_definitions(${Qt5${dt5_dep}_DEFINITIONS})
include_directories(${Qt5${dt5_dep}_INCLUDE_DIRS})
list(APPEND CVV_LIBRARIES ${Qt5${dt5_dep}_LIBRARIES})
endforeach()
set(the_description "Debug visualization framework")
ocv_define_module(cvv opencv_core opencv_imgproc opencv_features2d ${CVV_LIBRARIES})
Copyright (c) 2013/2014 Johannes Bechberger
Copyright (c) 2013/2014 Erich Bretnütz
Copyright (c) 2013/2014 Nikolai Gaßner
Copyright (c) 2013/2014 Raphael Grimm
Copyright (c) 2013/2014 Clara Scherer
Copyright (c) 2013/2014 Florian Weber
Copyright (c) 2013/2014 Andreas Bihlmaier
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name CVVisual nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
CVVisual
========
#Über CVVisual
CVVisual ist eine Debug-Bibliothek für OpenCV, die verschiedene Möglichkeiten der Darstellung von Bildern und Ergebnissen von beispielsweise Filter- und Match-Operationen von OpenCV anbietet.
##Benutzung: Beispiel
Ist die Bibliothek eingebunden, das CVVISUAL\_DEBUG-Makro definiert und die benötigten Header in den Code eingebunden, kann durch den Aufruf einer CVVisual-Funktion mit den von OpenCV gelieferten Daten als Argumenten das CVV-Hauptfenster geöffnet werden.
Beispielsweise könnte ein Codestück folgendermaßen aussehen:
//...
cvv::debugDMatch(src, keypoints1, src, keypoints2, match, CVVISUAL\_LOCATION);
![](../images_ueberblick/MainWindow.PNG)
Die Bilder werden zusammen mit Informationen und Metadaten in der Overview-Tabelle angezeigt.
Ein Doppelklick darauf öffnet ein Tab, in dem die Bilder und Matches groß angezeigt werden.
![](../images_ueberblick/LineMatchViewTab.PNG)
In dieser Ansicht, genannt *Line Match View* werden die KeyPoints der Matches, d.h. die von OpenCV gelieferten ähnlichen Bildpunkte, durch Linien verbunden. Im Akkordeonmenü kann man beispielsweise deren Farbe änder. `Strg + Mausrad` erlaubt, zu zoomen.
![](../images_ueberblick/LineMatchViewZoomed.PNG)
Die Art der Darstellung kann im `View`-Dropdown-Menü geändert werden; so können die Matches etwa auch als Translationslinien angezeigt werden.
![](../images_ueberblick/TranslationMatchViewTab.PNG)
Zudem gibt es bei Matches auch die Möglichkeit, die Daten in einer Tabelle anzuzeigen, im sogenannten
*Raw View*. Die Daten können hier über einen Linksklick als JSON oder CSV ins Clipboard kopiert
werden.
![](../images_ueberblick/RawviewTab.PNG)
Wird `Step` geklickt wird die Ausführung des zu debuggenden Programmes, das beim Aufruf des Hauptfensters angehalten wurde fortgesetzt, bis es auf eine weitere CVVisual-Funktion
stößt:
//...
cvv::debugFilter(src, dest, CVVISUAL\_LOCATION, filename);
Das Hauptfenster erscheint erneut, wobei der neuen Datensatz der Tabelle hinzugefügt wird.
![](../images_ueberblick/MainWindowTwoCalls.PNG)
Da es sich hier um eine Filter-Operation handelt, ist die Anzeige im Tab eine andere:
![](../images_ueberblick/DefaultFilterViewTab.PNG)
Auch die möglichen Anzeigen unterscheiden sich von denen für Match-Operationen.
Der *Dual Filter View* erlaubt zum Beispiel zusätzlich, ein Differenzbild der beiden übergebenen anzuzeigen.
![](../images_ueberblick/DualfilterViewDiffImg.PNG)
Nach einem *fast-forward* (`>>`) über die weiteren Schritte des Programms
//...
cvv::debugDMatch(src, keypoints1, src, keypoints2, match, CVVISUAL\_LOCATION)
//...
cvv::debugFilter(src, dest, CVVISUAL\_LOCATION, filename);
//...
cvv::debugFilter(src, dest, CVVISUAL\_LOCATION, filename);
//...
cvv::debugDMatch(src, keypoints1, src, keypoints2, match, CVVISUAL\_LOCATION);
//...
cvv::showImage(img, CVVISUAL\_LOCATION);
//...
cvv::finalShow();
ergibt sich im Overview folgendes Bild:
![](../images_ueberblick/MainWindowFull.PNG)
Dabei wird durch den letzten Aufruf nur ein einziges Bild zur Anzeige übergeben:
![](../images_ueberblick/SingleImageTab.PNG)
Mithilfe der Textzeile lassen sich durch Kommandos der *Filter Query Language* von CVVisual die Datensätze ordnen, filtern und gruppieren. Hier wurde nach ID gruppiert:
![](../images_ueberblick/OverviewFilterQueryGroupByID.PNG)
Dies funktioniert auch im *Raw View*.
Hinter dem letzten Aufruf einer regulären CVVisual-Funktion muss, wie oben gesehen, `finalShow` aufgerufen werden:
//...
cvv::finalShow();
//...
Es wird ein weiteres Mal das Hauptfenster angezeigt; wird jedoch der nun der einzige verbleibende, der `Close`-Knopf betätigt, wird das Hauptfenster endgültig geschlossen.
Dies beschließt die Debug-Sitzung.
[Quelle des zur Demonstration benutzten Bildes.](http://commons.wikimedia.org/wiki/File:PNG-Gradient.png)
cmake_minimum_required(VERSION 2.8)
project(cvvisual_test)
SET(CMAKE_PREFIX_PATH ~/software/opencv/install)
SET(CMAKE_CXX_COMPILER "g++-4.8")
SET(CMAKE_CXX_FLAGS "-std=c++11 -O2 -pthread -Wall -Werror")
OPTION(CVV_DEBUG_MODE "cvvisual-debug-mode" ON)
if(CVV_DEBUG_MODE MATCHES ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCVVISUAL_DEBUGMODE")
endif()
FIND_PACKAGE(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
FIND_PACKAGE(Qt5Core REQUIRED)
include_directories(${Qt5Core_INCLUDE_DIRS})
FIND_PACKAGE(Qt5Widgets REQUIRED)
include_directories(${Qt5Widgets_INCLUDE_DIRS})
add_definitions(${QT_DEFINITIONS})
SET(OpenCVVisual_DIR "$ENV{HOME}/<<<SET ME>>>")
include_directories("${OpenCVVisual_DIR}/include")
link_directories("${OpenCVVisual_DIR}/build/release")
add_executable(cvvt main.cpp)
target_link_libraries(cvvt
opencv_core opencv_highgui opencv_imgproc opencv_features2d
opencv_cvv
Qt5Core Qt5Widgets Qt5Gui
)
This is a tiny example of how to use CVVisual. It requires a webcam.
Note that the paths in CMakeLists.txt have to be set manually.
cvvisual_test was created by Andreas Bihlmaier.
\ No newline at end of file
// system includes
#include <getopt.h>
#include <iostream>
// library includes
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/cvv/debug_mode.hpp>
#include <opencv2/cvv/show_image.hpp>
#include <opencv2/cvv/filter.hpp>
#include <opencv2/cvv/dmatch.hpp>
#include <opencv2/cvv/final_show.hpp>
template<class T> std::string toString(const T& p_arg)
{
std::stringstream ss;
ss << p_arg;
return ss.str();
}
void
usage()
{
printf("usage: cvvt [-r WxH]\n");
printf("-h print this help\n");
printf("-r WxH change resolution to width W and height H\n");
}
int
main(int argc, char** argv)
{
cv::Size* resolution = nullptr;
// parse options
const char* optstring = "hr:";
int opt;
while ((opt = getopt(argc, argv, optstring)) != -1) {
switch (opt) {
case 'h':
usage();
return 0;
break;
case 'r':
{
char dummych;
resolution = new cv::Size();
if (sscanf(optarg, "%d%c%d", &resolution->width, &dummych, &resolution->height) != 3) {
printf("%s not a valid resolution\n", optarg);
return 1;
}
}
break;
default: /* '?' */
usage();
return 2;
}
}
// setup video capture
cv::VideoCapture capture(0);
if (!capture.isOpened()) {
std::cout << "Could not open VideoCapture" << std::endl;
return 3;
}
if (resolution) {
printf("Setting resolution to %dx%d\n", resolution->width, resolution->height);
capture.set(CV_CAP_PROP_FRAME_WIDTH, resolution->width);
capture.set(CV_CAP_PROP_FRAME_HEIGHT, resolution->height);
}
cv::Mat prevImgGray;
std::vector<cv::KeyPoint> prevKeypoints;
cv::Mat prevDescriptors;
int maxFeatureCount = 500;
cv::ORB detector(maxFeatureCount);
cv::BFMatcher matcher(cv::NORM_HAMMING);
for (int imgId = 0; imgId < 10; imgId++) {
// capture a frame
cv::Mat imgRead;
capture >> imgRead;
printf("%d: image captured\n", imgId);
std::string imgIdString{"imgRead"};
imgIdString += toString(imgId);
cvv::showImage(imgRead, CVVISUAL_LOCATION, imgIdString.c_str());
// convert to grayscale
cv::Mat imgGray;
cv::cvtColor(imgRead, imgGray, CV_BGR2GRAY);
cvv::debugFilter(imgRead, imgGray, CVVISUAL_LOCATION, "to gray");
// detect ORB features
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
detector(imgGray, cv::noArray(), keypoints, descriptors);
printf("%d: detected %zd keypoints\n", imgId, keypoints.size());
// match them to previous image (if available)
if (!prevImgGray.empty()) {
std::vector<cv::DMatch> matches;
matcher.match(prevDescriptors, descriptors, matches);
printf("%d: all matches size=%zd\n", imgId, matches.size());
std::string allMatchIdString{"all matches "};
allMatchIdString += toString(imgId-1) + "<->" + toString(imgId);
cvv::debugDMatch(prevImgGray, prevKeypoints, imgGray, keypoints, matches, CVVISUAL_LOCATION, allMatchIdString.c_str());
// remove worst (as defined by match distance) bestRatio quantile
double bestRatio = 0.8;
std::sort(matches.begin(), matches.end());
matches.resize(int(bestRatio * matches.size()));
printf("%d: best matches size=%zd\n", imgId, matches.size());
std::string bestMatchIdString{"best " + toString(bestRatio) + " matches "};
bestMatchIdString += toString(imgId-1) + "<->" + toString(imgId);
cvv::debugDMatch(prevImgGray, prevKeypoints, imgGray, keypoints, matches, CVVISUAL_LOCATION, bestMatchIdString.c_str());
}
prevImgGray = imgGray;
prevKeypoints = keypoints;
prevDescriptors = descriptors;
}
cvv::finalShow();
return 0;
}
#Introduction to filter function widgets
##The class, functions and types
If you want to enable the user to provide input to a filter you can inherit the virtual class FilterFunctionWidget<In,Out>.
It provides an interface for a filter accepting In images as an input and Out images as output.
The input images are passed using:
```cpp
InputArray = std::array<util::Reference<const cv::Mat>,In>
```
and the output is provided with an output parameter of the type:
```cpp
OutputArray = std::array<util::Reference<cv::Mat>,Out>
```
You should override following functions:
```cpp
virtual void applyFilter(InputArray in,OutputArray out) const;
virtual std::pair<bool, QString> checkInput(InputArray in) const;
```
`applyFilter` has to apply your filter and `checkInput` should check weather the filter can be applied (the first member of the returned pair).
In case the filter can not be applied the second member of the returned pair should contain a message for the user.
If user input changes the setting of the filter the function _emitSignal()_ of the member _signFilterSettingsChanged__ should be called.
For a detailed example look at _CVVisual/src/qtutil/filter/grayfilterwidget.{hpp, cpp}_
https://github.com/CVVisualPSETeam/CVVisual/blob/master/src/qtutil/filter/grayfilterwidget.hpp
https://github.com/CVVisualPSETeam/CVVisual/blob/master/src/qtutil/filter/grayfilterwidget.cpp
#Filter query language
The filter query language is the query language used in the overview and the raw match view to simply the task of filtering, sorting and grouping data sets in a table UI.
The following is a description of the simple syntax and the supported commands.
Just type `#` into the search field to see some supported commands, using the suggestions feature (it's inspired by the awesome z shell).
##Syntax
A query consist basically of many subqueries starting with a `#`:
`[raw filter subquery] #[subquery 1] [...] #[subquery n]`
The optional first part of the query doesn't start with a `#`, it's short for `#raw [...]`.
There three different types of subqueries:
###Sort query
A sort query has the following structure:
`sort by [sort subquery 1], [...], [sort subquery n]`
A sort subquery consist of a sort command (aka "the feature by which you want to sort the table") and a sort order:
- `[command]`: equivalent to `[command] asc`
- `[command] asc`: sorts in ascending order
- `[command] desc`: sorts in descending order
(The sort command is typically a single word.)
For your interest: The `[subquery n]` has higher priority than the `[subquery n+1]`.
###Group query
A group query has the following structure:
`group by [command 1], [...], [command n]`
A group command is a single word declaring the feature you want to group the data sets in the table by.
The group header consist of the `n` items.
For your interest: The raw view currently doesn't support group queries.
###Filter query
A filter query is the basic type of query, allowing you to filter the data sets by several criterias.
It has the following structure:
`#[filter command] [argument]`
It also supports several arguments for one filter command (via the comma seperated filters feature):
`#[cs filter command] [argument 1], [...], [argument n]`
####Range filter query
A range filter query uses basically a comma seperated filter command with two arguments, allowing you to
filter for a range of elements (`[lower bound]` <= `element` <= `[upper bound]`).
It has the following structure:
`#[filter command] [lower bound], [upper bound]`
##Overview
The following commands are supported in the overview:
feauture/command | sorting supported | grouping supported | filtering supported | description
-----------------|:-----------------:|:------------------:|:--------------------|:---------------------
id | yes | yes | yes, also range |
raw | yes | yes | only basic filter | alias for description
description | yes | yes | only basic filter |
image_count | yes | yes | yes, also range | number of images
function | yes | yes | yes | calling function
file | yes | yes | yes | inheriting file
line | yes | yes | yes, also range |
type | yes | yes | yes | call type
##Rawview
The following command are supported in the raw (match) view:
feauture/command | numeric type | description/property
-----------------|:-------------|:---------------------------------------------
match_distance | float | match distance
img_idx | integer | match img idx
query_idx | integer | match query idx
train_idx | integer | match train idx
x_1 | float | x coordinate of the "left" key point
y_1 | float | y coordinate of the "left" key point
size_1 | float | size of the "left" key point
angle_1 | float | angle of the "left" key point
response_1 | float | response (or strength) of the "left" key point
octave_1 | integer | octave of the "left" key point
x_2 | float | x coordinate of the "right" key point
y_2 | float | y coordinate of the "right" key point
size_2 | float | size of the "right" key point
angle_2 | float | angle of the "right" key point
response_2 | float | response (or strength) of the "right" key point
octave_2 | integer | octave of the "right" key point
All commands support range filtering, sorting and grouping and therefore only the used numeric type
(integer or float) is given.
See the opencv documentation for more information about the features.
#CVVisual Example
CVVisual is a debug visualization for OpenCV; thus, its main purpose is to offer different ways to visualize
the results of OpenCV functions to make it possible to see whether they are what the programmer had in mind;
and also to offer some functionality to try other operations on the images right in the debug window.
This text wants to illustrate the use of CVVisual on a code example.
Image we want to debug this program:
[code_example/main.cpp](https://github.com/CVVisualPSETeam/CVVisual/tree/master/doc/code_example/main.cpp)
Note the includes for CVVisual:
10 #include <opencv2/debug_mode.hpp>
11 #include <opencv2/show_image.hpp>
12 #include <opencv2/filter.hpp>
13#include <opencv2/dmatch.hpp>
14 #include <opencv2/final_show.hpp>
It takes 10 snapshots with the webcam.
With each, it first shows the image alone in the debug window,
97 cvv::showImage(imgRead, CVVISUAL_LOCATION, imgIdString.c_str());
then converts it to grayscale and calls CVVisual with the original and resulting image,
101 cv::cvtColor(imgRead, imgGray, CV_BGR2GRAY);
102 cvv::debugFilter(imgRead, imgGray, CVVISUAL_LOCATION, "to gray");
detects the grayscale image's ORB features
107 detector(imgGray, cv::noArray(), keypoints, descriptors);
and matches them to those of the previous image, if available. It calls cvv::debugDMatch() with the results.
113 matcher.match(prevDescriptors, descriptors, matches);
...
117 cvv::debugDMatch(prevImgGray, prevKeypoints, imgGray, keypoints, matches, CVVISUAL_LOCATION, allMatchIdString.c_str());
Finally, it removes the worst (as defined by match distance) 0.8 quantile of matches and calls cvv::debugDMatch() again.
121 std::sort(matches.begin(), matches.end());
122 matches.resize(int(bestRatio * matches.size()));
...
126 cvv::debugDMatch(prevImgGray, prevKeypoints, imgGray, keypoints, matches, CVVISUAL_LOCATION, bestMatchIdString.c_str());
After we started the program, the CVVisual Main Window opens with one _Call_, that is, the first image that a `cvv::showImage()` was called with (the program execution was halted at this call).
![](../images_example/overview_single_call.png)
The image is shown as a small thumbnail in the _Overview table_, together with additional information on it, like the line of the call and the description passed as a parameter.
We double-click it, and a tab opens, where the image is shown bigger. It looks like the webcam worked, so we press `Step` and go to the _Overview_.
![](../images_example/single_image_tab.png)
The window shows up again, this time with the first _Call_ to `cvv::debugFilter()` added.
![](../images_example/overview_two_calls.png)
We open its tab, too, because, say, the grayscale image does not exactly look like what we wanted.
![](../images_example/filter_tab_default.png)
After switching to _SingleFilterView_, which will be more useful to us here, we select to not show the right two images - the grayscale image and the one below, where results of filter operations in this tab are depicted.
![](../images_example/single_filter_right_two_imgs_unselected.png)
In `Select a filter`, a gray filter can be applied with different parameters.
![](../images_example/single_filter_gray.png)
This looks more like what we wanted.
Rechecking `Show image` for the unselected result image of the actual filter operation and zooming (`Ctrl` + `Mouse wheel`) into all images synchronously deeper than 60% shows the different values of the pixels.
![](../images_example/single_filter_deep_zoom.png)
Sadly, we can't do anything about this situation in this session, though, so we just continue.
As stepping through each single _Call_ seems quite tedious, we use the _fast-forward_ button, `>>`.
The program runs until it reaches `finalShow()`, taking images with the webcam along the way.
This saved us some clicking; on the downside, we now have quite an amount of _Calls_ in the table.
![](../images_example/overview_all.png)
Using the [filter query language](http://cvv.mostlynerdless.de/ref/filters-ref.html), the _Calls_ to `debugDMatch()` can be filtered out as they have the specific type "match".
![](../images_example/overview_matches_filtered.png)
We open the tab of the last such _Call_, and find ourselves greeted with a dense bundle of lines across both images, which represent the matches between the two.
![](../images_example/match_tab_line.png)
It is a bit unclear where there actually are matches in this case, so we switch to _TranslationMatchView_, which is a little bit better (especially after scrolling a bit to the right in the left image).
![](../images_example/match_translations.png)
_TranslationMatchView_ shows how the matching _KeyPoints_ are moved in the respective other image.
It seems more fitting for this debug session than the _LineMatchView_, thus, we `Set`it `as default`.
Still, there are too many matches for our taste.
Back in the _Overview_, we open the _Call_ before the last, which is the one where the upper 80% of matches were not yet filtered out.
![](../images_example/match_tab_translations_2.png)
Here, the best 70% of matches can be chosen. The result looks more acceptable, and we take a mental note to change the threshold to 0.7.
![](../images_example/match_translations_2_70percent.png)
The matches can also be shown in a table, the so called _RawView_:
![](../images_example/raw_view.png)
Here, you could copy a selection of them as CSV, JSON, Ruby or Python to the clipboard.
We don't need that in the moment, though; we just close the window, and the program finishes.
We now know what we might want to change in the program.
Finally, a little note on the `cvv::finalShow()` function:
It needs to be there in every program using CVVisual, after the last call to another CVVisual function, er else, the program will crash in the end.
Hopefully, this example shed some light on how CVVisual can be used.
If you want to learn more, refer to the [API](http://cvv.mostlynerdless.de/api) or other documentation on the [web page](http://cvv.mostlynerdless.de/).
Credit, and special thanks, goes to Andreas Bihlmaier, supervisor of the project, who provided the example code.
#Introduction to using CVVisual
##Enabling debug mode
Define the CVV\_DEBUG_MODE macro somewhere in the translation unit.
##Opening the debug window
Open the debug window by putting one of the functions from the [CVVisual API](http://cvv.mostlynerdless.de/api) into your code.
In this example, we want to debug a call to 'dilate' (line.1) which is a filter, so we use debugFilter.
###Example: Code
src, dest and the structuring element elem are of type cv::Mat (see the OpenCV doc on [dilate()](http://docs.opencv.org/modules/imgproc/doc/filtering.html#dilate));
CVVISUAL\_LOCATION is a special macro that inserts the location of the code and description and view can be either string-literals or std::strings. The later three are all optional.
```cpp
#include <filter.hpp>
//...
cv::dilate(src, dest, elem);
cvv::debugFilter(src, dest, CVVISUAL_LOCATION, description, view);
```
When executing the code, the debugFilter function will open the window and halt the execution.
##The Overview Tab
![](images_tut/dilate_overview.PNG)
You are now in the overview tab. Each time you call one of the CVVisual functions, a *Call* is added to the table.
You can see the images you passed to the funtion as well as metadata and additional information.
The text field allows you to sort or group the Calls by different criteria; see the [filter query language documentation](http://cvv.mostlynerdless.de/ref/filterquery-ref.html) on how to use it.
Now double-click on the Call or select `Open in 'CVVisual|main window'` from the context menu.
(You can also choose to remove the Call or open it in a new window there)
##Debugging a filter operation
![](images_tut/dilate_calltab_defaultfview.PNG)
A *CallTab* opens. In the center, there are the images from the call.
In the `View` drop-down menu you find different *Views* of the Call, that is, different visualizations of it. The accordion menu on the left offers information on the images and additional options depending on the View and the type of the Call.
Important here might be that `ImageInformation` offers the possibility to zoom (you can also use `Ctrl` plus the mouse wheel); if you zoom in more than 60%, the image pixels will be overlaid with the channel values, in a 3-channel image usually in order (top-down) BGR.
As our dilate seems to have produced acceptable results, we want to continue through the code.
So, we push the `Step` button in the upper left.
The window will comes up again the next time one of the CVVisual functions is called.
Then, we see two Calls in the Overview table, the one from before and the new one.
You need to put `finalShow()` after the last regular CVVisual function. If the program reaches it, `Step` and the fast-forward button `>>` will vanish, so we press `Close`, which does exactly what it says.
([Source](http://commons.wikimedia.org/wiki/File:PNG-Gradient.png) of the image used for demonstration.
Note that the screenshots were taken during development and may not depict all features of the current version.)
filterquery: filterquery-ref.md
SingleImageView: views-ref.md#toc_2
DefaultFilterView: views-ref.md#toc_4
DualFilterView: views-ref.md#toc_5
SingleFilterView: views-ref.md#toc_6
DepthMatchView: views-ref.md#toc_8
LineMatchView: views-ref.md#toc_9
RawView: views-ref.md#toc_10
TranslationMatchView: views-ref.md#toc_11
#Views
##General information:
Most views offer an `ImageInformation` collapsable in their accordion menus.
The zoom can be found here.
`Ctrl`+`Mouse wheel` is also zoom; `Ctrl`+`Shift`+`Mouse wheel` is a slower zoom.
If the zoom is deeper than 60%, the image's pixels will be overlaid with their channel values; usually, the order is BGR[+alpha] from the top.
##Single Image View:
Associated with the `debugSingleImage()` function.
Shows one single image with no features other than `Image Information`.
##Filter Views:
Associated with the `debugFilter()` function.
###DefaultFilterView:
Shows two images with only the basic features of `ImageInformation`, synchronized zoom and `Histogram`.
###DualFilterView:
Shows the two images given to the CVVisual function and _Result Image_ inbetween
which represents the result of a filter that was applied to the others via the `Filter selection` collapsable,
like a difference image between the two.
###SingleFilterView:
Allows to apply filters to the images it shows via the `Select a filter` collapsable.
##Match Views:
Associated with the `debugDMatch()` function.
###PointMatchView:
Interprets the translation of matches as depth value.
###LineMatchView:
Connects matching key points in the images with lines.
###Rawview:
Shows in a table data of the matches.
The table entries can be filtered, sorted and grouped by using commands from CVVisual's [filter query language](filterquery-ref.html) in the text box.
###TranslationMatchView
Shows the distance between a keypoint in one image to its match in the other as an arrow or line in one image.
#ifndef CVVISUAL_CALL_DATA_HPP
#define CVVISUAL_CALL_DATA_HPP
#include <string>
#include <cstddef>
#include <utility>
namespace cvv
{
namespace impl
{
/**
* @brief Optional information about a location in Code.
*/
struct CallMetaData
{
public:
/**
* @brief Creates an unknown location.
*/
CallMetaData()
: file(nullptr), line(0), function(nullptr), isKnown(false)
{
}
/**
* @brief Creates the provided location.
*
* Argument should be self-explaining.
*/
CallMetaData(const char *file, size_t line, const char *function)
: file(file), line(line), function(function), isKnown(true)
{
}
operator bool()
{
return isKnown;
}
// self-explaining:
const char *file;
const size_t line;
const char *function;
/**
* @brief Whether *this holds actual data.
*/
const bool isKnown;
};
}
} // namespaces
#ifdef __GNUC__
#define CVVISUAL_FUNCTION_NAME_MACRO __PRETTY_FUNCTION__
#else
#define CVVISUAL_FUNCTION_NAME_MACRO __func__
#endif
/**
* @brief Creates an instance of CallMetaData with the location of the macro as
* value.
*/
#define CVVISUAL_LOCATION \
::cvv::impl::CallMetaData(__FILE__, __LINE__, \
CVVISUAL_FUNCTION_NAME_MACRO)
#endif
#include <opencv2/cvv/call_meta_data.hpp>
#include <opencv2/cvv/debug_mode.hpp>
#include <opencv2/cvv/dmatch.hpp>
#include <opencv2/cvv/filter.hpp>
#include <opencv2/cvv/final_show.hpp>
#include <opencv2/cvv/show_image.hpp>
#ifndef CVVISUAL_DEBUG_MODE_HPP
#define CVVISUAL_DEBUG_MODE_HPP
#if __cplusplus >= 201103L && defined CVVISUAL_USE_THREAD_LOCAL
#define CVVISUAL_THREAD_LOCAL thread_local
#else
#define CVVISUAL_THREAD_LOCAL
#endif
namespace cvv
{
namespace impl
{
/**
* The debug-flag-singleton
*/
static inline bool &getDebugFlag()
{
CVVISUAL_THREAD_LOCAL static bool flag = true;
return flag;
}
} // namespace impl
/**
* @brief Returns whether debug-mode is active for this TU and thread.
*/
static inline bool debugMode()
{
return impl::getDebugFlag();
}
/**
* @brief Set the debug-mode for this TU and thread.
*/
static inline void setDebugFlag(bool active)
{
impl::getDebugFlag() = active;
}
} // namespace cvv
#endif
#ifndef CVVISUAL_DEBUG_DMATCH_HPP
#define CVVISUAL_DEBUG_DMATCH_HPP
#include <string>
#include "opencv2/core/core.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "call_meta_data.hpp"
#include "debug_mode.hpp"
namespace cvv
{
namespace impl
{
void debugDMatch(cv::InputArray img1, std::vector<cv::KeyPoint> keypoints1,
cv::InputArray img2, std::vector<cv::KeyPoint> keypoints2,
std::vector<cv::DMatch> matches, const CallMetaData &data,
const char *description, const char *view,
bool useTrainDescriptor);
} // namespace impl
#ifdef CVVISUAL_DEBUGMODE
static inline void
debugDMatch(cv::InputArray img1, std::vector<cv::KeyPoint> keypoints1,
cv::InputArray img2, std::vector<cv::KeyPoint> keypoints2,
std::vector<cv::DMatch> matches, const impl::CallMetaData &data,
const char *description = nullptr, const char *view = nullptr,
bool useTrainDescriptor = true)
{
if (debugMode())
{
impl::debugDMatch(img1, std::move(keypoints1), img2,
std::move(keypoints2), std::move(matches),
data, description, view, useTrainDescriptor);
}
}
static inline void
debugDMatch(cv::InputArray img1, std::vector<cv::KeyPoint> keypoints1,
cv::InputArray img2, std::vector<cv::KeyPoint> keypoints2,
std::vector<cv::DMatch> matches, const impl::CallMetaData &data,
const std::string &description, const std::string &view,
bool useTrainDescriptor = true)
{
if (debugMode())
{
impl::debugDMatch(img1, std::move(keypoints1), img2,
std::move(keypoints2), std::move(matches),
data, description.c_str(), view.c_str(),
useTrainDescriptor);
}
}
#else
/**
* @brief Debug a set of matches between two images.
*/
static inline void debugDMatch(cv::InputArray, std::vector<cv::KeyPoint>,
cv::InputArray, std::vector<cv::KeyPoint>,
std::vector<cv::DMatch>,
const impl::CallMetaData &,
const char * = nullptr, const char * = nullptr,
bool = true)
{
}
/**
* Dito.
*/
static inline void debugDMatch(cv::InputArray, std::vector<cv::KeyPoint>,
cv::InputArray, std::vector<cv::KeyPoint>,
std::vector<cv::DMatch>,
const impl::CallMetaData &, const std::string &,
const std::string &, bool = true)
{
}
#endif
} // namespace cvv
#endif
#ifndef CVVISUAL_DEBUG_FILTER_HPP
#define CVVISUAL_DEBUG_FILTER_HPP
#include <string>
#include "opencv2/core/core.hpp"
#include "call_meta_data.hpp"
#include "debug_mode.hpp"
namespace cvv
{
namespace impl
{
// implementation outside API
void debugFilter(cv::InputArray original, cv::InputArray result,
const CallMetaData &data, const char *description,
const char *view);
} // namespace impl
#ifdef CVVISUAL_DEBUGMODE
static inline void
debugFilter(cv::InputArray original, cv::InputArray result,
impl::CallMetaData metaData = impl::CallMetaData(),
const char *description = nullptr, const char *view = nullptr)
{
if (debugMode())
{
impl::debugFilter(original, result, metaData, description,
view);
}
}
static inline void debugFilter(cv::InputArray original, cv::InputArray result,
impl::CallMetaData metaData,
const ::std::string &description,
const ::std::string &view = "")
{
if (debugMode())
{
impl::debugFilter(original, result, metaData,
description.c_str(), view.c_str());
}
}
#else
/**
* @brief Use the debug-framework to compare two images (from which the second
* is intended to be the result of
* a filter applied to the first).
*/
static inline void debugFilter(cv::InputArray, cv::InputArray,
impl::CallMetaData = impl::CallMetaData(),
const char * = nullptr, const char * = nullptr)
{
}
/**
* Dito.
*/
static inline void debugFilter(cv::InputArray, cv::InputArray,
impl::CallMetaData, const ::std::string &,
const ::std::string &)
{
}
#endif
} // namespace cvv
#endif
#ifndef CVVISUAL_FINAL_SHOW_HPP
#define CVVISUAL_FINAL_SHOW_HPP
#include "debug_mode.hpp"
namespace cvv
{
namespace impl
{
void finalShow();
}
/**
* @brief Passes the control to the debug-window for a last time.
*
* This function must be called once if there was any prior debug-call. After that all debug-data
* are freed.
*
* If there was no prior call it may be called once in which case it returns
* without opening a window.
*
* In either case no further debug-calls must be made (undefined behaviour!!).
*
*/
inline void finalShow()
{
#ifdef CVVISUAL_DEBUGMODE
if (debugMode())
{
impl::finalShow();
}
#endif
}
/**
* @brief RAII-class to call finalShow() in it's dtor.
*/
class FinalShowCaller
{
public:
/**
* @brief Calls finalShow().
*/
~FinalShowCaller()
{
finalShow();
}
};
}
#endif
#ifndef CVVISUAL_DEBUG_SHOW_IMAGE_HPP
#define CVVISUAL_DEBUG_SHOW_IMAGE_HPP
#include <string>
#include "opencv2/core/core.hpp"
#include "call_meta_data.hpp"
#include "debug_mode.hpp"
namespace cvv
{
namespace impl
{
// implementation outside API
void showImage(cv::InputArray img, const CallMetaData &data,
const char *description, const char *view);
} // namespace impl
#ifdef CVVISUAL_DEBUGMODE
static inline void showImage(cv::InputArray img,
impl::CallMetaData metaData = impl::CallMetaData(),
const char *description = nullptr,
const char *view = nullptr)
{
if (debugMode())
{
impl::showImage(img, metaData, description, view);
}
}
static inline void showImage(cv::InputArray img, impl::CallMetaData metaData,
const ::std::string &description,
const ::std::string &view = "")
{
if (debugMode())
{
impl::showImage(img, metaData, description.c_str(),
view.c_str());
}
}
#else
/**
* Use the debug-framework to show a single image.
*/
static inline void showImage(cv::InputArray,
impl::CallMetaData = impl::CallMetaData(),
const char * = nullptr, const char * = nullptr)
{
}
/**
* Dito.
*/
static inline void showImage(cv::InputArray, impl::CallMetaData,
const ::std::string &, const ::std::string &)
{
}
#endif
} // namespace cvv
#endif
// system includes
#include <getopt.h>
#include <iostream>
// library includes
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
#define CVVISUAL_DEBUGMODE
#include <opencv2/cvv/debug_mode.hpp>
#include <opencv2/cvv/show_image.hpp>
#include <opencv2/cvv/filter.hpp>
#include <opencv2/cvv/dmatch.hpp>
#include <opencv2/cvv/final_show.hpp>
template<class T> std::string toString(const T& p_arg)
{
std::stringstream ss;
ss << p_arg;
return ss.str();
}
void
usage()
{
printf("usage: cvv_demo [-r WxH]\n");
printf("-h print this help\n");
printf("-r WxH change resolution to width W and height H\n");
}
int
main(int argc, char** argv)
{
cv::Size* resolution = nullptr;
// parse options
const char* optstring = "hr:";
int opt;
while ((opt = getopt(argc, argv, optstring)) != -1) {
switch (opt) {
case 'h':
usage();
return 0;
break;
case 'r':
{
char dummych;
resolution = new cv::Size();
if (sscanf(optarg, "%d%c%d", &resolution->width, &dummych, &resolution->height) != 3) {
printf("%s not a valid resolution\n", optarg);
return 1;
}
}
break;
default: /* '?' */
usage();
return 2;
}
}
// setup video capture
cv::VideoCapture capture(0);
if (!capture.isOpened()) {
std::cout << "Could not open VideoCapture" << std::endl;
return 3;
}
if (resolution) {
printf("Setting resolution to %dx%d\n", resolution->width, resolution->height);
capture.set(CV_CAP_PROP_FRAME_WIDTH, resolution->width);
capture.set(CV_CAP_PROP_FRAME_HEIGHT, resolution->height);
}
cv::Mat prevImgGray;
std::vector<cv::KeyPoint> prevKeypoints;
cv::Mat prevDescriptors;
int maxFeatureCount = 500;
cv::ORB detector(maxFeatureCount);
cv::BFMatcher matcher(cv::NORM_HAMMING);
for (int imgId = 0; imgId < 10; imgId++) {
// capture a frame
cv::Mat imgRead;
capture >> imgRead;
printf("%d: image captured\n", imgId);
std::string imgIdString{"imgRead"};
imgIdString += toString(imgId);
cvv::showImage(imgRead, CVVISUAL_LOCATION, imgIdString.c_str());
// convert to grayscale
cv::Mat imgGray;
cv::cvtColor(imgRead, imgGray, CV_BGR2GRAY);
cvv::debugFilter(imgRead, imgGray, CVVISUAL_LOCATION, "to gray");
// detect ORB features
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
detector(imgGray, cv::noArray(), keypoints, descriptors);
printf("%d: detected %zd keypoints\n", imgId, keypoints.size());
// match them to previous image (if available)
if (!prevImgGray.empty()) {
std::vector<cv::DMatch> matches;
matcher.match(prevDescriptors, descriptors, matches);
printf("%d: all matches size=%zd\n", imgId, matches.size());
std::string allMatchIdString{"all matches "};
allMatchIdString += toString(imgId-1) + "<->" + toString(imgId);
cvv::debugDMatch(prevImgGray, prevKeypoints, imgGray, keypoints, matches, CVVISUAL_LOCATION, allMatchIdString.c_str());
// remove worst (as defined by match distance) bestRatio quantile
double bestRatio = 0.8;
std::sort(matches.begin(), matches.end());
matches.resize(int(bestRatio * matches.size()));
printf("%d: best matches size=%zd\n", imgId, matches.size());
std::string bestMatchIdString{"best " + toString(bestRatio) + " matches "};
bestMatchIdString += toString(imgId-1) + "<->" + toString(imgId);
cvv::debugDMatch(prevImgGray, prevKeypoints, imgGray, keypoints, matches, CVVISUAL_LOCATION, bestMatchIdString.c_str());
}
prevImgGray = imgGray;
prevKeypoints = keypoints;
prevDescriptors = descriptors;
}
cvv::finalShow();
return 0;
}
#include "view_controller.hpp"
#include <stdexcept>
#include <iostream>
#include <QApplication>
#include <QDesktopServices>
#include <QUrl>
#include "../gui/call_tab.hpp"
#include "../gui/call_window.hpp"
#include "../gui/overview_panel.hpp"
#include "../gui/main_call_window.hpp"
#include "../gui/filter_call_tab.hpp"
#include "../gui/match_call_tab.hpp"
#include "../gui/image_call_tab.hpp"
#include "../impl/init.hpp"
#include "../impl/filter_call.hpp"
#include "../impl/match_call.hpp"
#include "../impl/single_image_call.hpp"
#include "../impl/data_controller.hpp"
#include "../qtutil/util.hpp"
namespace cvv
{
namespace controller
{
// It's only used for instatiating a QApplication.
// static char *emptyArray[] = {""};
static char *parameterSystemV[] = { new char[1]{ 0 }, nullptr };
static int parameterSystemC = 1;
ViewController::ViewController()
{
impl::initializeFilterAndViews();
if (!QApplication::instance())
{
auto tmp =
new QApplication{ parameterSystemC, parameterSystemV };
ownsQApplication = true;
(void)tmp;
}
ovPanel = new gui::OverviewPanel{ util::makeRef(*this) };
mainWindow = new gui::MainCallWindow(util::makeRef(*this), 0, ovPanel);
windowMap[0] = std::unique_ptr<gui::CallWindow>(mainWindow);
max_window_id = 0;
mainWindow->show();
}
ViewController::~ViewController()
{
callTabMap.clear();
windowMap.clear();
windowMap.clear();
if (ownsQApplication)
{
delete QApplication::instance();
}
}
void ViewController::addCallType(const QString typeName, TabFactory constr)
{
ViewController::callTabType[typeName] = constr;
}
std::unique_ptr<cvv::gui::FilterCallTab>
makeFilterCallTab(cvv::util::Reference<cvv::impl::Call> call)
{
return cvv::util::make_unique<cvv::gui::FilterCallTab>(
*call.castTo<cvv::impl::FilterCall>());
}
std::unique_ptr<cvv::gui::MatchCallTab>
makeMatchCallTab(cvv::util::Reference<cvv::impl::Call> call)
{
return cvv::util::make_unique<cvv::gui::MatchCallTab>(
*call.castTo<cvv::impl::MatchCall>());
}
std::unique_ptr<cvv::gui::ImageCallTab>
makeImageCallTab(cvv::util::Reference<cvv::impl::Call> call)
{
return cvv::util::make_unique<cvv::gui::ImageCallTab>(
*call.castTo<cvv::impl::SingleImageCall>());
}
std::map<QString, TabFactory> ViewController::callTabType{
{ "filter", makeFilterCallTab }, { "match", makeMatchCallTab },
{ "singleImage", makeImageCallTab }
};
void ViewController::addCall(util::Reference<impl::Call> data)
{
updateMode();
if (mode == Mode::NORMAL)
{
ovPanel->addElement(*data);
mainWindow->showOverviewTab();
}
else if (mode == Mode::FAST_FORWARD)
{
ovPanel->addElementBuffered(*data);
}
}
void ViewController::exec()
{
updateMode();
if (mode == Mode::NORMAL)
{
QApplication::instance()->exec();
}
}
impl::Call &ViewController::getCall(size_t id)
{
return impl::dataController().getCall(id);
}
QString ViewController::getSetting(const QString &scope, const QString &key)
{
return qtutil::getSetting(scope, key);
}
std::vector<util::Reference<gui::CallWindow>> ViewController::getTabWindows()
{
std::vector<util::Reference<gui::CallWindow>> windows{};
for (auto &it : windowMap)
{
windows.push_back(util::makeRef(*(it.second)));
}
return windows;
}
util::Reference<gui::MainCallWindow> ViewController::getMainWindow()
{
return util::makeRef(*mainWindow);
}
void ViewController::moveCallTabToNewWindow(size_t tabId)
{
if (!hasCall(tabId))
return;
auto newWindow = util::make_unique<gui::CallWindow>(
util::makeRef<ViewController>(*this), ++max_window_id);
removeCallTab(tabId);
newWindow->addTab(getCallTab(tabId));
newWindow->show();
if (doesShowExitProgramButton)
{
newWindow->showExitProgramButton();
}
windowMap[max_window_id] = std::move(newWindow);
removeEmptyWindowsWithDelay();
}
void ViewController::moveCallTabToWindow(size_t tabId, size_t windowId)
{
if (!hasCall(tabId))
return;
removeCallTab(tabId);
auto tab = getCallTab(tabId);
windowMap[windowId]->addTab(tab);
removeEmptyWindowsWithDelay();
}
void ViewController::removeCallTab(size_t tabId, bool deleteIt, bool deleteCall, bool updateUI)
{
auto *curWindow = getCurrentWindowOfTab(tabId);
if (curWindow->hasTab(tabId))
{
getCurrentWindowOfTab(tabId)->removeTab(tabId);
if (deleteIt)
{
callTabMap.erase(tabId);
}
}
if (deleteCall && hasCall(tabId))
{
if (updateUI)
{
ovPanel->removeElement(tabId);
}
impl::dataController().removeCall(tabId);
}
removeEmptyWindowsWithDelay();
}
void ViewController::openHelpBrowser(const QString &topic)
{
qtutil::openHelpBrowser(topic);
}
void ViewController::resumeProgramExecution()
{
QApplication::instance()->exit();
}
void ViewController::setDefaultSetting(const QString &scope, const QString &key,
const QString &value)
{
qtutil::setDefaultSetting(scope, key, value);
}
void ViewController::setSetting(const QString &scope, const QString &key,
const QString &value)
{
qtutil::setSetting(scope, key, value);
}
void ViewController::showCallTab(size_t tabId)
{
auto *window = getCurrentWindowOfTab(tabId);
window->showTab(tabId);
window->setWindowState((window->windowState() & ~Qt::WindowMinimized) |
Qt::WindowActive);
window->raise();
}
void ViewController::showAndOpenCallTab(size_t tabId)
{
auto curWindow = getCurrentWindowOfTab(tabId);
if (!curWindow->hasTab(tabId))
{
moveCallTabToWindow(tabId, 0);
curWindow = mainWindow;
}
curWindow->showTab(tabId);
}
void ViewController::openCallTab(size_t tabId)
{
auto curWindow = getCurrentWindowOfTab(tabId);
if (!curWindow->hasTab(tabId))
{
moveCallTabToWindow(tabId, 0);
curWindow = mainWindow;
}
}
void ViewController::showOverview()
{
mainWindow->setWindowState(
(mainWindow->windowState() & ~Qt::WindowMinimized) |
Qt::WindowActive);
mainWindow->raise();
mainWindow->showOverviewTab();
}
gui::CallWindow *ViewController::getCurrentWindowOfTab(size_t tabId)
{
for (auto &elem : windowMap)
{
if (elem.second->hasTab(tabId))
{
return elem.second.get();
}
}
return mainWindow;
}
gui::CallTab *ViewController::getCallTab(size_t tabId)
{
if (callTabMap.count(tabId) == 0)
{
auto *call = &(getCall(tabId));
if (callTabType.count(call->type()) == 0)
{
throw std::invalid_argument{
"no such type '" + call->type().toStdString() +
"'"
};
}
callTabMap[tabId] =
callTabType[call->type()](util::makeRef(*call));
}
return callTabMap[tabId].get();
}
void ViewController::removeWindowFromMaps(size_t windowId)
{
if (windowMap.count(windowId) > 0)
{
windowMap[windowId].release();
windowMap.erase(windowId);
}
}
void ViewController::removeEmptyWindows()
{
std::vector<size_t> remIds{};
for (auto &elem : windowMap)
{
if (elem.second->tabCount() == 0 && elem.second->getId() != 0)
{
remIds.push_back(elem.first);
}
}
for (auto windowId : remIds)
{
auto window = windowMap[windowId].release();
windowMap.erase(windowId);
window->deleteLater();
}
shouldRunRemoveEmptyWindows_ = false;
}
void ViewController::removeEmptyWindowsWithDelay()
{
shouldRunRemoveEmptyWindows_ = true;
}
bool ViewController::shouldRunRemoveEmptyWindows()
{
return shouldRunRemoveEmptyWindows_;
}
void ViewController::showExitProgramButton()
{
for (auto &elem : windowMap)
{
elem.second->showExitProgramButton();
}
doesShowExitProgramButton = true;
}
bool ViewController::hasCall(size_t id)
{
return impl::dataController().hasCall(id);
}
void ViewController::setMode(Mode newMode)
{
mode = newMode;
switch (newMode)
{
case Mode::NORMAL:
break;
case Mode::HIDE:
hideAll();
QApplication::instance()->exit();
break;
case Mode::FAST_FORWARD:
if (!doesShowExitProgramButton)
{
QApplication::instance()->exit();
}
else
{
mode = Mode::NORMAL;
}
break;
}
}
Mode ViewController::getMode()
{
return mode;
}
void ViewController::updateMode()
{
if (mode == Mode::FAST_FORWARD && hasFinalCall())
{
mode = Mode::NORMAL;
ovPanel->flushElementBuffer();
}
}
void ViewController::hideAll()
{
for (auto &window : windowMap)
{
window.second->hide();
}
}
bool ViewController::hasFinalCall()
{
return doesShowExitProgramButton;
}
}
}
#ifndef CVVISUAL_VIEWCONTROLLER_HPP
#define CVVISUAL_VIEWCONTROLLER_HPP
#include <vector>
#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <functional>
#include <utility>
#include <QString>
#include "../util/util.hpp"
#include "../impl/call.hpp"
#include "../gui/call_window.hpp"
#include "../gui/call_tab.hpp"
namespace cvv
{
namespace gui
{
class CallTab;
class CallWindow;
class MainCallWindow;
class OverviewPanel;
}
namespace controller
{
/**
* @brief Modes that this cvv application can be running in.
*/
enum class Mode
{
/**
* @brief The normal mode.
*/
NORMAL = 0,
/**
* @brief The cvv UI is hidden.
*/
HIDE = 1,
/**
* @brief The cvv UI stops only at the final call
* The final call is the call which is called after `cvv::finalShow()`)
*/
FAST_FORWARD = 2
};
class ViewController;
/**
* @brief Typedef for a function that creates a CallTab from a impl::Call.
*/
using TabFactory =
std::function<std::unique_ptr<gui::CallTab>(util::Reference<impl::Call>)>;
/**
* @brief Controlls the windows, call tabs and the event fetch loop.
* Its the layer between the low level model (aka DataController) an the high
* level GUI (aka CallTab, OverviewPanel, ...).
*/
class ViewController
{
public:
/**
* @brief The default contructor for this class.
*/
ViewController();
/**
* @brief Clean up.
*/
~ViewController();
/**
* @brief Adds the new call tab type.
* @param typeName name of the new type
* @param constr function constructing an instance of this call tab
* type
* @return an instance of the new call tab type
*/
static void addCallType(const QString typeName, TabFactory constr);
/**
* @brief Adds a new call and shows it in the overview table.
* @param data new call (data)
*/
void addCall(util::Reference<impl::Call> data);
/**
* @brief Execute the Qt event loop.
*/
void exec();
/**
* @brief Get the call with the given id.
* @param id given id
* @return call with the given id
*/
impl::Call &getCall(size_t id);
/**
* @brief Get the current setting [key] in the given scope.
* Please use `setDefaultSetting` to set a default value that's other
* than
* an empty QString.
* @param scope given scope (e.g. 'Overview')
* @param key settings key (e.g. 'autoOpenTabs')
* @return settings string
*/
QString getSetting(const QString &scope, const QString &key);
/**
* @brief Get the inherited call windows with tabs.
* @return the inherited CallWindows
*/
std::vector<util::Reference<gui::CallWindow>> getTabWindows();
/**
* @brief Get the inherited main window.
* @return the inherited main window
*/
util::Reference<gui::MainCallWindow> getMainWindow();
/**
* @brief Move the call tab with the given id to a new window.
* @param tabId given call tab id
*/
void moveCallTabToNewWindow(size_t tabId);
/**
* @brief Move the given call tab to the given window.
* @param tabId id of the given call tab
* @param windowId id of the given window (0 is the main window)
*/
void moveCallTabToWindow(size_t tabId, size_t windowId);
/**
* @brief Removes the call tab with the given id.
* @param tabId given id
* @param deleteCall if deleteCall and deleteIt are true, it also
* deletes the proper Call
*/
void removeCallTab(size_t tabId, bool deleteIt = true,
bool deleteCall = false, bool updateUI = true);
/**
* @brief Opens the users default browser with the topic help page.
* Current URL: cvv.mostlynerdless.de/help.php?topic=[topic]
*
* Topics can be added via appending the doc/topics.yml file.
*
* @param topic help topic
*/
void openHelpBrowser(const QString &topic);
/**
* @brief Resume the execution of the calling program.
*/
void resumeProgramExecution();
/**
* @brief Set the default setting for a given stettings key and scope.
* It doesn't override existing settings.
* @param scope given settings scope
* @param key given settings key
* @param value default value of the setting
*/
void setDefaultSetting(const QString &scope, const QString &key,
const QString &value);
/**
* @brief Set the setting for a given stettings key and scope.
* @param scope given settings scope
* @param key given settings key
* @param value new value of the setting
*/
void setSetting(const QString &scope, const QString &key,
const QString &value);
/**
* @brief Show the given call tab and bring it's window to the front.
* @note It's not guaranteed that it really brings the tabs' window to the front.
* @param tabId id of the given call tab
*/
void showCallTab(size_t tabId);
/**
* @brief Shows the tab and opens it if neccessary.
* @param tabId id of the tab
*/
void showAndOpenCallTab(size_t tabId);
/**
* @brief Opens the tab it if neccessary.
* @param tabId id of the tab
*/
void openCallTab(size_t tabId);
/**
* @brief Show the overview tab (and table) and bring it's window to the
* front.
* @note The latter is not guaranteed.
*/
void showOverview();
/**
* @brief Get the window in which the given tab lays currently.
* @param tabId id of the given call tab
* @return current window
*/
gui::CallWindow *getCurrentWindowOfTab(size_t tabId);
/**
* @brief Returns the call tab with the given id and constructs it if
* doesn't exit.
* @param tabId given id
* @return call tab with given id
*/
gui::CallTab *getCallTab(size_t tabId);
/**
* @brief Remove the window from the internal data structures.
* @param windowId id of the window
* @note Only call this method if you now the implacations of deleting
* the window.
*/
void removeWindowFromMaps(size_t windowId);
/**
* @brief Shows an "Exit program" button on each window.
*/
void showExitProgramButton();
/**
* @brief Removes the empty windows.
* @note It's safer to call the removeEmptyWindowsWithDelay method
* instead.
*/
void removeEmptyWindows();
/**
* @brief Removes the empty windows with a small delay.
*/
void removeEmptyWindowsWithDelay();
/**
* @brief Checks whether or not is useful to call the
* removeEmptyWindows() method.
* @return Is is useful to call the removeEmptyWindows() method?
* @note Please don't call this method outside a periodcally called
* method.
*/
bool shouldRunRemoveEmptyWindows();
/**
* @brief Set the mode that this application is running in.
* @param newMode mode to be set
*/
void setMode(Mode newMode);
/**
* @brief Returns the mode this program is running in.
* @return the current mode, NROMAL, HIDE or FAST_FORWARD
*/
Mode getMode();
/**
* @brief Checks whether or not the `cvv::finalCall()` method has been
* called?
* @return Has the `cvv::finalCall()` method been called?
*/
bool hasFinalCall();
private:
static std::map<QString, TabFactory> callTabType;
std::map<size_t, std::unique_ptr<gui::CallWindow>> windowMap{};
gui::MainCallWindow *mainWindow;
std::map<size_t, std::unique_ptr<gui::CallTab>> callTabMap{};
gui::OverviewPanel *ovPanel;
bool doesShowExitProgramButton = false;
/**
* @brief Counter == 0 <=> you should run `removeEmptyWindows()`.
*/
bool shouldRunRemoveEmptyWindows_ = true;
Mode mode = Mode::NORMAL;
bool ownsQApplication = false;
size_t max_window_id = 0;
bool hasCall(size_t id);
void updateMode();
void hideAll();
};
}
}
#endif
#include "api.hpp"
#include "../gui/filter_call_tab.hpp"
#include "../gui/match_call_tab.hpp"
namespace cvv
{
namespace extend
{
void addCallType(const QString name, TabFactory factory)
{
controller::ViewController::addCallType(name, factory);
}
}
} // namespaces cvv::extend
#ifndef CVVISUAL_EXTENSION_API_HPP
#define CVVISUAL_EXTENSION_API_HPP
#include <opencv2/core/core.hpp>
#include <QString>
#include <QWidget>
#include "../impl/call.hpp"
#include "../controller/view_controller.hpp"
#include "../view/filter_view.hpp"
#include "../gui/match_call_tab.hpp"
#include "../gui/filter_call_tab.hpp"
#include "../qtutil/filterselectorwidget.hpp"
namespace cvv
{
namespace extend
{
/**
* @brief Introduces a new filter-view.
* @param name of the new FilterView.
* @tparam FView A FilterView. Needs to have a constructor of the form
* FView(const cvv::impl::FilterCall&, QWidget*).
*/
template <class FView> void addFilterView(const QString name)
{
cvv::gui::FilterCallTab::registerFilterView<FView>(name);
}
/**
* @brief Introduces a new match-view.
* @param name of the new MatchView.
* @tparam MView A MatchView. Needs to have a constructor of the form
* MView(const cvv::impl::MatchCall&, QWidget*).
*/
template <class MView> void addMatchView(const QString name)
{
cvv::gui::MatchCallTab::registerMatchView<MView>(name);
}
using TabFactory = controller::TabFactory;
/**
* @brief Introduces a new call-type.
* @param factory A function that recieves a reference to a call and should
* return the appropriate
* window.
*/
void addCallType(const QString name, TabFactory factory);
template <std::size_t In, std::size_t Out, class Filter>
/**
* @brief Introduces a new filter for the filter-selector-widget.
*/
bool registerFilter(const QString &name)
{
return cvv::qtutil::registerFilter<In, Out, Filter>(name);
}
}
} // namespaces cvv::extend
#endif
#ifndef CVVISUAL_CALL_TAB_HPP
#define CVVISUAL_CALL_TAB_HPP
#include <QString>
#include <QWidget>
#include "../util/util.hpp"
namespace cvv
{
namespace gui
{
/**
* @brief Super class of the inner part of a tab or window.
* A call tab.
* The inner part of a tab or a window.
* Super class for actual call tabs containing views.
*/
class CallTab : public QWidget
{
Q_OBJECT
public:
/**
* @brief Returns the name of this tab.
* @return current name
*/
const QString getName() const
{
return name;
}
/**
* @brief Sets the name of this tab.
* @param name new name
*/
void setName(const QString &newName)
{
name = newName;
}
/**
* @brief Returns the of this CallTab.
* @return the ID of the CallTab
* (ID is equal to the ID of the associated call in derived classes)
*/
virtual size_t getId() const
{
return 0;
}
private:
QString name;
};
}
} // namespaces
#endif
#include "call_window.hpp"
#include <QMenu>
#include <QStatusBar>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVariant>
#include "../stfl/stringutils.hpp"
namespace cvv
{
namespace controller
{
class ViewController;
}
namespace gui
{
CallWindow::CallWindow(util::Reference<controller::ViewController> controller,
size_t id)
: id{ id }, controller{ controller }
{
initTabs();
initFooter();
setWindowTitle(QString("CVVisual | window no. %1").arg(id));
setMinimumWidth(600);
setMinimumHeight(600);
}
void CallWindow::initTabs()
{
tabWidget = new TabWidget(this);
tabWidget->setTabsClosable(true);
tabWidget->setMovable(true);
setCentralWidget(tabWidget);
auto *flowButtons = new QHBoxLayout();
auto *flowButtonsWidget = new QWidget(this);
tabWidget->setCornerWidget(flowButtonsWidget, Qt::TopLeftCorner);
flowButtonsWidget->setLayout(flowButtons);
flowButtons->setAlignment(Qt::AlignLeft | Qt::AlignTop);
closeButton = new QPushButton("Close", this);
flowButtons->addWidget(closeButton);
closeButton->setStyleSheet(
"QPushButton {background-color: red; color: white;}");
closeButton->setToolTip("Close this debugging application.");
connect(closeButton, SIGNAL(clicked()), this, SLOT(closeApp()));
fastForwardButton = new QPushButton(">>", this);
flowButtons->addWidget(fastForwardButton);
fastForwardButton->setStyleSheet(
"QPushButton {background-color: yellow; color: blue;}");
fastForwardButton->setToolTip(
"Fast forward until cvv::finalCall() gets called.");
connect(fastForwardButton, SIGNAL(clicked()), this,
SLOT(fastForward()));
stepButton = new QPushButton("Step", this);
flowButtons->addWidget(stepButton);
stepButton->setStyleSheet(
"QPushButton {background-color: green; color: white;}");
stepButton->setToolTip(
"Resume program execution for a next debugging step.");
connect(stepButton, SIGNAL(clicked()), this, SLOT(step()));
flowButtons->setContentsMargins(0, 0, 0, 0);
flowButtons->setSpacing(0);
auto *tabBar = tabWidget->getTabBar();
tabBar->setElideMode(Qt::ElideRight);
tabBar->setContextMenuPolicy(Qt::CustomContextMenu);
connect(tabBar, SIGNAL(customContextMenuRequested(QPoint)), this,
SLOT(contextMenuRequested(QPoint)));
connect(tabBar, SIGNAL(tabCloseRequested(int)), this,
SLOT(tabCloseRequested(int)));
}
void CallWindow::initFooter()
{
leftFooter = new QLabel();
rightFooter = new QLabel();
QStatusBar *bar = statusBar();
bar->addPermanentWidget(leftFooter, 2);
bar->addPermanentWidget(rightFooter, 2);
}
void CallWindow::showExitProgramButton()
{
stepButton->setVisible(false);
fastForwardButton->setVisible(false);
}
void CallWindow::addTab(CallTab *tab)
{
tabMap[tab->getId()] = tab;
QString name = QString("[%1] %2").arg(tab->getId()).arg(tab->getName());
int index =
tabWidget->addTab(tab, stfl::shortenString(name, 20, true, true));
tabWidget->getTabBar()->setTabData(index, QVariant((int)tab->getId()));
}
size_t CallWindow::getId()
{
return id;
}
void CallWindow::removeTab(CallTab *tab)
{
tabMap.erase(tabMap.find(tab->getId()));
int index = tabWidget->indexOf(tab);
tabWidget->removeTab(index);
}
void CallWindow::removeTab(size_t tabId)
{
if (hasTab(tabId))
{
removeTab(tabMap[tabId]);
}
}
void CallWindow::showTab(CallTab *tab)
{
tabWidget->setCurrentWidget(tab);
}
void CallWindow::showTab(size_t tabId)
{
if (hasTab(tabId))
{
showTab(tabMap[tabId]);
}
}
void CallWindow::updateLeftFooter(QString newText)
{
leftFooter->setText(newText);
}
void CallWindow::updateRightFooter(QString newText)
{
rightFooter->setText(newText);
}
void CallWindow::step()
{
controller->resumeProgramExecution();
}
void CallWindow::fastForward()
{
controller->setMode(controller::Mode::FAST_FORWARD);
}
void CallWindow::closeApp()
{
controller->setMode(controller::Mode::HIDE);
}
bool CallWindow::hasTab(size_t tabId)
{
return tabMap.count(tabId);
}
void CallWindow::contextMenuRequested(const QPoint &location)
{
controller->removeEmptyWindows();
auto tabBar = tabWidget->getTabBar();
int tabIndex = tabBar->tabAt(location);
if (tabIndex == tabOffset - 1)
return;
QMenu *menu = new QMenu(this);
connect(menu, SIGNAL(triggered(QAction *)), this,
SLOT(contextMenuAction(QAction *)));
auto windows = controller->getTabWindows();
menu->addAction(new QAction("Remove call", this));
menu->addAction(new QAction("Close tab", this));
menu->addAction(new QAction("Open in new window", this));
for (auto window : windows)
{
if (window->getId() != id)
{
menu->addAction(new QAction(
QString("Open in '%1'").arg(window->windowTitle()),
this));
}
}
currentContextMenuTabId = getCallTabIdByTabIndex(tabIndex);
menu->popup(tabBar->mapToGlobal(location));
}
void CallWindow::contextMenuAction(QAction *action)
{
if (currentContextMenuTabId == -1)
{
return;
}
auto text = action->text();
if (text == "Open in new window")
{
controller->moveCallTabToNewWindow(currentContextMenuTabId);
}
else if (text == "Remove call")
{
controller->removeCallTab(currentContextMenuTabId, true, true);
}
else if (text == "Close tab")
{
controller->removeCallTab(currentContextMenuTabId);
}
else
{
auto windows = controller->getTabWindows();
for (auto window : windows)
{
if (text ==
QString("Open in '%1'").arg(window->windowTitle()))
{
controller->moveCallTabToWindow(
currentContextMenuTabId, window->getId());
break;
}
}
}
currentContextMenuTabId = -1;
}
size_t CallWindow::tabCount()
{
return tabMap.size();
}
std::vector<size_t> CallWindow::getCallTabIds()
{
std::vector<size_t> ids{};
for (auto &elem : tabMap)
{
ids.push_back(elem.first);
}
return ids;
}
void CallWindow::closeEvent(QCloseEvent *event)
{
controller->removeWindowFromMaps(id);
// FIXME: tabWidget is already freed sometimes: Use-after-free Bug
tabWidget->clear();
for (auto &elem : tabMap)
{
controller->removeCallTab(elem.first, true);
}
event->accept();
}
void CallWindow::tabCloseRequested(int index)
{
if (hasTabAtIndex(index))
{
controller->removeCallTab(getCallTabIdByTabIndex(index));
}
controller->removeEmptyWindows();
}
size_t CallWindow::getCallTabIdByTabIndex(int index)
{
if (hasTabAtIndex(index))
{
auto tabData = tabWidget->getTabBar()->tabData(index);
bool ok = true;
size_t callTabId = tabData.toInt(&ok);
if (ok && tabMap.count(callTabId) > 0)
{
return callTabId;
}
}
return 0;
}
bool CallWindow::hasTabAtIndex(int index)
{
auto tabData = tabWidget->getTabBar()->tabData(index);
return tabData != 0 && !tabData.isNull() && tabData.isValid();
}
}
}
#ifndef CVVISUAL_CALLWINDOW_HPP
#define CVVISUAL_CALLWINDOW_HPP
#include <vector>
#include <map>
#include <QTabWidget>
#include <QMainWindow>
#include <QString>
#include <vector>
#include <QLabel>
#include <QKeyEvent>
#include <QPoint>
#include <QCloseEvent>
#include <QPushButton>
#include "call_tab.hpp"
#include "../controller/view_controller.hpp"
#include "../util/util.hpp"
#include "tabwidget.hpp"
namespace cvv
{
namespace controller
{
class ViewController;
}
namespace gui
{
/**
* @brief Window inheriting some call tabs with in a tab widget.
*/
class CallWindow : public QMainWindow
{
Q_OBJECT
public:
/**
* @brief Contructs a new call window.
* @param controller view controller that this window belongs to
* @param id id of the window
*/
CallWindow(util::Reference<controller::ViewController> controller,
size_t id);
/**
* @brief Shows an "Exit program" button.
*/
void showExitProgramButton();
/**
* @brief Add a new tab to the inherited tab widget.
* @param tab new tab
*/
void addTab(CallTab *tab);
/**
* @brief Get the id of this window.
* @return id of this window.
*/
size_t getId();
/**
* @brief Remove the given tab from this window.
* @param given tab to remove
*/
void removeTab(CallTab *tab);
/**
* @brief Remove the given tab from this window.
* @param id of the given tab
*/
void removeTab(size_t tabId);
/**
* @brief Show the given tab.
* @param given tab
*/
void showTab(CallTab *tab);
/**
* @brief Show the given tab.
* @param id of the given tab
*/
void showTab(size_t tabId);
/**
* @brief Examines whether or not the given is inherited in this window.
* @param id of the given tab
*/
bool hasTab(size_t tabId);
/**
* @brief Returns the number of tabs shown in this window.
* @return number of tabs
*/
size_t tabCount();
/**
* @brief Returns the ids of the available call tabs.
* @return available call tabs' ids
*/
std::vector<size_t> getCallTabIds();
public slots:
/**
* @brief Update the left footer with the given text.
* @param newText given text
*/
void updateLeftFooter(QString newText);
/**
* @brief Update the right footer with the given text.
* @param newText given text
*/
void updateRightFooter(QString newText);
private slots:
void contextMenuRequested(const QPoint &location);
void contextMenuAction(QAction *action);
void tabCloseRequested(int index);
void step();
void fastForward();
void closeApp();
protected:
size_t id;
util::Reference<controller::ViewController> controller;
TabWidget *tabWidget;
QMainWindow *window;
QPushButton *closeButton;
QPushButton *stepButton;
QPushButton *fastForwardButton;
std::map<size_t, CallTab *> tabMap;
QLabel *leftFooter;
QLabel *rightFooter;
int currentContextMenuTabId = -1;
int tabOffset = 0;
void initMenu();
void initTabs();
void initFooter();
void closeEvent(QCloseEvent *event);
size_t getCallTabIdByTabIndex(int index);
bool hasTabAtIndex(int index);
};
}
}
#endif
#ifndef CVVISUAL_FILTER_CALL_TAB_HPP
#define CVVISUAL_FILTER_CALL_TAB_HPP
#include <QString>
#include <QWidget>
#include "multiview_call_tab.hpp"
#include "../view/filter_view.hpp"
#include "../impl/filter_call.hpp"
namespace cvv
{
namespace gui
{
/** Filter Call Tab.
* @brief Inner part of a tab, contains a FilterView.
* The inner part of a tab or window
* containing a FilterView.
* Allows to switch views and to access the help.
*/
class FilterCallTab
: public MultiViewCallTab<cvv::view::FilterView, cvv::impl::FilterCall>
{
Q_OBJECT
public:
/**
* @brief Short constructor named after the Call, using the requested View
* from the Call or, if no or invalid request, default view.
* Initializes the FilterCallTab with the requested or default view and names it
* after the associated FilterCall.
* @param filterCall - the FilterCall containing the information to be
* visualized.
*/
FilterCallTab(const cvv::impl::FilterCall &filterCall)
: FilterCallTab{
filterCall, filterCall.requestedView()
}
{
}
/**
* @brief Constructor with possibility to select view.
* Note that the default view is still created first.
* @param call - the MatchCall containing the information to be
* visualized.
* @param filterViewId - ID of the View to be set up. If a view of this name does
* not exist, the default view will be used.
*/
FilterCallTab(const cvv::impl::FilterCall &filterCall, const QString& filterViewId)
: MultiViewCallTab<cvv::view::FilterView, cvv::impl::FilterCall>{
filterCall, filterViewId, QString{ "default_filter_view" }, QString{ "DefaultFilterView" }
}
{
}
~FilterCallTab()
{
}
/**
* @brief Register the template class to the map of FilterViews.
* View needs to offer a constructor of the form View(const
* cvv::impl::FilterCall&, QWidget*).
* @param name to register the class under.
* @tparam View - Class to register.
* @return true when the view was registered and false when the name was
* already taken.
*/
template <class View>
static bool registerFilterView(const QString &name)
{
return registerView<View>(name);
}
};
}
} // namespaces
#endif
#include <QString>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include "image_call_tab.hpp"
#include "../view/image_view.hpp"
#include "../controller/view_controller.hpp"
#include "../impl/single_image_call.hpp"
#include "../qtutil/util.hpp"
namespace cvv
{
namespace gui
{
ImageCallTab::ImageCallTab(const cvv::impl::SingleImageCall &call)
: imageCall_{ call }
{
setName(imageCall_->description());
createGui();
}
ImageCallTab::ImageCallTab(const QString &tabName,
const cvv::impl::SingleImageCall &call)
: imageCall_{ call }
{
setName(tabName);
createGui();
}
void ImageCallTab::helpButtonClicked() const
{
cvv::qtutil::openHelpBrowser("SingleImageView");
}
size_t ImageCallTab::getId() const
{
return imageCall_->getId();
}
void ImageCallTab::createGui()
{
hlayout_ = new QHBoxLayout{ this };
hlayout_->setAlignment(Qt::AlignTop);
hlayout_->addWidget(new QLabel{ "Single Image View" });
helpButton_ = new QPushButton{ "Help", this };
hlayout_->addWidget(helpButton_);
connect(helpButton_, SIGNAL(clicked()), this,
SLOT(helpButtonClicked()));
upperBar_ = new QWidget{ this };
upperBar_->setLayout(hlayout_);
vlayout_ = new QVBoxLayout{ this };
vlayout_->addWidget(upperBar_);
setView();
setLayout(vlayout_);
imageView_->showFullImage();
}
void ImageCallTab::setView()
{
imageView_ = new cvv::view::ImageView{ imageCall_->mat(), this };
vlayout_->addWidget(imageView_);
}
}
} // namespaces
#ifndef CVVISUAL_IMAGE_CALL_TAB_HPP
#define CVVISUAL_IMAGE_CALL_TAB_HPP
#include <QHBoxLayout>
#include <QString>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include "call_tab.hpp"
#include "../view/image_view.hpp"
#include "../controller/view_controller.hpp"
#include "../impl/single_image_call.hpp"
namespace cvv
{
namespace gui
{
/** Single Image Call Tab.
* @brief Inner part of a tab, contains an IageView.
* The inner part of a tab or window
* containing an ImageView.
* Allows to access the help.
*/
class ImageCallTab : public CallTab
{
Q_OBJECT
public:
/**
* @brief Short constructor named after the Call.
* Initializes the ImageCallTab and names it after the associated
* FilterCall.
* @param call the SingleImageCall containing the information to be
* visualized.
*/
ImageCallTab(const cvv::impl::SingleImageCall &call);
/**
* @brief Constructor using default view.
* Short constructor..
* @param tabName.
* @param call the SingleImageCall containing the information to be
* visualized.
* @attention might be deleted.
*/
ImageCallTab(const QString &tabName,
const cvv::impl::SingleImageCall &call);
/**
* @brief get ID.
* @return the ID of the CallTab.
* (ID is equal to the ID of the associated call).
* Overrides CallTab's getId.
*/
size_t getId() const override;
private
slots:
/**
* @brief Help Button clicked.
* Called when the help button is clicked.
*/
void helpButtonClicked() const;
private:
/**
* @brief Sets up the visible parts.
* Called by the constructors.
*/
void createGui();
/**
* @brief sets up View referred to by viewId.
* @param viewId ID of the view to be set.
* @throw std::out_of_range if no view named viewId was registered.
*/
void setView();
util::Reference<const cvv::impl::SingleImageCall> imageCall_;
cvv::view::ImageView *imageView_;
QPushButton *helpButton_;
QHBoxLayout *hlayout_;
QVBoxLayout *vlayout_;
QWidget *upperBar_;
};
}
} // namespaces
#endif
#include "main_call_window.hpp"
#include <QApplication>
#include <QPoint>
#include "../util/util.hpp"
#include "../stfl/stringutils.hpp"
namespace cvv
{
namespace gui
{
MainCallWindow::MainCallWindow(
util::Reference<controller::ViewController> controller, size_t id,
OverviewPanel *ovPanel)
: CallWindow(controller, id), ovPanel{ ovPanel }
{
tabOffset = 1;
QString name = "Overview";
tabWidget->insertTab(0, ovPanel, name);
auto *tabBar = tabWidget->getTabBar();
tabBar->tabButton(0, QTabBar::RightSide)->hide();
setWindowTitle(QString("CVVisual | main window"));
}
void MainCallWindow::showOverviewTab()
{
tabWidget->setCurrentWidget(ovPanel);
}
void MainCallWindow::closeEvent(QCloseEvent *event)
{
(void)event;
controller->setMode(controller::Mode::HIDE);
}
}
}
#ifndef CVVISUAL_MAINCALLWINDOW_HPP
#define CVVISUAL_MAINCALLWINDOW_HPP
#include <memory>
#include <QCloseEvent>
#include "call_window.hpp"
#include "overview_panel.hpp"
#include "../controller/view_controller.hpp"
#include "../util/util.hpp"
namespace cvv
{
namespace controller
{
class ViewController;
}
namespace gui
{
class OverviewPanel;
/**
* @brief A call window also inheriting the overview panel.
*/
class MainCallWindow : public CallWindow
{
Q_OBJECT
public:
/**
* @brief Constructs a new main call window.
* @param controller view controller inheriting this main window
* @param id id of this main window
* @param ovPanel inherited overview panel
*/
MainCallWindow(util::Reference<controller::ViewController> controller,
size_t id, OverviewPanel *ovPanel);
~MainCallWindow()
{
}
/**
* @brief Show the overview tab.
*/
void showOverviewTab();
/**
* @brief Hides the close window.
*/
void hideCloseWindow();
protected:
void closeEvent(QCloseEvent *event);
private:
OverviewPanel *ovPanel;
};
}
}
#endif
#ifndef CVVISUAL_MATCH_CALL_TAB_HPP
#define CVVISUAL_MATCH_CALL_TAB_HPP
#include <memory>
#include <QString>
#include <QWidget>
#include "multiview_call_tab.hpp"
#include "../view/match_view.hpp"
#include "../impl/match_call.hpp"
#include "../util/util.hpp"
namespace cvv
{
namespace gui
{
/** Match Call Tab.
* @brief Inner part of a tab, contains a MatchView.
* The inner part of a tab or window
* containing a MatchView.
* Allows to switch views and to access the help.
*/
class MatchCallTab
: public MultiViewCallTab<cvv::view::MatchView, cvv::impl::MatchCall>
{
Q_OBJECT
public:
/**
* @brief Short constructor named after Call and using the requested View
* from the Call or, if no or invalid request, default view.
* Initializes the MatchCallTab with the requested or default view and names it after
* the associated MatchCall.
* @param matchCall - the MatchCall containing the information to be
* visualized.
*/
MatchCallTab(const cvv::impl::MatchCall &matchCall)
: MatchCallTab{
matchCall, matchCall.requestedView()
}
{
}
/**
* @brief Constructor with possibility to select view.
* Note that the default view is still created first.
* @param matchCall - the MatchCall containing the information to be
* visualized.
* @param matchViewId - ID of the View to be set up. If a view of this name does
* not exist, the default view will be used.
*/
MatchCallTab(const cvv::impl::MatchCall& matchCall, const QString& matchViewId)
: MultiViewCallTab<cvv::view::MatchView, cvv::impl::MatchCall>{
matchCall, matchViewId, QString{ "default_match_view" }, QString{ "LineMatchView" }
}
{
oldView_ = view_;
connect(&this->viewSet, SIGNAL(signal()), this, SLOT(viewChanged()));
}
~MatchCallTab()
{
}
/**
* @brief Register the template class to the map of MatchViews.
* View needs to offer a constructor of the form View(const
* cvv::impl::MatchCall&, QWidget*).
* @param name to register the class under.
* @tparam View - Class to register.
* @return true when the view was registered and false when the name was
* already taken.
*/
template <class View> static bool registerMatchView(const QString &name)
{
return registerView<View>(name);
}
private slots:
/**
* @brief Slot called when the view has completely changed.
*/
void viewChanged()
{
if(oldView_ != nullptr)
{
view_->setKeyPointSelection(oldView_->getKeyPointSelection());
view_->setMatchSelection(oldView_->getMatchSelection());
}
oldView_ = view_;
}
private:
/**
* @brief usually equal to view_, but not immediately changed when view_ is changed.
*/
cvv::view::MatchView* oldView_;
};
}
} // namespaces
#endif
#ifndef CVVISUAL_MULTIVIEW_CALL_TAB_HPP
#define CVVISUAL_MULTIVIEW_CALL_TAB_HPP
#include <vector>
#include <memory>
#include <QObject>
#include <QString>
#include <QMap>
#include <QPushButton>
#include <QComboBox>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include "call_tab.hpp"
#include "../util/util.hpp"
#include "../qtutil/registerhelper.hpp"
#include "../qtutil/signalslot.hpp"
#include "../qtutil/util.hpp"
namespace cvv
{
namespace gui
{
/** Call Tab for multiple views.
* @brief Inner part of a tab, contains a View.
* The inner part of a tab or window
* containing a View.
* Allows to switch between different views and to access the help.
* @tparam ViewType A type of View.
* @tparam CallType A type of Call.
*/
template <class ViewType, class CallType>
class MultiViewCallTab
: public CallTab,
public cvv::qtutil::RegisterHelper<ViewType, const CallType &, QWidget *>
{
public:
/**
* @brief Short constructor named after Call and using the default view.
* Initializes the MultiViewCallTab with the default view and names it after
* the associated Call.
* @param call - the Call containing the information to be
* visualized.
* @param default_key - Key under which the default view is to be saved.
* @param standard_default - Standard default view.
*/
MultiViewCallTab(const CallType &call, const QString& default_key, const QString& standard_default)
: MultiViewCallTab{ call.description(), call, default_key, standard_default }
{
}
/**
* @brief Constructor using the default view.
* Initializes the MultiViewCallTab with the default view.
* @param name - Name to give the CallTab.
* @param call - the Call containing the information to be
* visualized.
* @param default_key - Key under which the default view is to be saved.
* @param standard_default - Standard default view.
*/
MultiViewCallTab(const QString &tabName, const CallType &call, const QString& default_key, const QString& standard_default)
: call_{ call }, currentIndexChanged{ [&]()
{
vlayout_->removeWidget(view_);
view_->setVisible(false);
setView();
} },
helpButtonClicked{ [&]()
{ qtutil::openHelpBrowser(viewId_); } },
setAsDefaultButtonClicked{ [&]()
{ qtutil::setSetting(default_scope_, default_key_, viewId_); } }
{
setName(tabName);
default_scope_ = QString{ "default_views" };
default_key_ = default_key;
standard_default_ = standard_default;
// Sets standard_default_ as default in case no other default is
// set:
qtutil::setDefaultSetting(default_scope_, default_key_,
standard_default_);
viewId_ = qtutil::getSetting(default_scope_, default_key_);
createGui();
}
/**
* @brief Constructor with possibility to select view.
* Note that the default view is still created first.
* @param call - the Call containing the information to be
* visualized.
* @param viewId - ID of the View to be set up. If a view of this name does
* not exist, the default view will be used.
* @param default_key - Key under which the default view is to be saved.
* @param standard_default - Standard default view.
*/
MultiViewCallTab(const CallType& call, const QString& viewId, const QString& default_key, const QString& standard_default)
: MultiViewCallTab{call, default_key, standard_default}
{
this->select(viewId);
}
~MultiViewCallTab()
{
}
/**
* @brief get ID.
* @return the ID of the CallTab.
* (ID is equal to the ID of the associated call).
* Overrides CallTab's getId.
*/
size_t getId() const override
{
return call_->getId();
}
/**
* @brief Register the template class to the map of Views.
* View needs to offer a constructor of the form View(const
* cvv::impl::CallType&, QWidget*).
* @param name to register the class under.
* @tparam View - Class to register.
* @return true when the view was registered and false when the name was
* already taken.
*/
template <class View> static bool registerView(const QString &name)
{
return MultiViewCallTab<ViewType, CallType>::registerElement(
name, [](const CallType &call, QWidget *parent)
{
return cvv::util::make_unique<View>(call, parent);
});
}
protected:
/**
* @brief Scope to search the default view in.
*/
QString default_scope_;
/**
* @brief Key under which the default view is saved.
*/
QString default_key_;
/**
* @brief standard default view.
*/
QString standard_default_;
/**
* @brief Sets up the visible parts.
* Called by the constructors.
*/
void createGui()
{
if (!this->select(viewId_))
{
this->select(standard_default_);
viewId_ = this->selection();
setAsDefaultButtonClicked.slot(); // Set as default.
/* If viewId_ does not name a valid View, it will be
* attempted to set standard_default_.
* If that was not registered either, the current
* selection of the ComboBox will be used automatically.
* Whichever was chosen will be set as the new default.
*/
}
hlayout_ = new QHBoxLayout{};
hlayout_->setAlignment(Qt::AlignTop | Qt::AlignRight);
hlayout_->addWidget(new QLabel{ "View:" });
hlayout_->addWidget(this->comboBox_);
setAsDefaultButton_ = new QPushButton{ "Set as default", this };
hlayout_->addWidget(setAsDefaultButton_);
helpButton_ = new QPushButton{ "Help", this };
hlayout_->addWidget(helpButton_);
upperBar_ = new QWidget{ this };
upperBar_->setLayout(hlayout_);
vlayout_ = new QVBoxLayout{};
vlayout_->addWidget(upperBar_);
setView();
setLayout(vlayout_);
QObject::connect(setAsDefaultButton_, SIGNAL(clicked()),
&setAsDefaultButtonClicked, SLOT(slot()));
QObject::connect(helpButton_, SIGNAL(clicked()),
&helpButtonClicked, SLOT(slot()));
QObject::connect(&this->signalElementSelected(),
SIGNAL(signal(QString)), &currentIndexChanged,
SLOT(slot()));
}
/**
* @brief sets up the View currently selected in the ComboBox inherited
* from RegisterHelper.
*/
void setView()
{
viewId_ = this->selection();
if (viewHistory_.count(this->selection()))
{
view_ = viewHistory_.at(this->selection());
vlayout_->addWidget(view_);
view_->setVisible(true);
}
else
{
viewHistory_.emplace(
this->selection(),
((*this)()(*call_, this).release()));
view_ = viewHistory_.at(this->selection());
vlayout_->addWidget(view_);
}
viewSet.emitSignal();
}
util::Reference<const CallType> call_;
QString viewId_;
ViewType *view_;
std::map<QString, ViewType *> viewHistory_;
QPushButton *helpButton_;
QPushButton *setAsDefaultButton_;
QHBoxLayout *hlayout_;
QVBoxLayout *vlayout_;
QWidget *upperBar_;
//signals:
/**
* @brief signal emitted whem view is completely set up.
*/
qtutil::Signal viewSet;
// slots:
/**
* @brief View selection change.
* Slot called when the index of the view selection changes.
*/
qtutil::Slot currentIndexChanged;
/**
* @brief Help Button clicked.
* Called when the help button is clicked.
*/
const qtutil::Slot helpButtonClicked;
/**
* @brief setAsDefaultButton clicked.
* Called when the setAsDefaultButton,which sets the current view as
* default, is clicked.
*/
qtutil::Slot setAsDefaultButtonClicked;
};
}
} // namespaces
#endif
#include "overview_group_subtable.hpp"
#include <utility>
#include <algorithm>
#include <sstream>
#include <QVBoxLayout>
#include <QStringList>
#include <QModelIndex>
#include <QMenu>
#include <QAction>
#include <QHeaderView>
#include <QList>
#include <QSize>
#include "call_window.hpp"
#include "overview_table.hpp"
#include "../controller/view_controller.hpp"
namespace cvv
{
namespace gui
{
OverviewGroupSubtable::OverviewGroupSubtable(
util::Reference<controller::ViewController> controller,
OverviewTable *parent, stfl::ElementGroup<OverviewTableRow> group)
: controller{ controller }, parent{ parent }, group{ std::move(group) }
{
controller->setDefaultSetting("overview", "imgsize",
QString::number(100));
initUI();
}
void OverviewGroupSubtable::initUI()
{
controller->setDefaultSetting("overview", "imgzoom", "30");
qTable = new QTableWidget(this);
qTable->setSelectionBehavior(QAbstractItemView::SelectRows);
qTable->setSelectionMode(QAbstractItemView::SingleSelection);
qTable->setTextElideMode(Qt::ElideNone);
auto verticalHeader = qTable->verticalHeader();
verticalHeader->setVisible(false);
auto horizontalHeader = qTable->horizontalHeader();
horizontalHeader->setSectionResizeMode(QHeaderView::ResizeToContents);
horizontalHeader->setStretchLastSection(false);
connect(qTable, SIGNAL(cellDoubleClicked(int, int)), this,
SLOT(rowClicked(int, int)));
qTable->setContextMenuPolicy(Qt::CustomContextMenu);
connect(qTable, SIGNAL(customContextMenuRequested(QPoint)), this,
SLOT(customMenuRequested(QPoint)));
auto *layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(qTable);
setLayout(layout);
updateUI();
}
void OverviewGroupSubtable::updateUI()
{
imgSize = controller->getSetting("overview", "imgzoom").toInt() *
width() / 400;
QStringList list{};
list << "ID";
maxImages = 0;
for (auto element : group.getElements())
{
if (maxImages < element.call()->matrixCount())
{
maxImages = element.call()->matrixCount();
}
}
if (parent->isShowingImages())
{
for (auto element : group.getElements())
{
if (maxImages < element.call()->matrixCount())
{
maxImages = element.call()->matrixCount();
}
}
for (size_t i = 0; i < maxImages; i++)
{
list << QString("Image ") + QString::number(i + 1);
}
}
list << "Description"
<< "Function"
<< "File"
<< "Line"
<< "Type";
qTable->setRowCount(group.size());
qTable->setColumnCount(list.size());
qTable->setHorizontalHeaderLabels(list);
int textRowHeight = qTable->fontMetrics().height() + 5;
if (textRowHeight >= imgSize)
{
qTable->setVerticalScrollMode(QAbstractItemView::ScrollPerItem);
}
else
{
qTable->setVerticalScrollMode(
QAbstractItemView::ScrollPerPixel);
}
rowHeight = std::max(imgSize, textRowHeight);
for (size_t i = 0; i < group.size(); i++)
{
group.get(i).addToTable(qTable, i, parent->isShowingImages(),
maxImages, imgSize, imgSize);
qTable->setRowHeight(i, rowHeight);
}
auto header = qTable->horizontalHeader();
header->setSectionResizeMode(0, QHeaderView::ResizeToContents);
for (size_t i = 1; i < maxImages + 1; i++)
{
header->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
for (size_t i = maxImages + 1; i < maxImages + 4; i++)
{
header->setSectionResizeMode(i, QHeaderView::Stretch);
}
header->setSectionResizeMode(maxImages + 4,
QHeaderView::ResizeToContents);
header->setSectionResizeMode(maxImages + 5,
QHeaderView::ResizeToContents);
updateMinimumSize();
}
void OverviewGroupSubtable::rowClicked(int row, int collumn)
{
(void)collumn;
size_t tabId = group.get(row).id();
controller->showAndOpenCallTab(tabId);
}
void OverviewGroupSubtable::customMenuRequested(QPoint location)
{
if (qTable->rowCount() == 0)
{
return;
}
controller->removeEmptyWindows();
QMenu *menu = new QMenu(this);
auto windows = controller->getTabWindows();
menu->addAction(new QAction("Open in new window", this));
for (auto window : windows)
{
menu->addAction(new QAction(
QString("Open in '%1'").arg(window->windowTitle()), this));
}
menu->addAction(new QAction("Remove call", this));
QModelIndex index = qTable->indexAt(location);
if (!index.isValid())
{
return;
}
int row = index.row();
QString idStr = qTable->item(row, 0)->text();
connect(menu, SIGNAL(triggered(QAction *)), this,
SLOT(customMenuAction(QAction *)));
std::stringstream{ idStr.toStdString() } >> currentCustomMenuCallTabId;
currentCustomMenuCallTabIdValid = true;
menu->popup(mapToGlobal(location));
}
void OverviewGroupSubtable::customMenuAction(QAction *action)
{
if (!currentCustomMenuCallTabIdValid)
{
return;
}
QString actionText = action->text();
if (actionText == "Open in new window")
{
controller->moveCallTabToNewWindow(currentCustomMenuCallTabId);
currentCustomMenuCallTabId = -1;
return;
}
else if (actionText == "Remove call")
{
controller->removeCallTab(currentCustomMenuCallTabId, true,
true);
currentCustomMenuCallTabId = -1;
return;
}
auto windows = controller->getTabWindows();
for (auto window : windows)
{
if (actionText ==
QString("Open in '%1'").arg(window->windowTitle()))
{
controller->moveCallTabToWindow(
currentCustomMenuCallTabId, window->getId());
break;
}
}
currentCustomMenuCallTabId = -1;
}
void OverviewGroupSubtable::resizeEvent(QResizeEvent *event)
{
(void)event;
imgSize = controller->getSetting("overview", "imgzoom").toInt() *
width() / 400;
rowHeight = std::max(imgSize, qTable->fontMetrics().height() + 5);
for (size_t row = 0; row < group.size(); row++)
{
group.get(row).resizeInTable(qTable, row,
parent->isShowingImages(), maxImages,
imgSize, imgSize);
qTable->setRowHeight(row, rowHeight);
}
updateMinimumSize();
event->accept();
}
void OverviewGroupSubtable::removeRow(size_t id)
{
for (size_t i = 0; i < group.size(); i++)
{
if (group.get(i).id() == id)
{
group.removeElement(i);
updateUI();
break;
}
}
}
bool OverviewGroupSubtable::hasRow(size_t id)
{
for (size_t i = 0; i < group.size(); i++)
{
if (group.get(i).id() == id)
{
return true;
}
}
return false;
}
void OverviewGroupSubtable::setRowGroup(
stfl::ElementGroup<OverviewTableRow> &newGroup)
{
auto compFunc = [](const OverviewTableRow &first,
const OverviewTableRow &second)
{ return first.id() == second.id(); };
if (group.hasSameElementList(newGroup, compFunc))
{
return;
}
// Now both groups aren't the same
size_t newMax = 0;
for (auto row : newGroup.getElements())
{
if (row.call()->matrixCount() > newMax)
{
newMax = row.call()->matrixCount();
}
}
if (newMax == maxImages)
{
group = newGroup;
updateUI();
return;
}
// Now both groups have the same maximum number of images within their
// elements
size_t minLength = std::min(group.size(), newGroup.size());
for (size_t i = 0; i < minLength; i++)
{
if (group.get(i).id() != newGroup.get(i).id())
{
group = newGroup;
updateUI();
return;
}
}
// Now the bigger group's element lists starts with the smaller group's
// one
if (group.size() < newGroup.size())
{
// Now the new group appends the current group
for (size_t row = group.size(); row < newGroup.size(); row++)
{
newGroup.get(row)
.addToTable(qTable, row, parent->isShowingImages(),
maxImages, imgSize, imgSize);
qTable->setRowHeight(row, rowHeight);
}
}
else
{
// Now the new group deletes elements from the current group
for (size_t row = group.size() - 1; row >= newGroup.size();
row--)
{
qTable->removeRow(row);
}
}
group = newGroup;
updateMinimumSize();
}
void OverviewGroupSubtable::updateMinimumSize()
{
int width = qTable->sizeHint().width();
setMinimumWidth(width);
int height = qTable->horizontalHeader()->height();
for (int a = 0; a < qTable->rowCount(); ++a)
{
height += qTable->rowHeight(a);
}
setMinimumHeight(height + (qTable->rowCount() * 1));
}
}
}
#ifndef CVVISUAL_OVERVIEW_GROUP_SUBTABLE_HPP
#define CVVISUAL_OVERVIEW_GROUP_SUBTABLE_HPP
#include <memory>
#include <QWidget>
#include <QTableWidget>
#include <QAction>
#include <QResizeEvent>
#include "../stfl/element_group.hpp"
#include "overview_table_row.hpp"
#include "../util/util.hpp"
#include "../controller/view_controller.hpp"
namespace cvv
{
namespace controller
{
class ViewController;
}
}
namespace cvv
{
namespace gui
{
class OverviewTable;
/**
* @brief A table for the a group of overview data sets.
*/
class OverviewGroupSubtable : public QWidget
{
Q_OBJECT
public:
/**
* @brief Constructs an over group subtable.
* @param controller view controller
* @param parent parent table
* @param group the displayed group of overview data sets
*/
OverviewGroupSubtable(
util::Reference<controller::ViewController> controller,
OverviewTable *parent, stfl::ElementGroup<OverviewTableRow> group);
~OverviewGroupSubtable()
{
}
/**
* @brief Updates the displayed table UI.
*/
void updateUI();
/**
* @brief Remove the row with the given id.
* @param given table row id
*/
void removeRow(size_t id);
/**
* @brief Checks whether or not the table shows the row with the given
* id.
* @param id given row id
* @return Does the table show the row with the given id?
*/
bool hasRow(size_t id);
/**
* @brief Set the displayed rows.
* @note This method does some optimisations to only fully rebuild all
* rows if neccessary.
* @param newGroup new group of rows that will be displayed
*/
void setRowGroup(stfl::ElementGroup<OverviewTableRow> &newGroup);
protected:
void resizeEvent(QResizeEvent *event);
private slots:
void rowClicked(int row, int collumn);
void customMenuRequested(QPoint location);
void customMenuAction(QAction *action);
private:
util::Reference<controller::ViewController> controller;
OverviewTable *parent;
stfl::ElementGroup<OverviewTableRow> group;
QTableWidget *qTable;
size_t currentCustomMenuCallTabId = 0;
bool currentCustomMenuCallTabIdValid = false;
size_t maxImages = 0;
int imgSize = 0;
int rowHeight = 0;
void initUI();
void updateMinimumSize();
};
}
}
#endif
#include "overview_panel.hpp"
#include <functional>
#include <math.h>
#include <memory>
#include <iostream>
#include <QMap>
#include <QSet>
#include <QString>
#include <QVBoxLayout>
#include <QWidget>
#include <QScrollArea>
#include "../controller/view_controller.hpp"
#include "../qtutil/stfl_query_widget.hpp"
#include "../qtutil/util.hpp"
#include "../stfl/element_group.hpp"
namespace cvv
{
namespace gui
{
OverviewPanel::OverviewPanel(
util::Reference<controller::ViewController> controller)
: controller{ controller }
{
qtutil::setDefaultSetting("overview", "imgzoom", "20");
QVBoxLayout *layout = new QVBoxLayout{};
setLayout(layout);
layout->setContentsMargins(0, 0, 0, 0);
queryWidget = new qtutil::STFLQueryWidget();
layout->addWidget(queryWidget);
table = new OverviewTable(controller);
layout->addWidget(table);
auto bottomArea = new QWidget{ this };
auto bottomLayout = new QHBoxLayout;
imgSizeSliderLabel = new QLabel{ "Zoom", bottomArea };
imgSizeSliderLabel->setMaximumWidth(50);
imgSizeSliderLabel->setAlignment(Qt::AlignRight);
bottomLayout->addWidget(imgSizeSliderLabel);
imgSizeSlider = new QSlider{ Qt::Horizontal, bottomArea };
imgSizeSlider->setMinimumWidth(50);
imgSizeSlider->setMaximumWidth(200);
imgSizeSlider->setMinimum(0);
imgSizeSlider->setMaximum(100);
imgSizeSlider->setSliderPosition(
qtutil::getSetting("overview", "imgzoom").toInt());
connect(imgSizeSlider, SIGNAL(valueChanged(int)), this,
SLOT(imgSizeSliderAction()));
bottomLayout->addWidget(imgSizeSlider);
bottomArea->setLayout(bottomLayout);
layout->addWidget(bottomArea);
initEngine();
connect(queryWidget, SIGNAL(showHelp(QString)), this,
SLOT(showHelp(QString)));
// connect(queryWidget, SIGNAL(userInputUpdate(QString)), this,
// SLOT(updateQuery(QString)));
connect(queryWidget, SIGNAL(filterSignal(QString)), this,
SLOT(filterQuery(QString)));
connect(queryWidget, SIGNAL(requestSuggestions(QString)), this,
SLOT(requestSuggestions(QString)));
}
void OverviewPanel::initEngine()
{
// raw and description filter
auto rawFilter = [](const OverviewTableRow &elem)
{
return elem.description();
};
queryEngine.addStringCmdFunc("raw", rawFilter, false);
queryEngine.addStringCmdFunc("description", rawFilter, false);
// file filter
queryEngine.addStringCmdFunc("file", [](const OverviewTableRow &elem)
{
return elem.file();
});
// function filter
queryEngine.addStringCmdFunc("function",
[](const OverviewTableRow &elem)
{
return elem.function();
});
// line filter
queryEngine.addIntegerCmdFunc("line", [](const OverviewTableRow &elem)
{ return elem.line(); });
// id filter
queryEngine.addIntegerCmdFunc("id", [](const OverviewTableRow &elem)
{
return elem.id();
});
// type filter
queryEngine.addStringCmdFunc("type", [](const OverviewTableRow &elem)
{
return elem.type();
});
//"number of images" filter
queryEngine.addIntegerCmdFunc("image_count",
[](const OverviewTableRow &elem)
{
return elem.call()->matrixCount();
});
//additional commands
//open call command
queryEngine.addAdditionalCommand("open",
[&](QStringList args, std::vector<stfl::ElementGroup<OverviewTableRow>>& groups)
{
openCommand(args, groups);
}, {"first_of_group", "last_of_group", "shown"});
}
void OverviewPanel::openCommand(QStringList args,
std::vector<stfl::ElementGroup<OverviewTableRow>>& groups)
{
if (args.contains("shown"))
{
for (auto &group : groups)
{
for (auto &elem : group.getElements())
{
controller->openCallTab(elem.id());
}
}
return;
}
if (args.contains("first_of_group") || args.contains("last_of_group"))
{
bool first = args.contains("first_of_group") ;
for (auto &group : groups)
{
if (group.size() > 0)
{
size_t index = first ? 0 : group.size() - 1;
size_t id = group.get(index).id();
controller->openCallTab(id);
}
}
}
}
void OverviewPanel::addElement(const util::Reference<const impl::Call> newCall)
{
OverviewTableRow row(newCall);
queryEngine.addNewElement(row);
table->updateRowGroups(queryEngine.reexecuteLastQuery());
}
void OverviewPanel::addElementBuffered(const util::Reference<const impl::Call> newCall)
{
elementBuffer.push_back(newCall);
}
void OverviewPanel::flushElementBuffer()
{
std::vector<OverviewTableRow> rows;
for (const util::Reference<const impl::Call> call : elementBuffer)
{
rows.push_back(OverviewTableRow(call));
}
queryEngine.addElements(std::move(rows));
table->updateRowGroups(queryEngine.reexecuteLastQuery());
elementBuffer.clear();
}
void OverviewPanel::removeElement(size_t id)
{
queryEngine.removeElements([id](OverviewTableRow elem)
{
return elem.id() == id;
});
table->removeElement(id);
}
void OverviewPanel::filterQuery(QString query)
{
table->updateRowGroups(queryEngine.query(query));
}
void OverviewPanel::updateQuery(QString query)
{
filterQuery(query);
}
void OverviewPanel::requestSuggestions(QString query)
{
queryWidget->showSuggestions(queryEngine.getSuggestions(query));
}
void OverviewPanel::imgSizeSliderAction()
{
controller->setSetting("overview", "imgzoom",
QString::number(imgSizeSlider->value()));
table->updateUI();
}
void OverviewPanel::showHelp(QString topic)
{
controller->openHelpBrowser(topic);
}
}
}
#ifndef CVVISUAL_OVERVIEWPANEL_HPP
#define CVVISUAL_OVERVIEWPANEL_HPP
#include <vector>
#include <QWidget>
#include <QString>
#include <QSlider>
#include <QLabel>
#include <QPushButton>
#include "../stfl/stfl_engine.hpp"
#include "../impl/call.hpp"
#include "overview_table.hpp"
#include "overview_table_row.hpp"
#include "../util/util.hpp"
#include "../controller/view_controller.hpp"
namespace cvv
{
namespace controller
{
class ViewController;
}
namespace qtutil
{
class STFLQueryWidget;
}
namespace gui
{
class OverviewTable;
class OverviewTableRow;
/**
* @brief The overview showing a filterable table displaying the different
* calls.
*/
class OverviewPanel : public QWidget
{
Q_OBJECT
public:
/**
* @brief Contructs an OverviewPanel.
* @param controller ViewController that inherits this overview
*/
OverviewPanel(util::Reference<controller::ViewController> controller);
/**
* @brief Adds the given call to the shown overview table.
* @param newCall given call
*/
void addElement(const util::Reference<const impl::Call> newCall);
/**
* @brief Changes the "Resume program execution" button label to "Exit
* Application."
*/
void showExitApplicationButton();
/**
* @brief Adds the given call buffered to the shown overview table.
* @note Be sure to flush the buffer via flushElementBuffer() later.
* @param newCall given call
*/
void addElementBuffered(const util::Reference<const impl::Call> newCall);
/**
* @brief Flushes the element buffer and shows its elements in the overview table.
*/
void flushElementBuffer();
/**
* @brief Removes and deletes the element with the given id.
* @param id given element id
*/
void removeElement(size_t id);
private slots:
void filterQuery(QString query);
void updateQuery(QString query);
void requestSuggestions(QString query);
void imgSizeSliderAction();
void showHelp(QString topic);
private:
stfl::STFLEngine<OverviewTableRow> queryEngine{"Overview"};
qtutil::STFLQueryWidget *queryWidget;
OverviewTable *table;
util::Reference<controller::ViewController> controller;
QLabel *imgSizeSliderLabel;
QSlider *imgSizeSlider;
std::vector<util::Reference<const impl::Call>> elementBuffer;
void initEngine();
void openCommand(QStringList args,
std::vector<stfl::ElementGroup<OverviewTableRow>>& groups);
};
}
}
#endif
#include "overview_table.hpp"
#include <utility>
#include <algorithm>
#include <QVBoxLayout>
#include <QStringList>
#include "../stfl/element_group.hpp"
#include "overview_table_row.hpp"
#include "overview_group_subtable.hpp"
#include "../qtutil/accordion.hpp"
namespace cvv
{
namespace gui
{
OverviewTable::OverviewTable(
util::Reference<controller::ViewController> controller)
: controller{ controller }
{
subtableAccordion = new qtutil::Accordion{};
auto *layout = new QVBoxLayout{};
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(subtableAccordion);
setLayout(layout);
}
void OverviewTable::updateRowGroups(
std::vector<stfl::ElementGroup<OverviewTableRow>> newGroups)
{
bool startTheSame = true;
for (size_t i = 0; i < std::min(groups.size(), newGroups.size()); i++)
{
if (!newGroups.at(i).hasSameTitles(groups.at(i)))
{
startTheSame = false;
break;
}
}
if (startTheSame && groups.size() <= newGroups.size())
{
for (size_t i = 0;
i < std::min(groups.size(), newGroups.size()); i++)
{
subTables.at(i)->setRowGroup(newGroups.at(i));
subTables.at(i)->updateUI();
}
for (size_t i = groups.size(); i < newGroups.size(); i++)
{
appendRowGroupToTable(newGroups.at(i));
subTables.at(i)->setRowGroup(newGroups.at(i));
}
}
else
{
subtableAccordion->clear();
subTables.clear();
for (auto &group : newGroups)
{
appendRowGroupToTable(group);
}
}
groups = newGroups;
}
void OverviewTable::hideImages()
{
doesShowImages = false;
updateUI();
}
void OverviewTable::showImages()
{
doesShowImages = true;
updateUI();
}
bool OverviewTable::isShowingImages()
{
return doesShowImages;
}
void OverviewTable::updateUI()
{
for (auto *subTable : subTables)
{
subTable->updateUI();
}
}
void OverviewTable::removeElement(size_t id)
{
for (auto *subTable : subTables)
{
if (subTable->hasRow(id))
{
subTable->removeRow(id);
break;
}
}
}
void
OverviewTable::appendRowGroupToTable(stfl::ElementGroup<OverviewTableRow> group)
{
if (group.size() > 0)
{
auto subtable = util::make_unique<OverviewGroupSubtable>(
controller, this, std::move(group));
auto subtablePtr = subtable.get();
auto titles = group.getTitles();
QString title =
"No grouping specified, use #group to specify one";
if (titles.size() != 0)
{
title = titles.join(", ");
}
subtableAccordion->push_back(title, std::move(subtable), false);
subTables.push_back(subtablePtr);
}
}
}
}
#ifndef CVVISUAL_OVERVIEWTABLE_HPP
#define CVVISUAL_OVERVIEWTABLE_HPP
#include <vector>
#include <QWidget>
#include <QList>
#include "overview_panel.hpp"
#include "overview_table_row.hpp"
#include "../stfl/element_group.hpp"
#include "../qtutil/accordion.hpp"
#include "../util/util.hpp"
#include "../controller/view_controller.hpp"
#include "overview_group_subtable.hpp"
namespace cvv
{
namespace gui
{
class OverviewPanel;
class OverviewTableRow;
/**
* @brief A table displaying the different calls in the overview.
* It's actually an accordion of subtables to support grouping.
*/
class OverviewTable : public QWidget
{
Q_OBJECT
public:
/**
* @brief Constructs a new OverviewTable.
* @param controller it's ViewController
*/
OverviewTable(util::Reference<controller::ViewController> controller);
~OverviewTable()
{
}
/**
* @brief Update the inherited groups of rows and rebuild the UI fully.
* @param newGroups new groups for this table
*/
void updateRowGroups(
const std::vector<stfl::ElementGroup<OverviewTableRow>> newGroups);
/**
* @brief Hide the thumbnail images in the tables.
*/
void hideImages();
/**
* @brief Show thumbnail images in the tables.
*/
void showImages();
/**
* @brief Does this the tables show thumbnail images?
*/
bool isShowingImages();
/**
* @brief Updates the UI.
* Updates all subtables.
*/
void updateUI();
/**
* @brief Removes the table element with the given id.
* @param id given element id
*/
void removeElement(size_t id);
private:
util::Reference<controller::ViewController> controller;
bool doesShowImages = true;
qtutil::Accordion *subtableAccordion;
std::vector<OverviewGroupSubtable *> subTables{};
std::vector<stfl::ElementGroup<OverviewTableRow>> groups;
void appendRowGroupToTable(stfl::ElementGroup<OverviewTableRow> group);
};
}
}
#endif
#include "overview_table_row.hpp"
#include <algorithm>
#include <memory>
#include <QTableWidgetItem>
#include <QImage>
#include "../qtutil/util.hpp"
#include "../stfl/stringutils.hpp"
namespace cvv
{
namespace gui
{
OverviewTableRow::OverviewTableRow(util::Reference<const impl::Call> call)
: call_{ call }
{
id_ = call_->getId();
idStr = QString::number(call_->getId());
for (size_t i = 0; i < 2 && i < call->matrixCount(); i++)
{
QPixmap img;
std::tie(std::ignore, img) =
qtutil::convertMatToQPixmap(call->matrixAt(i));
imgs.push_back(std::move(img));
}
description_ = QString(call_->description());
if (call_->metaData().isKnown)
{
const auto &data = call_->metaData();
line_ = data.line;
lineStr = QString::number(data.line);
fileStr = data.file;
functionStr = data.function;
}
typeStr = QString(call_->type());
}
void OverviewTableRow::addToTable(QTableWidget *table, size_t row,
bool showImages, size_t maxImages,
int imgHeight, int imgWidth)
{
std::vector<std::unique_ptr<QTableWidgetItem>> items{};
items.push_back(util::make_unique<QTableWidgetItem>(idStr));
if (showImages)
{
for (size_t i = 0; i < imgs.size() && i < maxImages; i++)
{
auto imgWidget = util::make_unique<QTableWidgetItem>("");
imgWidget->setData(
Qt::DecorationRole,
imgs.at(i).scaled(imgHeight, imgWidth,
Qt::KeepAspectRatio,
Qt::SmoothTransformation));
imgWidget->setTextAlignment(Qt::AlignHCenter);
items.push_back(std::move(imgWidget));
}
}
size_t emptyImagesToAdd =
showImages ? maxImages - std::min(maxImages, imgs.size())
: maxImages;
for (size_t i = 0; i < emptyImagesToAdd; i++)
{
items.push_back(util::make_unique<QTableWidgetItem>(""));
}
items.push_back(util::make_unique<QTableWidgetItem>(description_));
items.push_back(util::make_unique<QTableWidgetItem>(functionStr, 30));
items.push_back(util::make_unique<QTableWidgetItem>(fileStr));
items.push_back(util::make_unique<QTableWidgetItem>(lineStr));
items.push_back(util::make_unique<QTableWidgetItem>(typeStr));
for (size_t i = 0; i < items.size(); i++)
{
items[i]->setFlags(items[i]->flags() ^ Qt::ItemIsEditable);
table->setItem(row, i, items[i].release());
}
}
void OverviewTableRow::resizeInTable(QTableWidget *table, size_t row,
bool showImages, size_t maxImages,
int imgHeight, int imgWidth)
{
if (showImages)
{
for (size_t i = 0; i < imgs.size() && i < maxImages; i++)
{
auto imgWidget = util::make_unique<QTableWidgetItem>("");
imgWidget->setData(
Qt::DecorationRole,
imgs.at(i).scaled(imgHeight, imgWidth,
Qt::KeepAspectRatio,
Qt::SmoothTransformation));
imgWidget->setTextAlignment(Qt::AlignHCenter);
imgWidget->setFlags(imgWidget->flags() ^ Qt::ItemIsEditable);
table->setItem(row, i + 1, imgWidget.release());
}
}
}
}
}
#ifndef CVVISUAL_OVERVIEWTABLEROW_HPP
#define CVVISUAL_OVERVIEWTABLEROW_HPP
#include <vector>
#include <QTableWidget>
#include <QString>
#include <QPixmap>
#include "../impl/call.hpp"
#include "../util/util.hpp"
namespace cvv
{
namespace gui
{
/**
* @brief A UI wrapper for an impl::Call, providing utility and UI functions.
*
* Also allowing it to add its data to a table.
*/
class OverviewTableRow
{
public:
/**
* @brief Constructor of this class.
* @param call call this row is based on
*/
OverviewTableRow(util::Reference<const impl::Call> call);
~OverviewTableRow()
{
}
/**
* @brief Adds the inherited data set to the given table.
* @param table given table
* @param row row index at which the data will be shown
* @param showImages does the table show images?
* @param maxImages the maximum number of images the table shows
* @param imgHeight height of the shown images
* @param imgWidth width of the shown images
*/
void addToTable(QTableWidget *table, size_t row, bool showImages,
size_t maxImages, int imgHeight = 100,
int imgWidth = 100);
/**
* @brief Resizes the images in the given row.
* Make sure to call this after (!) you called addToTable() with the
* same row parameter on this object some time.
* @param table given table
* @param row row index at which the data will be shown
* @param showImages does the table show images?
* @param maxImages the maximum number of images the table shows
* @param imgHeight height of the shown images
* @param imgWidth width of the shown images
*/
void resizeInTable(QTableWidget *table, size_t row, bool showImages,
size_t maxImages, int imgHeight = 100,
int imgWidth = 100);
/**
* @brief Get the inherited call.
* @return the inherited call
*/
util::Reference<const impl::Call> call() const
{
return call_;
}
/**
* @brief Returns the description of the inherited call.
* @return description of the inherited call.
*/
QString description() const
{
return description_;
}
/**
* @brief Returns the id of the inherited call.
* @return id of the inherited call.
*/
size_t id() const
{
return id_;
}
/**
* @brief Returns the function name property of the inherited call.
* @return function name property of the inherited call.
*/
QString function() const
{
return functionStr;
}
/**
* @brief Returns the file name of the inherited call.
* @return file name of the inherited call.
*/
QString file() const
{
return fileStr;
}
/**
* @brief Returns the line property of the inherited call.
* @return line property of the inherited call.
*/
size_t line() const
{
return line_;
}
/**
* @brief Returns the type of the inherited call.
* @return type of the inherited call.
*/
QString type() const
{
return typeStr;
}
private:
util::Reference<const impl::Call> call_;
size_t id_ = 0;
size_t line_ = 0;
QString idStr = "";
QString description_ = "";
std::vector<QPixmap> imgs{};
QString functionStr = "";
QString fileStr = "";
QString lineStr = "";
QString typeStr = "";
};
}
}
#endif
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
#ifndef CVVISUAL_TABWIDGET
#define CVVISUAL_TABWIDGET
#include <QTabWidget>
#include <QTabBar>
namespace cvv
{
namespace gui
{
/**
* @brief A simple to QTabWidget Subclass, enabling the access to protected
* members.
*/
class TabWidget : public QTabWidget
{
public:
/**
* @brief Constructor of this class.
*/
TabWidget(QWidget *parent) : QTabWidget(parent)
{
};
/**
* @brief Returns the shown tab bar.
* This method helps to access the member tabBar which has by default
* only a protected setter.
* @return shown tab bar.
*/
QTabBar *getTabBar() const
{
return tabBar();
}
};
}
}
#endif
#include "call.hpp"
namespace cvv
{
namespace impl
{
size_t newCallId()
{
thread_local size_t nextId = 1;
return nextId++;
}
Call::Call() : metaData_{}, id{ newCallId() }, calltype{}
{
}
Call::Call(impl::CallMetaData callData, QString type, QString description,
QString requestedView)
: metaData_{ std::move(callData) }, id{ newCallId() },
calltype{ std::move(type) }, description_{ std::move(description) },
requestedView_{ std::move(requestedView) }
{
}
}
} // namespaces cvv::impl
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment