diff --git a/benchmarks/kis_floodfill_benchmark.cpp b/benchmarks/kis_floodfill_benchmark.cpp index f0a2f91b85..e2fc4cf2ba 100644 --- a/benchmarks/kis_floodfill_benchmark.cpp +++ b/benchmarks/kis_floodfill_benchmark.cpp @@ -1,112 +1,189 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "kis_benchmark_values.h" #include "kis_random_accessor_ng.h" #include #include #include #include #include #include "kis_floodfill_benchmark.h" #include void KisFloodFillBenchmark::initTestCase() { m_colorSpace = KoColorSpaceRegistry::instance()->rgb8(); - m_device = new KisPaintDevice(m_colorSpace); + m_deviceStandardFloodFill = new KisPaintDevice(m_colorSpace); m_color = KoColor(m_colorSpace); QColor qcolor(Qt::red); srand(31524744); int tilew = 38; int tileh = 56; m_color.fromQColor(QColor(0,0,0,0)); // default pixel - m_device->fill( 0,0,GMP_IMAGE_WIDTH, GMP_IMAGE_HEIGHT,m_color.data() ); + m_deviceStandardFloodFill->fill( 0,0,GMP_IMAGE_WIDTH, GMP_IMAGE_HEIGHT,m_color.data() ); // fill the image with red ellipses (like some random dabs) m_color.fromQColor(Qt::red); - KisPainter painter(m_device); + KisPainter painter(m_deviceStandardFloodFill); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setPaintColor(m_color); int x = 0; int y = 0; for (int i = 0; i < 100;i++){ x = rand() % GMP_IMAGE_WIDTH; y = rand() % GMP_IMAGE_HEIGHT; // plus 10 so that we don't fill the ellipse painter.paintEllipse(x+ 10, y+ 10, tilew, tileh); } + // copy to other tests + m_deviceWithoutSelectionAsBoundary = new KisPaintDevice(m_colorSpace); + m_deviceWithSelectionAsBoundary = new KisPaintDevice(m_colorSpace); + + + KisPainter::copyAreaOptimized(QPoint(), m_deviceStandardFloodFill, + m_deviceWithoutSelectionAsBoundary, m_deviceWithoutSelectionAsBoundary->exactBounds()); + KisPainter::copyAreaOptimized(QPoint(), m_deviceStandardFloodFill, + m_deviceWithSelectionAsBoundary, m_deviceWithSelectionAsBoundary->exactBounds()); + + //m_deviceWithoutSelectionAsBoundary = m_deviceStandardFloodFill-> + + const KoColorSpace* alphacs = KoColorSpaceRegistry::instance()->alpha8(); + KoColor defaultSelected = KoColor(alphacs); + defaultSelected.fromQColor(QColor(255, 255, 255)); + m_existingSelection = new KisPaintDevice(alphacs); + m_existingSelection->fill(0, 0, GMP_IMAGE_WIDTH, GMP_IMAGE_HEIGHT, defaultSelected.data()); } void KisFloodFillBenchmark::benchmarkFlood() { KoColor fg(m_colorSpace); KoColor bg(m_colorSpace); fg.fromQColor(Qt::blue); bg.fromQColor(Qt::black); QBENCHMARK { - KisFillPainter fillPainter(m_device); + KisFillPainter fillPainter(m_deviceStandardFloodFill); //setupPainter(&fillPainter); fillPainter.setPaintColor( fg ); fillPainter.setBackgroundColor( bg ); fillPainter.beginTransaction(kundo2_noi18n("Flood Fill")); //fillPainter.setProgress(updater->startSubtask()); fillPainter.setOpacity(OPACITY_OPAQUE_U8); // default fillPainter.setFillThreshold(15); fillPainter.setCompositeOp(COMPOSITE_OVER); fillPainter.setCareForSelection(true); fillPainter.setWidth(GMP_IMAGE_WIDTH); fillPainter.setHeight(GMP_IMAGE_HEIGHT); // fill twice - fillPainter.fillColor(1, 1, m_device); + fillPainter.fillColor(1, 1, m_deviceStandardFloodFill); fillPainter.deleteTransaction(); } // uncomment this to see the output //QImage out = m_device->convertToQImage(m_colorSpace->profile(),0,0,GMP_IMAGE_WIDTH,GMP_IMAGE_HEIGHT); //out.save("fill_output.png"); } +void KisFloodFillBenchmark::benchmarkFloodWithoutSelectionAsBoundary() +{ + KoColor fg(m_colorSpace); + KoColor bg(m_colorSpace); + fg.fromQColor(Qt::blue); + bg.fromQColor(Qt::black); + + QBENCHMARK + { + KisFillPainter fillPainter(m_deviceWithoutSelectionAsBoundary); + fillPainter.setPaintColor( fg ); + fillPainter.setBackgroundColor( bg ); + + fillPainter.beginTransaction(kundo2_noi18n("Flood Fill")); + + fillPainter.setOpacity(OPACITY_OPAQUE_U8); + // default + fillPainter.setFillThreshold(15); + fillPainter.setCompositeOp(COMPOSITE_OVER); + fillPainter.setCareForSelection(true); + fillPainter.setWidth(GMP_IMAGE_WIDTH); + fillPainter.setHeight(GMP_IMAGE_HEIGHT); + fillPainter.setUseSelectionAsBoundary(false); + + fillPainter.createFloodSelection(1, 1, m_deviceWithoutSelectionAsBoundary, m_existingSelection); + + fillPainter.deleteTransaction(); + } +} + +void KisFloodFillBenchmark::benchmarkFloodWithSelectionAsBoundary() +{ + KoColor fg(m_colorSpace); + KoColor bg(m_colorSpace); + fg.fromQColor(Qt::blue); + bg.fromQColor(Qt::black); + + QBENCHMARK + { + KisFillPainter fillPainter(m_deviceWithSelectionAsBoundary); + fillPainter.setPaintColor( fg ); + fillPainter.setBackgroundColor( bg ); + + fillPainter.beginTransaction(kundo2_noi18n("Flood Fill")); + + fillPainter.setOpacity(OPACITY_OPAQUE_U8); + // default + fillPainter.setFillThreshold(15); + fillPainter.setCompositeOp(COMPOSITE_OVER); + fillPainter.setCareForSelection(true); + fillPainter.setWidth(GMP_IMAGE_WIDTH); + fillPainter.setHeight(GMP_IMAGE_HEIGHT); + fillPainter.setUseSelectionAsBoundary(true); + + fillPainter.createFloodSelection(1, 1, m_deviceWithSelectionAsBoundary, m_existingSelection); + + fillPainter.deleteTransaction(); + } +} + void KisFloodFillBenchmark::cleanupTestCase() { } QTEST_MAIN(KisFloodFillBenchmark) diff --git a/benchmarks/kis_floodfill_benchmark.h b/benchmarks/kis_floodfill_benchmark.h index 522a41a38e..7b804eb229 100644 --- a/benchmarks/kis_floodfill_benchmark.h +++ b/benchmarks/kis_floodfill_benchmark.h @@ -1,50 +1,56 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_FLOODFILL_BENCHMARK_H #define KIS_FLOODFILL_BENCHMARK_H #include #include #include #include class KoColor; class KisFloodFillBenchmark : public QObject { Q_OBJECT private: const KoColorSpace * m_colorSpace; KoColor m_color; - KisPaintDeviceSP m_device; + KisPaintDeviceSP m_deviceStandardFloodFill; + KisPaintDeviceSP m_deviceWithSelectionAsBoundary; + KisPaintDeviceSP m_deviceWithoutSelectionAsBoundary; + KisPaintDeviceSP m_existingSelection; int m_startX; int m_startY; private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void benchmarkFlood(); + void benchmarkFloodWithoutSelectionAsBoundary(); + void benchmarkFloodWithSelectionAsBoundary(); + }; #endif diff --git a/krita/CMakeLists.txt b/krita/CMakeLists.txt index b0e823ace6..f49a97c227 100644 --- a/krita/CMakeLists.txt +++ b/krita/CMakeLists.txt @@ -1,113 +1,123 @@ project(krita) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${Vc_INCLUDE_DIR} ) add_subdirectory( dtd ) add_subdirectory( data ) add_subdirectory( integration ) # Install the application icons following the freedesktop icon theme spec add_subdirectory( "pics/branding/${BRANDING}" ) if (ANDROID) include_directories (${Qt5AndroidExtras_INCLUDE_DIRS}) endif() set(krita_SRCS main.cc) # Set the application icon on the application if (NOT APPLE) file(GLOB ICON_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/*-apps-krita.png") else() set(ICON_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/16-apps-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/32-apps-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/48-apps-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/128-apps-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/256-apps-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/512-apps-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/branding/${BRANDING}/1024-apps-krita.png" ) endif() ecm_add_app_icon(krita_SRCS ICONS ${ICON_SRCS}) # Install the mimetype icons ecm_install_icons(ICONS "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/16-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/22-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/32-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/48-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/64-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/128-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/256-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/512-mimetypes-application-x-krita.png" "${CMAKE_CURRENT_SOURCE_DIR}/pics/mimetypes/1024-mimetypes-application-x-krita.png" DESTINATION ${KDE_INSTALL_ICONDIR} THEME hicolor) # separate listing, both used by Krita and KritaSketch set(krita_QRCS ${CMAKE_SOURCE_DIR}/krita/krita.qrc ${CMAKE_SOURCE_DIR}/krita/pics/Breeze-dark/breeze-dark-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/Breeze-light/breeze-light-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/layerbox/layerbox-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/layerbox/svg/layerbox-svg-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/layers/layers-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/misc-light/misc-light-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/misc-dark/misc-dark-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/paintops/paintops-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/tools/SVG/16/tools-svg-16-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/tool_transform/tool-transform-icons.qrc ${CMAKE_SOURCE_DIR}/krita/pics/svg/svg-icons.qrc ${CMAKE_SOURCE_DIR}/libs/flake/flake.qrc ${CMAKE_SOURCE_DIR}/libs/widgets/kritawidgets.qrc ${CMAKE_SOURCE_DIR}/pics/icons.qrc ${CMAKE_SOURCE_DIR}/krita/data/aboutdata/aboutdata.qrc ${CMAKE_SOURCE_DIR}/krita/data/shaders/shaders.qrc ${CMAKE_SOURCE_DIR}/krita/data/cursors/cursors.qrc CACHE INTERNAL "krita_QRCS" ) qt5_add_resources(krita_SRCS ${krita_QRCS}) if (ANDROID) add_library(krita SHARED ${krita_SRCS}) target_link_libraries(krita PRIVATE Qt5::AndroidExtras) else() add_executable(krita ${krita_SRCS}) endif() target_link_libraries(krita PRIVATE kritaui Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Xml Qt5::Network Qt5::PrintSupport Qt5::Svg Qt5::Concurrent) if(HAVE_KCRASH) target_link_libraries(krita PRIVATE KF5::Crash) endif() if (APPLE) set_target_properties(krita PROPERTIES INSTALL_RPATH "@loader_path/../../../../lib;@loader_path/../lib;@loader_path/../Frameworks;@executable_path/../lib;@executable_path/../Frameworks") set_target_properties(krita PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.template) set_target_properties(krita PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.krita") set_target_properties(krita PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Krita") set_target_properties(krita PROPERTIES MACOSX_BUNDLE_ICON_FILE "krita_SRCS.icns") set_target_properties(krita PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING ${KRITA_VERSION_STRING}) set_target_properties(krita PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING ${KRITA_VERSION_STRING}) set_target_properties(krita PROPERTIES MACOSX_BUNDLE_VERSION ${KRITA_VERSION_STRING}) set_target_properties(krita PROPERTIES MACOSX_BUNDLE_COPYRIGHT "GNU Public License, V2 or, at your option, any later version.") endif () +set(KRITAVERSION_SRCS kritaversion.cpp) +add_executable(krita_version ${KRITAVERSION_SRCS}) +target_link_libraries(krita_version + PRIVATE + Qt5::Core + kritaversion +) + + install(TARGETS krita ${INSTALL_TARGETS_DEFAULT_ARGS}) +install(TARGETS krita_version ${INSTALL_TARGETS_DEFAULT_ARGS}) install(PROGRAMS org.kde.krita.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) install(FILES krita.action kritamenu.action DESTINATION ${DATA_INSTALL_DIR}/krita/actions) install(FILES org.kde.krita.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install(DIRECTORY DESTINATION ${DATA_INSTALL_DIR}/krita/shortcuts) diff --git a/krita/kritaversion.cpp b/krita/kritaversion.cpp new file mode 100644 index 0000000000..9acaa598c7 --- /dev/null +++ b/krita/kritaversion.cpp @@ -0,0 +1,36 @@ +/* +* Copyright (c) 1999 Matthias Elter +* Copyright (c) 2002 Patrick Julien +* Copyright (c) 2015 Boudewijn Rempt +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include +#include + +#include + +QTextStream& qStdOut() +{ + static QTextStream ts( stdout ); + return ts; +} + +extern "C" int main(int , char **) +{ + qStdOut() << KRITA_VERSION_STRING << "\n"; + return 0; +} diff --git a/libs/image/floodfill/kis_scanline_fill.cpp b/libs/image/floodfill/kis_scanline_fill.cpp index a65204ccca..f9e9c109ad 100644 --- a/libs/image/floodfill/kis_scanline_fill.cpp +++ b/libs/image/floodfill/kis_scanline_fill.cpp @@ -1,726 +1,842 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_scanline_fill.h" #include #include #include #include #include #include "kis_image.h" #include "kis_fill_interval_map.h" #include "kis_pixel_selection.h" #include "kis_random_accessor_ng.h" #include "kis_fill_sanity_checks.h" template class CopyToSelection : public BaseClass { public: typedef KisRandomConstAccessorSP SourceAccessorType; SourceAccessorType createSourceDeviceAccessor(KisPaintDeviceSP device) { return device->createRandomConstAccessorNG(); } public: void setDestinationSelection(KisPaintDeviceSP pixelSelection) { m_pixelSelection = pixelSelection; m_it = m_pixelSelection->createRandomAccessorNG(); } ALWAYS_INLINE void fillPixel(quint8 *dstPtr, quint8 opacity, int x, int y) { Q_UNUSED(dstPtr); m_it->moveTo(x, y); *m_it->rawData() = opacity; } private: KisPaintDeviceSP m_pixelSelection; KisRandomAccessorSP m_it; }; template class FillWithColor : public BaseClass { public: typedef KisRandomAccessorSP SourceAccessorType; SourceAccessorType createSourceDeviceAccessor(KisPaintDeviceSP device) { return device->createRandomAccessorNG(); } public: FillWithColor() : m_pixelSize(0) {} void setFillColor(const KoColor &sourceColor) { m_sourceColor = sourceColor; m_pixelSize = sourceColor.colorSpace()->pixelSize(); m_data = m_sourceColor.data(); } ALWAYS_INLINE void fillPixel(quint8 *dstPtr, quint8 opacity, int x, int y) { Q_UNUSED(x); Q_UNUSED(y); if (opacity == MAX_SELECTED) { memcpy(dstPtr, m_data, m_pixelSize); } } private: KoColor m_sourceColor; const quint8 *m_data; int m_pixelSize; }; template class FillWithColorExternal : public BaseClass { public: typedef KisRandomConstAccessorSP SourceAccessorType; SourceAccessorType createSourceDeviceAccessor(KisPaintDeviceSP device) { return device->createRandomConstAccessorNG(); } public: void setDestinationDevice(KisPaintDeviceSP device) { m_externalDevice = device; m_it = m_externalDevice->createRandomAccessorNG(); } void setFillColor(const KoColor &sourceColor) { m_sourceColor = sourceColor; m_pixelSize = sourceColor.colorSpace()->pixelSize(); m_data = m_sourceColor.data(); } ALWAYS_INLINE void fillPixel(quint8 *dstPtr, quint8 opacity, int x, int y) { Q_UNUSED(dstPtr); m_it->moveTo(x, y); if (opacity == MAX_SELECTED) { memcpy(m_it->rawData(), m_data, m_pixelSize); } } private: KisPaintDeviceSP m_externalDevice; KisRandomAccessorSP m_it; KoColor m_sourceColor; const quint8 *m_data {0}; int m_pixelSize {0}; }; class DifferencePolicySlow { public: ALWAYS_INLINE void initDifferences(KisPaintDeviceSP device, const KoColor &srcPixel, int threshold) { m_colorSpace = device->colorSpace(); m_srcPixel = srcPixel; m_srcPixelPtr = m_srcPixel.data(); m_threshold = threshold; } ALWAYS_INLINE quint8 calculateDifference(quint8* pixelPtr) { if (m_threshold == 1) { if (memcmp(m_srcPixelPtr, pixelPtr, m_colorSpace->pixelSize()) == 0) { return 0; } return quint8_MAX; } else { return m_colorSpace->differenceA(m_srcPixelPtr, pixelPtr); } } private: const KoColorSpace *m_colorSpace; KoColor m_srcPixel; const quint8 *m_srcPixelPtr; int m_threshold; }; template class DifferencePolicyOptimized { typedef SrcPixelType HashKeyType; typedef QHash HashType; public: ALWAYS_INLINE void initDifferences(KisPaintDeviceSP device, const KoColor &srcPixel, int threshold) { m_colorSpace = device->colorSpace(); m_srcPixel = srcPixel; m_srcPixelPtr = m_srcPixel.data(); m_threshold = threshold; } ALWAYS_INLINE quint8 calculateDifference(quint8* pixelPtr) { HashKeyType key = *reinterpret_cast(pixelPtr); quint8 result; typename HashType::iterator it = m_differences.find(key); if (it != m_differences.end()) { result = *it; } else { if (m_threshold == 1) { if (memcmp(m_srcPixelPtr, pixelPtr, m_colorSpace->pixelSize()) == 0) { result = 0; } else { result = quint8_MAX; } } else { result = m_colorSpace->differenceA(m_srcPixelPtr, pixelPtr); } m_differences.insert(key, result); } return result; } private: HashType m_differences; const KoColorSpace *m_colorSpace; KoColor m_srcPixel; const quint8 *m_srcPixelPtr; int m_threshold; }; +class SelectednessPolicyOptimized +{ + typedef quint8 HashKeyType; + typedef QHash HashType; + + KisRandomConstAccessorSP m_selectionIt; + +public: + + ALWAYS_INLINE void initSelectedness(KisPaintDeviceSP device, int threshold) { + m_colorSpace = device->colorSpace(); + m_threshold = threshold; + m_selectionIt = device->createRandomConstAccessorNG(); + } + + ALWAYS_INLINE quint8 calculateSelectedness(int x, int y) { + m_selectionIt->moveTo(x, y); + const quint8* pixelPtr = m_selectionIt->rawDataConst(); + return *pixelPtr; + } + +private: + HashType m_selectedness; + + const KoColorSpace *m_colorSpace; + int m_threshold; +}; + + template class PixelFiller> class SelectionPolicy : public PixelFiller { public: typename PixelFiller::SourceAccessorType m_srcIt; public: SelectionPolicy(KisPaintDeviceSP device, const KoColor &srcPixel, int threshold) : m_threshold(threshold) { this->initDifferences(device, srcPixel, threshold); m_srcIt = this->createSourceDeviceAccessor(device); } - ALWAYS_INLINE quint8 calculateOpacity(quint8* pixelPtr) { + ALWAYS_INLINE quint8 calculateOpacity(quint8* pixelPtr, int x, int y) { quint8 diff = this->calculateDifference(pixelPtr); + Q_UNUSED(x); + Q_UNUSED(y); if (!useSmoothSelection) { return diff <= m_threshold ? MAX_SELECTED : MIN_SELECTED; } else { quint8 selectionValue = qMax(0, m_threshold - diff); quint8 result = MIN_SELECTED; if (selectionValue > 0) { qreal selectionNorm = qreal(selectionValue) / m_threshold; result = MAX_SELECTED * selectionNorm; } + return result; + } + } + +private: + int m_threshold; +}; + + + +template class PixelFiller, + class SelectednessCheckPolicy> +class SelectionPolicyExtended : public SelectionPolicy + , public SelectednessCheckPolicy +{ + +public: + + SelectionPolicyExtended(KisPaintDeviceSP mainDevice, KisPaintDeviceSP selectionDevice, const KoColor &srcPixel, int threshold) + : SelectionPolicy(mainDevice, srcPixel, threshold) + { + m_threshold = threshold; + this->initSelectedness(selectionDevice, threshold); + } + +public: + ALWAYS_INLINE quint8 calculateOpacity(quint8* pixelPtr, int x, int y) { + quint8 diff = this->calculateDifference(pixelPtr); + quint8 selectedness = this->calculateSelectedness(x, y); + + if (!useSmoothSelection) { + return (diff <= m_threshold && selectedness > 0) ? MAX_SELECTED : MIN_SELECTED; + } else { + quint8 selectionValue = qMax(0, m_threshold - diff); + quint8 result = MIN_SELECTED; + + if (selectionValue > 0 && selectedness > 0) { + qreal selectionNorm = qreal(selectionValue) / m_threshold; + result = MAX_SELECTED * selectionNorm; + } return result; } } private: int m_threshold; + }; + + + + + class IsNonNullPolicySlow { public: ALWAYS_INLINE void initDifferences(KisPaintDeviceSP device, const KoColor &srcPixel, int /*threshold*/) { Q_UNUSED(srcPixel); m_pixelSize = device->pixelSize(); m_testPixel.resize(m_pixelSize); } ALWAYS_INLINE quint8 calculateDifference(quint8* pixelPtr) { if (memcmp(m_testPixel.data(), pixelPtr, m_pixelSize) == 0) { return 0; } return quint8_MAX; } private: int m_pixelSize {0}; QByteArray m_testPixel; }; template class IsNonNullPolicyOptimized { public: ALWAYS_INLINE void initDifferences(KisPaintDeviceSP device, const KoColor &srcPixel, int /*threshold*/) { Q_UNUSED(device); Q_UNUSED(srcPixel); } ALWAYS_INLINE quint8 calculateDifference(quint8* pixelPtr) { SrcPixelType *pixel = reinterpret_cast(pixelPtr); return *pixel == 0; } }; class GroupSplitPolicy { public: typedef KisRandomConstAccessorSP SourceAccessorType; SourceAccessorType m_srcIt; public: GroupSplitPolicy(KisPaintDeviceSP scribbleDevice, KisPaintDeviceSP groupMapDevice, qint32 groupIndex, quint8 referenceValue, int threshold) : m_threshold(threshold), m_groupIndex(groupIndex), m_referenceValue(referenceValue) { KIS_SAFE_ASSERT_RECOVER_NOOP(m_groupIndex > 0); m_srcIt = scribbleDevice->createRandomConstAccessorNG(); m_groupMapIt = groupMapDevice->createRandomAccessorNG(); } - ALWAYS_INLINE quint8 calculateOpacity(quint8* pixelPtr) { + ALWAYS_INLINE quint8 calculateOpacity(quint8* pixelPtr, int x, int y) { // TODO: either threshold should always be null, or there should be a special // case for *pixelPtr == 0, which is different from all the other groups, // whatever the threshold is + Q_UNUSED(x); + Q_UNUSED(y); int diff = qAbs(int(*pixelPtr) - m_referenceValue); return diff <= m_threshold ? MAX_SELECTED : MIN_SELECTED; } ALWAYS_INLINE void fillPixel(quint8 *dstPtr, quint8 opacity, int x, int y) { Q_UNUSED(opacity); // erase the scribble *dstPtr = 0; // write group index into the map m_groupMapIt->moveTo(x, y); qint32 *groupMapPtr = reinterpret_cast(m_groupMapIt->rawData()); if (*groupMapPtr != 0) { dbgImage << ppVar(*groupMapPtr) << ppVar(m_groupIndex); } KIS_SAFE_ASSERT_RECOVER_NOOP(*groupMapPtr == 0); *groupMapPtr = m_groupIndex; } private: int m_threshold; qint32 m_groupIndex; quint8 m_referenceValue; KisRandomAccessorSP m_groupMapIt; }; struct Q_DECL_HIDDEN KisScanlineFill::Private { KisPaintDeviceSP device; QPoint startPoint; QRect boundingRect; int threshold; int rowIncrement; KisFillIntervalMap backwardMap; QStack forwardStack; inline void swapDirection() { rowIncrement *= -1; KIS_SAFE_ASSERT_RECOVER_NOOP(forwardStack.isEmpty() && "FATAL: the forward stack must be empty " "on a direction swap"); forwardStack = QStack(backwardMap.fetchAllIntervals(rowIncrement)); backwardMap.clear(); } }; KisScanlineFill::KisScanlineFill(KisPaintDeviceSP device, const QPoint &startPoint, const QRect &boundingRect) : m_d(new Private) { m_d->device = device; m_d->startPoint = startPoint; m_d->boundingRect = boundingRect; m_d->rowIncrement = 1; m_d->threshold = 0; } KisScanlineFill::~KisScanlineFill() { } void KisScanlineFill::setThreshold(int threshold) { m_d->threshold = threshold; } template void KisScanlineFill::extendedPass(KisFillInterval *currentInterval, int srcRow, bool extendRight, T &pixelPolicy) { int x; int endX; int columnIncrement; int *intervalBorder; int *backwardIntervalBorder; KisFillInterval backwardInterval(currentInterval->start, currentInterval->end, srcRow); if (extendRight) { x = currentInterval->end; endX = m_d->boundingRect.right(); if (x >= endX) return; columnIncrement = 1; intervalBorder = ¤tInterval->end; backwardInterval.start = currentInterval->end + 1; backwardIntervalBorder = &backwardInterval.end; } else { x = currentInterval->start; endX = m_d->boundingRect.left(); if (x <= endX) return; columnIncrement = -1; intervalBorder = ¤tInterval->start; backwardInterval.end = currentInterval->start - 1; backwardIntervalBorder = &backwardInterval.start; } do { x += columnIncrement; pixelPolicy.m_srcIt->moveTo(x, srcRow); quint8 *pixelPtr = const_cast(pixelPolicy.m_srcIt->rawDataConst()); // TODO: avoid doing const_cast - quint8 opacity = pixelPolicy.calculateOpacity(pixelPtr); + quint8 opacity = pixelPolicy.calculateOpacity(pixelPtr, x, srcRow); if (opacity) { *intervalBorder = x; *backwardIntervalBorder = x; pixelPolicy.fillPixel(pixelPtr, opacity, x, srcRow); } else { break; } } while (x != endX); if (backwardInterval.isValid()) { m_d->backwardMap.insertInterval(backwardInterval); } } template void KisScanlineFill::processLine(KisFillInterval interval, const int rowIncrement, T &pixelPolicy) { m_d->backwardMap.cropInterval(&interval); if (!interval.isValid()) return; int firstX = interval.start; int lastX = interval.end; int x = firstX; int row = interval.row; int nextRow = row + rowIncrement; KisFillInterval currentForwardInterval; int numPixelsLeft = 0; quint8 *dataPtr = 0; const int pixelSize = m_d->device->pixelSize(); while(x <= lastX) { // a bit of optimzation for not calling slow random accessor // methods too often if (numPixelsLeft <= 0) { pixelPolicy.m_srcIt->moveTo(x, row); numPixelsLeft = pixelPolicy.m_srcIt->numContiguousColumns(x) - 1; dataPtr = const_cast(pixelPolicy.m_srcIt->rawDataConst()); } else { numPixelsLeft--; dataPtr += pixelSize; } quint8 *pixelPtr = dataPtr; - quint8 opacity = pixelPolicy.calculateOpacity(pixelPtr); + quint8 opacity = pixelPolicy.calculateOpacity(pixelPtr, x, row); if (opacity) { if (!currentForwardInterval.isValid()) { currentForwardInterval.start = x; currentForwardInterval.end = x; currentForwardInterval.row = nextRow; } else { currentForwardInterval.end = x; } pixelPolicy.fillPixel(pixelPtr, opacity, x, row); if (x == firstX) { extendedPass(¤tForwardInterval, row, false, pixelPolicy); } if (x == lastX) { extendedPass(¤tForwardInterval, row, true, pixelPolicy); } } else { if (currentForwardInterval.isValid()) { m_d->forwardStack.push(currentForwardInterval); currentForwardInterval.invalidate(); } } x++; } if (currentForwardInterval.isValid()) { m_d->forwardStack.push(currentForwardInterval); } } template void KisScanlineFill::runImpl(T &pixelPolicy) { KIS_ASSERT_RECOVER_RETURN(m_d->forwardStack.isEmpty()); KisFillInterval startInterval(m_d->startPoint.x(), m_d->startPoint.x(), m_d->startPoint.y()); m_d->forwardStack.push(startInterval); /** * In the end of the first pass we should add an interval * containing the starting pixel, but directed into the opposite * direction. We cannot do it in the very beginning because the * intervals are offset by 1 pixel during every swap operation. */ bool firstPass = true; while (!m_d->forwardStack.isEmpty()) { while (!m_d->forwardStack.isEmpty()) { KisFillInterval interval = m_d->forwardStack.pop(); if (interval.row > m_d->boundingRect.bottom() || interval.row < m_d->boundingRect.top()) { continue; } processLine(interval, m_d->rowIncrement, pixelPolicy); } m_d->swapDirection(); if (firstPass) { startInterval.row--; m_d->forwardStack.push(startInterval); firstPass = false; } } } void KisScanlineFill::fillColor(const KoColor &originalFillColor) { KoColor srcColor(m_d->device->pixel(m_d->startPoint)); KoColor fillColor(originalFillColor); fillColor.convertTo(m_d->device->colorSpace()); const int pixelSize = m_d->device->pixelSize(); if (pixelSize == 1) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(fillColor); runImpl(policy); } else if (pixelSize == 2) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(fillColor); runImpl(policy); } else if (pixelSize == 4) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(fillColor); runImpl(policy); } else if (pixelSize == 8) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(fillColor); runImpl(policy); } else { SelectionPolicy policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(fillColor); runImpl(policy); } } void KisScanlineFill::fillColor(const KoColor &originalFillColor, KisPaintDeviceSP externalDevice) { KoColor srcColor(m_d->device->pixel(m_d->startPoint)); KoColor fillColor(originalFillColor); fillColor.convertTo(m_d->device->colorSpace()); const int pixelSize = m_d->device->pixelSize(); if (pixelSize == 1) { SelectionPolicy, FillWithColorExternal> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationDevice(externalDevice); policy.setFillColor(fillColor); runImpl(policy); } else if (pixelSize == 2) { SelectionPolicy, FillWithColorExternal> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationDevice(externalDevice); policy.setFillColor(fillColor); runImpl(policy); } else if (pixelSize == 4) { SelectionPolicy, FillWithColorExternal> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationDevice(externalDevice); policy.setFillColor(fillColor); runImpl(policy); } else if (pixelSize == 8) { SelectionPolicy, FillWithColorExternal> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationDevice(externalDevice); policy.setFillColor(fillColor); runImpl(policy); } else { SelectionPolicy policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationDevice(externalDevice); policy.setFillColor(fillColor); runImpl(policy); } } +void KisScanlineFill::fillSelectionWithBoundary(KisPixelSelectionSP pixelSelection, KisPaintDeviceSP existingSelection) +{ + KoColor srcColor(m_d->device->pixel(m_d->startPoint)); + + const int pixelSize = m_d->device->pixelSize(); + + if (pixelSize == 1) { + SelectionPolicyExtended, CopyToSelection, SelectednessPolicyOptimized> + policy(m_d->device, existingSelection, srcColor, m_d->threshold); + policy.setDestinationSelection(pixelSelection); + runImpl(policy); + } else if (pixelSize == 2) { + SelectionPolicyExtended, CopyToSelection, SelectednessPolicyOptimized> + policy(m_d->device, existingSelection, srcColor, m_d->threshold); + policy.setDestinationSelection(pixelSelection); + runImpl(policy); + } else if (pixelSize == 4) { + SelectionPolicyExtended, CopyToSelection, SelectednessPolicyOptimized> + policy(m_d->device, existingSelection, srcColor, m_d->threshold); + policy.setDestinationSelection(pixelSelection); + runImpl(policy); + + } else if (pixelSize == 8) { + SelectionPolicyExtended, CopyToSelection, SelectednessPolicyOptimized> + policy(m_d->device, existingSelection, srcColor, m_d->threshold); + policy.setDestinationSelection(pixelSelection); + runImpl(policy); + } else { + SelectionPolicyExtended + policy(m_d->device, existingSelection, srcColor, m_d->threshold); + policy.setDestinationSelection(pixelSelection); + runImpl(policy); + } +} + void KisScanlineFill::fillSelection(KisPixelSelectionSP pixelSelection) { KoColor srcColor(m_d->device->pixel(m_d->startPoint)); const int pixelSize = m_d->device->pixelSize(); if (pixelSize == 1) { SelectionPolicy, CopyToSelection> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationSelection(pixelSelection); runImpl(policy); } else if (pixelSize == 2) { SelectionPolicy, CopyToSelection> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationSelection(pixelSelection); runImpl(policy); } else if (pixelSize == 4) { SelectionPolicy, CopyToSelection> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationSelection(pixelSelection); runImpl(policy); } else if (pixelSize == 8) { SelectionPolicy, CopyToSelection> policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationSelection(pixelSelection); runImpl(policy); } else { SelectionPolicy policy(m_d->device, srcColor, m_d->threshold); policy.setDestinationSelection(pixelSelection); runImpl(policy); } } void KisScanlineFill::clearNonZeroComponent() { const int pixelSize = m_d->device->pixelSize(); KoColor srcColor(Qt::transparent, m_d->device->colorSpace()); if (pixelSize == 1) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(srcColor); runImpl(policy); } else if (pixelSize == 2) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(srcColor); runImpl(policy); } else if (pixelSize == 4) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(srcColor); runImpl(policy); } else if (pixelSize == 8) { SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(srcColor); runImpl(policy); } else { SelectionPolicy policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(srcColor); runImpl(policy); } } void KisScanlineFill::fillContiguousGroup(KisPaintDeviceSP groupMapDevice, qint32 groupIndex) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->device->pixelSize() == 1); KIS_SAFE_ASSERT_RECOVER_RETURN(groupMapDevice->pixelSize() == 4); const quint8 referenceValue = *m_d->device->pixel(m_d->startPoint).data(); GroupSplitPolicy policy(m_d->device, groupMapDevice, groupIndex, referenceValue, m_d->threshold); runImpl(policy); } void KisScanlineFill::testingProcessLine(const KisFillInterval &processInterval) { KoColor srcColor(QColor(0,0,0,0), m_d->device->colorSpace()); KoColor fillColor(QColor(200,200,200,200), m_d->device->colorSpace()); SelectionPolicy, FillWithColor> policy(m_d->device, srcColor, m_d->threshold); policy.setFillColor(fillColor); processLine(processInterval, 1, policy); } QVector KisScanlineFill::testingGetForwardIntervals() const { return QVector(m_d->forwardStack); } KisFillIntervalMap* KisScanlineFill::testingGetBackwardIntervals() const { return &m_d->backwardMap; } diff --git a/libs/image/floodfill/kis_scanline_fill.h b/libs/image/floodfill/kis_scanline_fill.h index db752e1745..5d06ffcb2b 100644 --- a/libs/image/floodfill/kis_scanline_fill.h +++ b/libs/image/floodfill/kis_scanline_fill.h @@ -1,100 +1,106 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_SCANLINE_FILL_H #define __KIS_SCANLINE_FILL_H #include #include #include #include class KisFillInterval; class KisFillIntervalMap; class KRITAIMAGE_EXPORT KisScanlineFill { public: KisScanlineFill(KisPaintDeviceSP device, const QPoint &startPoint, const QRect &boundingRect); ~KisScanlineFill(); /** * Fill the source device with \p fillColor */ void fillColor(const KoColor &fillColor); /** * Fill \p externalDevice with \p fillColor basing on the contents * of the source device. */ void fillColor(const KoColor &fillColor, KisPaintDeviceSP externalDevice); + /** + * Fill \p pixelSelection with the opacity of the contiguous area. + * This method uses an existing selection as boundary for the flood fill. + */ + void fillSelectionWithBoundary(KisPixelSelectionSP pixelSelection, KisPaintDeviceSP existingSelection); + /** * Fill \p pixelSelection with the opacity of the contiguous area */ void fillSelection(KisPixelSelectionSP pixelSelection); /** * Clear the contiguous non-zero area of the device * * WARNING: the threshold parameter is not counted! */ void clearNonZeroComponent(); /** * A special filler algorithm for the Watershed initialization routine: * * 1) Clear the contiguous area in the destination device * 2) At the same time, fill the corresponding area of \p groupMapDevice with * value \p groupIndex * 3) \p groupMapDevice **must** store 4 bytes per pixel */ void fillContiguousGroup(KisPaintDeviceSP groupMapDevice, qint32 groupIndex); /** * Set the threshold of the filling operation * * Used in all functions except clearNonZeroComponent() */ void setThreshold(int threshold); private: friend class KisScanlineFillTest; Q_DISABLE_COPY(KisScanlineFill) template void processLine(KisFillInterval interval, const int rowIncrement, T &pixelPolicy); template void extendedPass(KisFillInterval *currentInterval, int srcRow, bool extendRight, T &pixelPolicy); template void runImpl(T &pixelPolicy); private: void testingProcessLine(const KisFillInterval &processInterval); QVector testingGetForwardIntervals() const; KisFillIntervalMap* testingGetBackwardIntervals() const; private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_SCANLINE_FILL_H */ diff --git a/libs/image/kis_fill_painter.cc b/libs/image/kis_fill_painter.cc index 7c461a97af..55a38ccbd6 100644 --- a/libs/image/kis_fill_painter.cc +++ b/libs/image/kis_fill_painter.cc @@ -1,358 +1,365 @@ /* * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Bart Coppens * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_fill_painter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "generator/kis_generator.h" #include "filter/kis_filter_configuration.h" #include "generator/kis_generator_registry.h" #include "kis_processing_information.h" #include "kis_debug.h" #include "kis_image.h" #include "kis_layer.h" #include "kis_paint_device.h" #include #include "KoColorSpace.h" #include "kis_transaction.h" #include "kis_pixel_selection.h" #include #include #include "kis_selection_filters.h" #include KisFillPainter::KisFillPainter() : KisPainter() { initFillPainter(); } KisFillPainter::KisFillPainter(KisPaintDeviceSP device) : KisPainter(device) { initFillPainter(); } KisFillPainter::KisFillPainter(KisPaintDeviceSP device, KisSelectionSP selection) : KisPainter(device, selection) { initFillPainter(); } void KisFillPainter::initFillPainter() { m_width = m_height = -1; m_careForSelection = false; m_sizemod = 0; m_feather = 0; m_useCompositioning = false; m_threshold = 0; + m_useSelectionAsBoundary = false; } void KisFillPainter::fillSelection(const QRect &rc, const KoColor &color) { KisPaintDeviceSP fillDevice = new KisPaintDevice(device()->colorSpace()); fillDevice->setDefaultPixel(color); bitBlt(rc.topLeft(), fillDevice, rc); } // 'regular' filling // XXX: This also needs renaming, since filling ought to keep the opacity and the composite op in mind, // this is more eraseToColor. void KisFillPainter::fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KoColor& kc, quint8 opacity) { if (w > 0 && h > 0) { // Make sure we're in the right colorspace KoColor kc2(kc); // get rid of const kc2.convertTo(device()->colorSpace()); quint8 * data = kc2.data(); device()->colorSpace()->setOpacity(data, opacity, 1); device()->fill(x1, y1, w, h, data); addDirtyRect(QRect(x1, y1, w, h)); } } void KisFillPainter::fillRect(const QRect &rc, const KoPatternSP pattern, const QPoint &offset) { fillRect(rc.x(), rc.y(), rc.width(), rc.height(), pattern, offset); } void KisFillPainter::fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KoPatternSP pattern, const QPoint &offset) { if (!pattern) return; if (!pattern->valid()) return; if (!device()) return; if (w < 1) return; if (h < 1) return; KisPaintDeviceSP patternLayer = new KisPaintDevice(device()->compositionSourceColorSpace(), pattern->name()); patternLayer->convertFromQImage(pattern->pattern(), 0); if (!offset.isNull()) { patternLayer->moveTo(offset); } fillRect(x1, y1, w, h, patternLayer, QRect(offset.x(), offset.y(), pattern->width(), pattern->height())); } void KisFillPainter::fillRect(const QRect &rc, const KoPatternSP pattern, const QTransform transform) { if (!pattern) return; if (!pattern->valid()) return; if (!device()) return; if (rc.width() < 1) return; if (rc.height() < 1) return; KisPaintDeviceSP patternLayer = new KisPaintDevice(device()->compositionSourceColorSpace(), pattern->name()); patternLayer->convertFromQImage(pattern->pattern(), 0); fillRect(rc.x(), rc.y(), rc.width(), rc.height(), patternLayer, QRect(0, 0, pattern->width(), pattern->height()), transform); } void KisFillPainter::fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KisPaintDeviceSP device, const QRect& deviceRect, const QTransform transform) { KisPaintDeviceSP wrapped = device; wrapped->setDefaultBounds(new KisWrapAroundBoundsWrapper(wrapped->defaultBounds(), deviceRect)); KisPerspectiveTransformWorker worker = KisPerspectiveTransformWorker(this->device(), transform, this->progressUpdater()); worker.runPartialDst(device, this->device(), QRect(x1, y1, w, h)); addDirtyRect(QRect(x1, y1, w, h)); } void KisFillPainter::fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KisPaintDeviceSP device, const QRect& deviceRect) { const QRect &patternRect = deviceRect; const QRect fillRect(x1, y1, w, h); auto toPatternLocal = [](int value, int offset, int width) { const int normalizedValue = value - offset; return offset + (normalizedValue >= 0 ? normalizedValue % width : width - (-normalizedValue - 1) % width - 1); }; int dstY = fillRect.y(); while (dstY <= fillRect.bottom()) { const int dstRowsRemaining = fillRect.bottom() - dstY + 1; const int srcY = toPatternLocal(dstY, patternRect.y(), patternRect.height()); const int height = qMin(patternRect.height() - srcY + patternRect.y(), dstRowsRemaining); int dstX = fillRect.x(); while (dstX <= fillRect.right()) { const int dstColumnsRemaining = fillRect.right() - dstX + 1; const int srcX = toPatternLocal(dstX, patternRect.x(), patternRect.width()); const int width = qMin(patternRect.width() - srcX + patternRect.x(), dstColumnsRemaining); bitBlt(dstX, dstY, device, srcX, srcY, width, height); dstX += width; } dstY += height; } addDirtyRect(QRect(x1, y1, w, h)); } void KisFillPainter::fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KisFilterConfigurationSP generator) { if (!generator) return; KisGeneratorSP g = KisGeneratorRegistry::instance()->value(generator->name()); if (!device()) return; if (w < 1) return; if (h < 1) return; QRect tmpRc(x1, y1, w, h); KisProcessingInformation dstCfg(device(), tmpRc.topLeft(), 0); g->generate(dstCfg, tmpRc.size(), generator); addDirtyRect(tmpRc); } // flood filling void KisFillPainter::fillColor(int startX, int startY, KisPaintDeviceSP sourceDevice) { if (!m_useCompositioning) { if (m_sizemod || m_feather || compositeOp()->id() != COMPOSITE_OVER || opacity() != MAX_SELECTED || sourceDevice != device()) { warnKrita << "WARNING: Fast Flood Fill (no compositioning mode)" << "does not support compositeOps, opacity, " << "selection enhancements and separate source " << "devices"; } QRect fillBoundsRect(0, 0, m_width, m_height); QPoint startPoint(startX, startY); if (!fillBoundsRect.contains(startPoint)) return; KisScanlineFill gc(device(), startPoint, fillBoundsRect); gc.setThreshold(m_threshold); gc.fillColor(paintColor()); } else { genericFillStart(startX, startY, sourceDevice); // Now create a layer and fill it KisPaintDeviceSP filled = device()->createCompositionSourceDevice(); Q_CHECK_PTR(filled); KisFillPainter painter(filled); painter.fillRect(0, 0, m_width, m_height, paintColor()); painter.end(); genericFillEnd(filled); } } void KisFillPainter::fillPattern(int startX, int startY, KisPaintDeviceSP sourceDevice, QTransform patternTransform) { genericFillStart(startX, startY, sourceDevice); // Now create a layer and fill it KisPaintDeviceSP filled = device()->createCompositionSourceDevice(); Q_CHECK_PTR(filled); KisFillPainter painter(filled); painter.fillRect(QRect(0, 0, m_width, m_height), pattern(), patternTransform); painter.end(); genericFillEnd(filled); } void KisFillPainter::genericFillStart(int startX, int startY, KisPaintDeviceSP sourceDevice) { Q_ASSERT(m_width > 0); Q_ASSERT(m_height > 0); // Create a selection from the surrounding area - KisPixelSelectionSP pixelSelection = createFloodSelection(startX, startY, sourceDevice); + KisPixelSelectionSP pixelSelection = createFloodSelection(startX, startY, sourceDevice, selection()->pixelSelection()); KisSelectionSP newSelection = new KisSelection(pixelSelection->defaultBounds()); newSelection->pixelSelection()->applySelection(pixelSelection, SELECTION_REPLACE); m_fillSelection = newSelection; } void KisFillPainter::genericFillEnd(KisPaintDeviceSP filled) { if (progressUpdater() && progressUpdater()->interrupted()) { m_width = m_height = -1; return; } // TODO: filling using the correct bound of the selection would be better, *but* // the selection is limited to the exact bound of a layer, while in reality, we don't // want that, since we want a transparent layer to be completely filled // QRect rc = m_fillSelection->selectedExactRect(); /** * Apply the real selection to a filled one */ KisSelectionSP realSelection = selection(); if (realSelection) { m_fillSelection->pixelSelection()->applySelection( realSelection->projection(), SELECTION_INTERSECT); } setSelection(m_fillSelection); bitBlt(0, 0, filled, 0, 0, m_width, m_height); setSelection(realSelection); if (progressUpdater()) progressUpdater()->setProgress(100); m_width = m_height = -1; } -KisPixelSelectionSP KisFillPainter::createFloodSelection(int startX, int startY, KisPaintDeviceSP sourceDevice) +KisPixelSelectionSP KisFillPainter::createFloodSelection(int startX, int startY, KisPaintDeviceSP sourceDevice, + KisPaintDeviceSP existingSelection) { KisPixelSelectionSP newSelection = new KisPixelSelection(new KisSelectionDefaultBounds(device())); - return createFloodSelection(newSelection, startX, startY, sourceDevice); + return createFloodSelection(newSelection, startX, startY, sourceDevice, existingSelection); } -KisPixelSelectionSP KisFillPainter::createFloodSelection(KisPixelSelectionSP pixelSelection, int startX, int startY, KisPaintDeviceSP sourceDevice) +KisPixelSelectionSP KisFillPainter::createFloodSelection(KisPixelSelectionSP pixelSelection, int startX, int startY, + KisPaintDeviceSP sourceDevice, KisPaintDeviceSP existingSelection) { if (m_width < 0 || m_height < 0) { if (selection() && m_careForSelection) { QRect rc = selection()->selectedExactRect(); m_width = rc.width() - (startX - rc.x()); m_height = rc.height() - (startY - rc.y()); } } dbgImage << "Width: " << m_width << " Height: " << m_height; // Otherwise the width and height should have been set Q_ASSERT(m_width > 0 && m_height > 0); QRect fillBoundsRect(0, 0, m_width, m_height); QPoint startPoint(startX, startY); if (!fillBoundsRect.contains(startPoint)) { return pixelSelection; } KisScanlineFill gc(sourceDevice, startPoint, fillBoundsRect); gc.setThreshold(m_threshold); - gc.fillSelection(pixelSelection); + if (m_useSelectionAsBoundary) { + gc.fillSelectionWithBoundary(pixelSelection, existingSelection); + } else { + gc.fillSelection(pixelSelection); + } if (m_sizemod > 0) { KisGrowSelectionFilter biggy(m_sizemod, m_sizemod); biggy.process(pixelSelection, pixelSelection->selectedRect().adjusted(-m_sizemod, -m_sizemod, m_sizemod, m_sizemod)); } else if (m_sizemod < 0) { KisShrinkSelectionFilter tiny(-m_sizemod, -m_sizemod, false); tiny.process(pixelSelection, pixelSelection->selectedRect()); } if (m_feather > 0) { KisFeatherSelectionFilter feathery(m_feather); feathery.process(pixelSelection, pixelSelection->selectedRect().adjusted(-m_feather, -m_feather, m_feather, m_feather)); } return pixelSelection; } diff --git a/libs/image/kis_fill_painter.h b/libs/image/kis_fill_painter.h index 4235e8283c..1e041d3e0e 100644 --- a/libs/image/kis_fill_painter.h +++ b/libs/image/kis_fill_painter.h @@ -1,319 +1,332 @@ /* * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Bart Coppens * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_FILL_PAINTER_H_ #define KIS_FILL_PAINTER_H_ #include #include #include #include #include "kis_painter.h" #include "kis_types.h" #include "kis_selection.h" #include class KisFilterConfiguration; // XXX: Filling should set dirty rect. /** * This painter can be used to fill paint devices in different ways. This can also be used * for flood filling related operations. */ class KRITAIMAGE_EXPORT KisFillPainter : public KisPainter { public: /** * Construct an empty painter. Use the begin(KisPaintDeviceSP) method to attach * to a paint device */ KisFillPainter(); /** * Start painting on the specified paint device */ KisFillPainter(KisPaintDeviceSP device); KisFillPainter(KisPaintDeviceSP device, KisSelectionSP selection); private: void initFillPainter(); public: /** * Fill a rectangle with black transparent pixels (0, 0, 0, 0 for RGBA). */ void eraseRect(qint32 x1, qint32 y1, qint32 w, qint32 h); /** * Overloaded version of the above function. */ void eraseRect(const QRect& rc); /** * Fill current selection of KisPainter with a specified \p color. * * The filling rect is limited by \p rc to allow multithreaded * filling/processing. */ void fillSelection(const QRect &rc, const KoColor &color); /** * Fill a rectangle with a certain color. */ void fillRect(qint32 x, qint32 y, qint32 w, qint32 h, const KoColor& c); /** * Overloaded version of the above function. */ void fillRect(const QRect& rc, const KoColor& c); /** * Fill a rectangle with a certain color and opacity. */ void fillRect(qint32 x, qint32 y, qint32 w, qint32 h, const KoColor& c, quint8 opacity); /** * Overloaded version of the above function. */ void fillRect(const QRect& rc, const KoColor& c, quint8 opacity); /** * Fill a rectangle with a certain pattern. The pattern is repeated if it does not fit the * entire rectangle. */ void fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KoPatternSP pattern, const QPoint &offset = QPoint()); /** * Fill a rectangle with a certain pattern. The pattern is repeated if it does not fit the * entire rectangle. * * This one uses blitting and thus makes use of proper composition. */ void fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KisPaintDeviceSP device, const QRect& deviceRect); /** * Overloaded version of the above function. */ void fillRect(const QRect& rc, const KoPatternSP pattern, const QPoint &offset = QPoint()); /** * @brief fillRect * Fill a rectangle with a certain pattern. The pattern is repeated if it does not fit the * entire rectangle. Differs from other functions that it uses a transform, does not support * composite ops in turn. * @param rc rectangle to fill. * @param pattern pattern to use. * @param transform transformation to apply to the pattern. */ void fillRect(const QRect& rc, const KoPatternSP pattern, const QTransform transform); /** * Fill a rectangle with a certain pattern. The pattern is repeated if it does not fit the * entire rectangle. * * This one supports transforms, but does not use blitting. */ void fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KisPaintDeviceSP device, const QRect& deviceRect, const QTransform transform); /** * Fill the specified area with the output of the generator plugin that is configured * in the generator parameter */ void fillRect(qint32 x1, qint32 y1, qint32 w, qint32 h, const KisFilterConfigurationSP generator); /** * Fills the enclosed area around the point with the set color. If * there is a selection, the whole selection is filled. Note that * you must have set the width and height on the painter if you * don't have a selection. * * @param startX the X position where the floodfill starts * @param startY the Y position where the floodfill starts * @param sourceDevice the sourceDevice that determines the area that * is floodfilled if sampleMerged is on */ void fillColor(int startX, int startY, KisPaintDeviceSP sourceDevice); /** * Fills the enclosed area around the point with the set pattern. * If there is a selection, the whole selection is filled. Note * that you must have set the width and height on the painter if * you don't have a selection. * * @param startX the X position where the floodfill starts * @param startY the Y position where the floodfill starts * @param sourceDevice the sourceDevice that determines the area that * is floodfilled if sampleMerged is on * @param patternTransform transform applied to the pattern; */ void fillPattern(int startX, int startY, KisPaintDeviceSP sourceDevice, QTransform patternTransform = QTransform()); /** * Returns a selection mask for the floodfill starting at the specified position. * This variant basically creates a new selection object and passes it down * to the other variant of the function. * * @param startX the X position where the floodfill starts * @param startY the Y position where the floodfill starts * @param sourceDevice the sourceDevice that determines the area that * is floodfilled if sampleMerged is on */ - KisPixelSelectionSP createFloodSelection(int startX, int startY, KisPaintDeviceSP sourceDevice); + KisPixelSelectionSP createFloodSelection(int startX, int startY, + KisPaintDeviceSP sourceDevice, KisPaintDeviceSP existingSelection); /** * Returns a selection mask for the floodfill starting at the specified position. * This variant requires an empty selection object. It is used in cases where the pointer * to the selection must be known beforehand, for example when the selection is filled * in a stroke and then the pointer to the pixel selection is needed later. * * @param selection empty new selection object * @param startX the X position where the floodfill starts * @param startY the Y position where the floodfill starts * @param sourceDevice the sourceDevice that determines the area that * is floodfilled if sampleMerged is on */ - KisPixelSelectionSP createFloodSelection(KisPixelSelectionSP newSelection, int startX, int startY, KisPaintDeviceSP sourceDevice); + KisPixelSelectionSP createFloodSelection(KisPixelSelectionSP newSelection, int startX, int startY, + KisPaintDeviceSP sourceDevice, KisPaintDeviceSP existingSelection); /** * Set the threshold for floodfill. The range is 0-255: 0 means the fill will only * fill parts that are the exact same color, 255 means anything will be filled */ void setFillThreshold(int threshold); /** Returns the fill threshold, see setFillThreshold for details */ int fillThreshold() const { return m_threshold; } bool useCompositioning() const { return m_useCompositioning; } void setUseCompositioning(bool useCompositioning) { m_useCompositioning = useCompositioning; } /** Sets the width of the paint device */ void setWidth(int w) { m_width = w; } /** Sets the height of the paint device */ void setHeight(int h) { m_height = h; } /** If true, floodfill doesn't fill outside the selected area of a layer */ bool careForSelection() const { return m_careForSelection; } /** Set caring for selection. See careForSelection for details */ void setCareForSelection(bool set) { m_careForSelection = set; } /** Sets the auto growth/shrinking radius */ void setSizemod(int sizemod) { m_sizemod = sizemod; } /** Sets how much to auto-grow or shrink (if @p sizemod is negative) the selection flood before painting, this affects every fill operation except fillRect */ int sizemod() { return m_sizemod; } /** Sets feathering radius */ void setFeather(int feather) { m_feather = feather; } /** defines the feathering radius for selection flood operations, this affects every fill operation except fillRect */ uint feather() { return m_feather; } + /** Sets selection borders being treated as boundary */ + void setUseSelectionAsBoundary(bool useSelectionAsBoundary) { + m_useSelectionAsBoundary = useSelectionAsBoundary; + } + + /** defines if the selection borders are treated as boundary in flood fill or not */ + uint useSelectionAsBoundary() { + return m_useSelectionAsBoundary; + } + private: // for floodfill void genericFillStart(int startX, int startY, KisPaintDeviceSP sourceDevice); void genericFillEnd(KisPaintDeviceSP filled); KisSelectionSP m_fillSelection; int m_feather; int m_sizemod; int m_threshold; int m_width, m_height; QRect m_rect; bool m_careForSelection; bool m_useCompositioning; + bool m_useSelectionAsBoundary; }; inline void KisFillPainter::fillRect(qint32 x, qint32 y, qint32 w, qint32 h, const KoColor& c) { fillRect(x, y, w, h, c, OPACITY_OPAQUE_U8); } inline void KisFillPainter::fillRect(const QRect& rc, const KoColor& c) { fillRect(rc.x(), rc.y(), rc.width(), rc.height(), c, OPACITY_OPAQUE_U8); } inline void KisFillPainter::eraseRect(qint32 x1, qint32 y1, qint32 w, qint32 h) { const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); KoColor c(Qt::black, cs); fillRect(x1, y1, w, h, c, OPACITY_TRANSPARENT_U8); } inline void KisFillPainter::eraseRect(const QRect& rc) { const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); KoColor c(Qt::black, cs); fillRect(rc.x(), rc.y(), rc.width(), rc.height(), c, OPACITY_TRANSPARENT_U8); } inline void KisFillPainter::fillRect(const QRect& rc, const KoColor& c, quint8 opacity) { fillRect(rc.x(), rc.y(), rc.width(), rc.height(), c, opacity); } inline void KisFillPainter::setFillThreshold(int threshold) { m_threshold = threshold; } #endif //KIS_FILL_PAINTER_H_ diff --git a/libs/ui/CMakeLists.txt b/libs/ui/CMakeLists.txt index f30e708c49..df86671484 100644 --- a/libs/ui/CMakeLists.txt +++ b/libs/ui/CMakeLists.txt @@ -1,657 +1,659 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/qtlockedfile ) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${OCIO_INCLUDE_DIR} ) if (ANDROID) add_definitions(-DQT_OPENGL_ES_3) add_definitions(-DHAS_ONLY_OPENGL_ES) include_directories (${Qt5AndroidExtras_INCLUDE_DIRS}) endif() add_subdirectory( tests ) if (APPLE) find_library(FOUNDATION_LIBRARY Foundation) find_library(APPKIT_LIBRARY AppKit) endif () set(kritaui_LIB_SRCS canvas/kis_canvas_widget_base.cpp canvas/kis_canvas2.cpp canvas/kis_canvas_updates_compressor.cpp canvas/kis_canvas_controller.cpp canvas/kis_display_color_converter.cpp canvas/kis_display_filter.cpp canvas/kis_exposure_gamma_correction_interface.cpp canvas/kis_tool_proxy.cpp canvas/kis_canvas_decoration.cc canvas/kis_coordinates_converter.cpp canvas/kis_grid_manager.cpp canvas/kis_grid_decoration.cpp canvas/kis_grid_config.cpp canvas/kis_prescaled_projection.cpp canvas/kis_qpainter_canvas.cpp canvas/kis_projection_backend.cpp canvas/kis_update_info.cpp canvas/kis_image_patch.cpp canvas/kis_image_pyramid.cpp canvas/kis_infinity_manager.cpp canvas/kis_change_guides_command.cpp canvas/kis_guides_decoration.cpp canvas/kis_guides_manager.cpp canvas/kis_guides_config.cpp canvas/kis_snap_config.cpp canvas/kis_snap_line_strategy.cpp canvas/KisSnapPointStrategy.cpp canvas/KisSnapPixelStrategy.cpp canvas/KisMirrorAxisConfig.cpp dialogs/kis_about_application.cpp dialogs/kis_dlg_adj_layer_props.cc dialogs/kis_dlg_adjustment_layer.cc dialogs/kis_dlg_filter.cpp dialogs/kis_dlg_generator_layer.cpp dialogs/kis_dlg_file_layer.cpp dialogs/kis_dlg_filter.cpp dialogs/kis_dlg_stroke_selection_properties.cpp dialogs/kis_dlg_image_properties.cc dialogs/kis_dlg_layer_properties.cc dialogs/kis_dlg_preferences.cc dialogs/slider_and_spin_box_sync.cpp dialogs/kis_dlg_layer_style.cpp dialogs/kis_dlg_png_import.cpp dialogs/kis_dlg_import_image_sequence.cpp dialogs/kis_delayed_save_dialog.cpp dialogs/KisSessionManagerDialog.cpp dialogs/KisNewWindowLayoutDialog.cpp dialogs/KisDlgChangeCloneSource.cpp dialogs/KisRecoverNamedAutosaveDialog.cpp flake/kis_node_dummies_graph.cpp flake/kis_dummies_facade_base.cpp flake/kis_dummies_facade.cpp flake/kis_node_shapes_graph.cpp flake/kis_node_shape.cpp flake/kis_shape_controller.cpp flake/kis_shape_layer.cc flake/kis_shape_layer_canvas.cpp flake/kis_shape_selection.cpp flake/kis_shape_selection_canvas.cpp flake/kis_shape_selection_model.cpp flake/kis_take_all_shapes_command.cpp brushhud/kis_uniform_paintop_property_widget.cpp brushhud/kis_brush_hud.cpp brushhud/kis_round_hud_button.cpp brushhud/kis_dlg_brush_hud_config.cpp brushhud/kis_brush_hud_properties_list.cpp brushhud/kis_brush_hud_properties_config.cpp kis_aspect_ratio_locker.cpp kis_autogradient.cc kis_bookmarked_configurations_editor.cc kis_bookmarked_configurations_model.cc kis_bookmarked_filter_configurations_model.cc KisPaintopPropertiesBase.cpp kis_canvas_resource_provider.cpp kis_derived_resources.cpp kis_categories_mapper.cpp kis_categorized_list_model.cpp kis_categorized_item_delegate.cpp kis_clipboard.cc kis_config.cc KisOcioConfiguration.cpp kis_control_frame.cpp kis_composite_ops_model.cc kis_paint_ops_model.cpp kis_custom_pattern.cc kis_file_layer.cpp kis_change_file_layer_command.h kis_safe_document_loader.cpp kis_splash_screen.cpp kis_filter_manager.cc kis_filters_model.cc KisImageBarrierLockerWithFeedback.cpp kis_image_manager.cc kis_image_view_converter.cpp kis_import_catcher.cc kis_layer_manager.cc kis_mask_manager.cc kis_mimedata.cpp kis_node_commands_adapter.cpp kis_node_manager.cpp kis_node_juggler_compressed.cpp kis_node_selection_adapter.cpp kis_node_insertion_adapter.cpp KisNodeDisplayModeAdapter.cpp kis_node_model.cpp kis_node_filter_proxy_model.cpp kis_model_index_converter_base.cpp kis_model_index_converter.cpp kis_model_index_converter_show_all.cpp kis_painting_assistant.cc kis_painting_assistants_decoration.cpp KisDecorationsManager.cpp kis_paintop_box.cc kis_paintop_option.cpp kis_paintop_options_model.cpp kis_paintop_settings_widget.cpp kis_popup_palette.cpp kis_png_converter.cpp kis_preference_set_registry.cpp KisResourceServerProvider.cpp KisSelectedShapesProxy.cpp kis_selection_decoration.cc kis_selection_manager.cc KisSelectionActionsAdapter.cpp kis_statusbar.cc kis_zoom_manager.cc kis_favorite_resource_manager.cpp kis_workspace_resource.cpp kis_action.cpp kis_action_manager.cpp KisActionPlugin.cpp kis_canvas_controls_manager.cpp kis_tooltip_manager.cpp kis_multinode_property.cpp kis_stopgradient_editor.cpp KisWelcomePageWidget.cpp KisChangeCloneLayersCommand.cpp kisexiv2/kis_exif_io.cpp kisexiv2/kis_exiv2.cpp kisexiv2/kis_iptc_io.cpp kisexiv2/kis_xmp_io.cpp opengl/kis_opengl.cpp opengl/kis_opengl_canvas2.cpp opengl/kis_opengl_canvas_debugger.cpp opengl/kis_opengl_image_textures.cpp opengl/kis_texture_tile.cpp opengl/kis_opengl_shader_loader.cpp opengl/kis_texture_tile_info_pool.cpp opengl/KisOpenGLUpdateInfoBuilder.cpp opengl/KisOpenGLModeProber.cpp opengl/KisScreenInformationAdapter.cpp kis_fps_decoration.cpp tool/KisToolChangesTracker.cpp tool/KisToolChangesTrackerData.cpp tool/kis_selection_tool_helper.cpp tool/kis_selection_tool_config_widget_helper.cpp tool/kis_rectangle_constraint_widget.cpp tool/kis_shape_tool_helper.cpp tool/kis_tool.cc tool/kis_delegated_tool_policies.cpp tool/kis_tool_freehand.cc tool/kis_speed_smoother.cpp tool/kis_painting_information_builder.cpp tool/kis_stabilized_events_sampler.cpp tool/kis_tool_freehand_helper.cpp tool/kis_tool_multihand_helper.cpp tool/kis_figure_painting_tool_helper.cpp tool/KisAsyncronousStrokeUpdateHelper.cpp tool/kis_tool_paint.cc tool/kis_tool_shape.cc tool/kis_tool_ellipse_base.cpp tool/kis_tool_rectangle_base.cpp tool/kis_tool_polyline_base.cpp tool/kis_tool_utils.cpp tool/kis_resources_snapshot.cpp tool/kis_smoothing_options.cpp tool/KisStabilizerDelayedPaintHelper.cpp tool/KisStrokeSpeedMonitor.cpp tool/strokes/freehand_stroke.cpp tool/strokes/KisStrokeEfficiencyMeasurer.cpp tool/strokes/kis_painter_based_stroke_strategy.cpp tool/strokes/kis_filter_stroke_strategy.cpp tool/strokes/kis_color_picker_stroke_strategy.cpp tool/strokes/KisFreehandStrokeInfo.cpp tool/strokes/KisMaskedFreehandStrokePainter.cpp tool/strokes/KisMaskingBrushRenderer.cpp tool/strokes/KisMaskingBrushCompositeOpFactory.cpp tool/strokes/move_stroke_strategy.cpp tool/strokes/KisNodeSelectionRecipe.cpp tool/KisSelectionToolFactoryBase.cpp tool/KisToolPaintFactoryBase.cpp widgets/kis_cmb_composite.cc widgets/kis_cmb_contour.cpp widgets/kis_cmb_gradient.cpp widgets/kis_paintop_list_widget.cpp widgets/kis_cmb_idlist.cc widgets/kis_color_space_selector.cc widgets/kis_advanced_color_space_selector.cc widgets/kis_cie_tongue_widget.cpp widgets/kis_tone_curve_widget.cpp widgets/kis_transport_controls.cpp widgets/kis_utility_title_bar.cpp widgets/kis_curve_widget.cpp widgets/kis_custom_image_widget.cc widgets/kis_image_from_clipboard_widget.cpp widgets/kis_double_widget.cc widgets/kis_filter_selector_widget.cc widgets/kis_gradient_chooser.cc widgets/kis_iconwidget.cc widgets/kis_mask_widgets.cpp widgets/kis_meta_data_merge_strategy_chooser_widget.cc widgets/kis_multi_bool_filter_widget.cc widgets/kis_multi_double_filter_widget.cc widgets/kis_multi_integer_filter_widget.cc widgets/kis_multipliers_double_slider_spinbox.cpp widgets/kis_paintop_presets_popup.cpp widgets/kis_tool_options_popup.cpp widgets/kis_paintop_presets_chooser_popup.cpp widgets/kis_paintop_presets_save.cpp widgets/kis_paintop_preset_icon_library.cpp widgets/kis_pattern_chooser.cc widgets/kis_preset_chooser.cpp widgets/kis_progress_widget.cpp widgets/kis_selection_options.cc widgets/kis_scratch_pad.cpp widgets/kis_scratch_pad_event_filter.cpp widgets/kis_preset_selector_strip.cpp widgets/KisSelectionPropertySlider.cpp widgets/kis_size_group.cpp widgets/kis_size_group_p.cpp widgets/kis_wdg_generator.cpp widgets/kis_workspace_chooser.cpp widgets/kis_categorized_list_view.cpp widgets/kis_widget_chooser.cpp widgets/kis_tool_button.cpp widgets/kis_floating_message.cpp widgets/kis_lod_availability_widget.cpp - widgets/kis_color_label_selector_widget.cpp widgets/kis_color_filter_combo.cpp + widgets/kis_color_label_selector_widget.cpp + widgets/kis_color_label_button.cpp + widgets/kis_layer_filter_widget.cpp widgets/kis_elided_label.cpp widgets/kis_stopgradient_slider_widget.cpp widgets/kis_preset_live_preview_view.cpp widgets/KisScreenColorPicker.cpp widgets/KoDualColorButton.cpp widgets/KoStrokeConfigWidget.cpp widgets/KoFillConfigWidget.cpp widgets/KisLayerStyleAngleSelector.cpp widgets/KisMemoryReportButton.cpp widgets/KisDitherWidget.cpp KisPaletteEditor.cpp dialogs/KisDlgPaletteEditor.cpp widgets/KisNewsWidget.cpp widgets/KisGamutMaskToolbar.cpp utils/kis_document_aware_spin_box_unit_manager.cpp utils/KisSpinBoxSplineUnitConverter.cpp utils/KisClipboardUtil.cpp utils/KisDitherUtil.cpp utils/KisFileIconCreator.cpp input/kis_input_manager.cpp input/kis_input_manager_p.cpp input/kis_extended_modifiers_mapper.cpp input/kis_abstract_input_action.cpp input/kis_tool_invocation_action.cpp input/kis_pan_action.cpp input/kis_alternate_invocation_action.cpp input/kis_rotate_canvas_action.cpp input/kis_zoom_action.cpp input/kis_change_frame_action.cpp input/kis_gamma_exposure_action.cpp input/kis_show_palette_action.cpp input/kis_change_primary_setting_action.cpp input/kis_abstract_shortcut.cpp input/kis_native_gesture_shortcut.cpp input/kis_single_action_shortcut.cpp input/kis_stroke_shortcut.cpp input/kis_shortcut_matcher.cpp input/kis_select_layer_action.cpp input/KisQtWidgetsTweaker.cpp input/KisInputActionGroup.cpp input/kis_zoom_and_rotate_action.cpp operations/kis_operation.cpp operations/kis_operation_configuration.cpp operations/kis_operation_registry.cpp operations/kis_operation_ui_factory.cpp operations/kis_operation_ui_widget.cpp operations/kis_filter_selection_operation.cpp actions/kis_selection_action_factories.cpp actions/KisPasteActionFactories.cpp actions/KisTransformToolActivationCommand.cpp input/kis_touch_shortcut.cpp kis_document_undo_store.cpp kis_gui_context_command.cpp kis_gui_context_command_p.cpp input/kis_tablet_debugger.cpp input/kis_input_profile_manager.cpp input/kis_input_profile.cpp input/kis_shortcut_configuration.cpp input/config/kis_input_configuration_page.cpp input/config/kis_edit_profiles_dialog.cpp input/config/kis_input_profile_model.cpp input/config/kis_input_configuration_page_item.cpp input/config/kis_action_shortcuts_model.cpp input/config/kis_input_type_delegate.cpp input/config/kis_input_mode_delegate.cpp input/config/kis_input_button.cpp input/config/kis_input_editor_delegate.cpp input/config/kis_mouse_input_editor.cpp input/config/kis_wheel_input_editor.cpp input/config/kis_key_input_editor.cpp processing/fill_processing_visitor.cpp canvas/kis_mirror_axis.cpp kis_abstract_perspective_grid.cpp KisApplication.cpp KisAutoSaveRecoveryDialog.cpp KisDetailsPane.cpp KisDocument.cpp KisCloneDocumentStroke.cpp kis_node_view_color_scheme.cpp KisImportExportFilter.cpp KisImportExportManager.cpp KisImportExportUtils.cpp kis_async_action_feedback.cpp KisMainWindow.cpp KisOpenPane.cpp KisPart.cpp KisTemplate.cpp KisTemplateCreateDia.cpp KisTemplateGroup.cpp KisTemplates.cpp KisTemplatesPane.cpp KisTemplateTree.cpp KisUndoActionsUpdateManager.cpp KisView.cpp KisCanvasWindow.cpp KisImportExportErrorCode.cpp KisImportExportAdditionalChecks.cpp thememanager.cpp kis_mainwindow_observer.cpp KisViewManager.cpp kis_mirror_manager.cpp qtlockedfile/qtlockedfile.cpp qtsingleapplication/qtlocalpeer.cpp qtsingleapplication/qtsingleapplication.cpp KisApplicationArguments.cpp KisNetworkAccessManager.cpp KisRssReader.cpp KisMultiFeedRSSModel.cpp KisRemoteFileFetcher.cpp KisSaveGroupVisitor.cpp KisWindowLayoutResource.cpp KisWindowLayoutManager.cpp KisSessionResource.cpp KisReferenceImagesDecoration.cpp KisReferenceImage.cpp flake/KisReferenceImagesLayer.cpp flake/KisReferenceImagesLayer.h KisMouseClickEater.cpp KisDecorationsWrapperLayer.cpp KoDocumentInfoDlg.cpp KoDocumentInfo.cpp ) if(WIN32) # Private headers are needed for: # * KisDlgCustomTabletResolution # * KisScreenInformationAdapter include_directories(SYSTEM ${Qt5Gui_PRIVATE_INCLUDE_DIRS}) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} qtlockedfile/qtlockedfile_win.cpp ) if (NOT USE_QT_TABLET_WINDOWS) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/wintab/kis_tablet_support_win.cpp input/wintab/kis_screen_size_choice_dialog.cpp input/wintab/kis_tablet_support_win8.cpp ) else() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} dialogs/KisDlgCustomTabletResolution.cpp ) endif() endif() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} kis_animation_frame_cache.cpp kis_animation_cache_populator.cpp KisAsyncAnimationRendererBase.cpp KisAsyncAnimationCacheRenderer.cpp KisAsyncAnimationFramesSavingRenderer.cpp dialogs/KisAsyncAnimationRenderDialogBase.cpp dialogs/KisAsyncAnimationCacheRenderDialog.cpp dialogs/KisAsyncAnimationFramesSaveDialog.cpp canvas/kis_animation_player.cpp kis_animation_importer.cpp KisSyncedAudioPlayback.cpp KisFrameDataSerializer.cpp KisFrameCacheStore.cpp KisFrameCacheSwapper.cpp KisAbstractFrameCacheSwapper.cpp KisInMemoryFrameCacheSwapper.cpp input/wintab/drawpile_tablettester/tablettester.cpp input/wintab/drawpile_tablettester/tablettest.cpp ) if (UNIX) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} qtlockedfile/qtlockedfile_unix.cpp ) endif() if (ENABLE_UPDATERS) if (UNIX) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} utils/KisAppimageUpdater.cpp ) endif() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} utils/KisUpdaterBase.cpp utils/KisManualUpdater.cpp utils/KisUpdaterStatus.cpp ) endif() if(APPLE) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/kis_extended_modifiers_mapper_osx.mm osx.mm ) endif() if (ANDROID) set (kritaui_LIB_SRCS ${kritaui_LIB_SRCS} KisAndroidFileManager.cpp) endif() ki18n_wrap_ui(kritaui_LIB_SRCS widgets/KoFillConfigWidget.ui widgets/KoStrokeConfigWidget.ui widgets/KisDitherWidget.ui forms/wdgdlgpngimport.ui forms/wdgfullscreensettings.ui forms/wdgautogradient.ui forms/wdggeneralsettings.ui forms/wdgperformancesettings.ui forms/wdggenerators.ui forms/wdgbookmarkedconfigurationseditor.ui forms/wdgapplyprofile.ui forms/wdgcustompattern.ui forms/wdglayerproperties.ui forms/wdgcolorsettings.ui forms/wdgtabletsettings.ui forms/wdgcolorspaceselector.ui forms/wdgcolorspaceselectoradvanced.ui forms/wdgdisplaysettings.ui forms/kis_matrix_widget.ui forms/wdgselectionoptions.ui forms/wdggeometryoptions.ui forms/wdgnewimage.ui forms/wdgimageproperties.ui forms/wdgmaskfromselection.ui forms/wdgmasksource.ui forms/wdgfilterdialog.ui forms/wdgmetadatamergestrategychooser.ui forms/wdgpaintoppresets.ui forms/wdgpaintopsettings.ui forms/wdgdlggeneratorlayer.ui forms/wdgdlgfilelayer.ui forms/wdgfilterselector.ui forms/wdgfilternodecreation.ui forms/wdgmultipliersdoublesliderspinbox.ui forms/wdgnodequerypatheditor.ui forms/wdgpresetselectorstrip.ui forms/wdgsavebrushpreset.ui forms/wdgpreseticonlibrary.ui forms/wdgrectangleconstraints.ui forms/wdgimportimagesequence.ui forms/wdgstrokeselectionproperties.ui forms/KisDetailsPaneBase.ui forms/KisOpenPaneBase.ui forms/wdgstopgradienteditor.ui forms/wdgsessionmanager.ui forms/wdgnewwindowlayout.ui forms/KisWelcomePage.ui forms/WdgDlgPaletteEditor.ui forms/KisNewsPage.ui forms/wdgGamutMaskToolbar.ui forms/wdgchangeclonesource.ui forms/koDocumentInfoAboutWidget.ui forms/koDocumentInfoAuthorWidget.ui brushhud/kis_dlg_brush_hud_config.ui dialogs/kis_delayed_save_dialog.ui dialogs/KisRecoverNamedAutosaveDialog.ui input/config/kis_input_configuration_page.ui input/config/kis_edit_profiles_dialog.ui input/config/kis_input_configuration_page_item.ui input/config/kis_mouse_input_editor.ui input/config/kis_wheel_input_editor.ui input/config/kis_key_input_editor.ui layerstyles/wdgBevelAndEmboss.ui layerstyles/wdgblendingoptions.ui layerstyles/WdgColorOverlay.ui layerstyles/wdgContour.ui layerstyles/wdgdropshadow.ui layerstyles/WdgGradientOverlay.ui layerstyles/wdgInnerGlow.ui layerstyles/wdglayerstyles.ui layerstyles/WdgPatternOverlay.ui layerstyles/WdgSatin.ui layerstyles/WdgStroke.ui layerstyles/wdgstylesselector.ui layerstyles/wdgTexture.ui layerstyles/wdgKisLayerStyleAngleSelector.ui wdgsplash.ui input/wintab/kis_screen_size_choice_dialog.ui input/wintab/drawpile_tablettester/tablettest.ui ) if(WIN32) if(USE_QT_TABLET_WINDOWS) ki18n_wrap_ui(kritaui_LIB_SRCS dialogs/KisDlgCustomTabletResolution.ui ) else() ki18n_wrap_ui(kritaui_LIB_SRCS input/wintab/kis_screen_size_choice_dialog.ui ) endif() endif() add_library(kritaui SHARED ${kritaui_HEADERS_MOC} ${kritaui_LIB_SRCS} ) generate_export_header(kritaui BASE_NAME kritaui) target_link_libraries(kritaui KF5::CoreAddons KF5::Completion KF5::I18n KF5::ItemViews Qt5::Network kritaversion kritaimpex kritacolor kritaimage kritalibbrush kritawidgets kritawidgetutils kritaresources ${PNG_LIBRARIES} LibExiv2::LibExiv2 ) if (ANDROID) target_link_libraries(kritaui GLESv3) target_link_libraries(kritaui Qt5::Gui) target_link_libraries(kritaui Qt5::AndroidExtras) endif() if (HAVE_QT_MULTIMEDIA) target_link_libraries(kritaui Qt5::Multimedia) endif() if (NOT WIN32 AND NOT APPLE AND NOT ANDROID) target_link_libraries(kritaui ${X11_X11_LIB} ${X11_Xinput_LIB}) endif() if(APPLE) target_link_libraries(kritaui ${FOUNDATION_LIBRARY}) target_link_libraries(kritaui ${APPKIT_LIBRARY}) endif () target_link_libraries(kritaui ${OPENEXR_LIBRARIES}) # Add VSync disable workaround if(NOT WIN32 AND NOT APPLE AND NOT ANDROID) target_link_libraries(kritaui ${CMAKE_DL_LIBS} Qt5::X11Extras) endif() if(X11_FOUND) target_link_libraries(kritaui Qt5::X11Extras ${X11_LIBRARIES}) endif() target_include_directories(kritaui PUBLIC $ $ $ $ $ $ $ ) set_target_properties(kritaui PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaui ${INSTALL_TARGETS_DEFAULT_ARGS}) if (APPLE) install(FILES osx.stylesheet DESTINATION ${DATA_INSTALL_DIR}/krita) endif () if (UNIX AND BUILD_TESTING AND ENABLE_UPDATERS) install(FILES tests/data/AppImageUpdateDummy PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) endif () diff --git a/libs/ui/actions/kis_selection_action_factories.cpp b/libs/ui/actions/kis_selection_action_factories.cpp index 32ebdd0ee1..1ebfe130fd 100644 --- a/libs/ui/actions/kis_selection_action_factories.cpp +++ b/libs/ui/actions/kis_selection_action_factories.cpp @@ -1,624 +1,625 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_selection_action_factories.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KisViewManager.h" #include "kis_canvas_resource_provider.h" #include "kis_clipboard.h" #include "kis_pixel_selection.h" #include "kis_paint_layer.h" #include "kis_image.h" #include "kis_image_barrier_locker.h" #include "kis_fill_painter.h" #include "kis_transaction.h" #include "kis_iterator_ng.h" #include "kis_processing_applicator.h" #include "kis_group_layer.h" #include "commands/kis_selection_commands.h" #include "commands/kis_image_layer_add_command.h" #include "kis_tool_proxy.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_selection_manager.h" #include "commands_new/kis_transaction_based_command.h" #include "kis_selection_filters.h" #include "kis_shape_selection.h" #include "kis_shape_layer.h" #include #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include #include #include "kis_figure_painting_tool_helper.h" #include "kis_update_outline_job.h" namespace ActionHelper { void copyFromDevice(KisViewManager *view, KisPaintDeviceSP device, bool makeSharpClip = false, const KisTimeRange &range = KisTimeRange()) { KisImageWSP image = view->image(); if (!image) return; KisSelectionSP selection = view->selection(); QRect rc = (selection) ? selection->selectedExactRect() : image->bounds(); KisPaintDeviceSP clip = new KisPaintDevice(device->colorSpace()); Q_CHECK_PTR(clip); const KoColorSpace *cs = clip->colorSpace(); // TODO if the source is linked... copy from all linked layers?!? // Copy image data KisPainter::copyAreaOptimized(QPoint(), device, clip, rc); if (selection) { // Apply selection mask. KisPaintDeviceSP selectionProjection = selection->projection(); KisHLineIteratorSP layerIt = clip->createHLineIteratorNG(0, 0, rc.width()); KisHLineConstIteratorSP selectionIt = selectionProjection->createHLineIteratorNG(rc.x(), rc.y(), rc.width()); const KoColorSpace *selCs = selection->projection()->colorSpace(); for (qint32 y = 0; y < rc.height(); y++) { for (qint32 x = 0; x < rc.width(); x++) { /** * Sharp method is an exact reverse of COMPOSITE_OVER * so if you cover the cut/copied piece over its source * you get an exactly the same image without any seams */ if (makeSharpClip) { qreal dstAlpha = cs->opacityF(layerIt->rawData()); qreal sel = selCs->opacityF(selectionIt->oldRawData()); qreal newAlpha = sel * dstAlpha / (1.0 - dstAlpha + sel * dstAlpha); float mask = newAlpha / dstAlpha; cs->applyAlphaNormedFloatMask(layerIt->rawData(), &mask, 1); } else { cs->applyAlphaU8Mask(layerIt->rawData(), selectionIt->oldRawData(), 1); } layerIt->nextPixel(); selectionIt->nextPixel(); } layerIt->nextRow(); selectionIt->nextRow(); } } KisClipboard::instance()->setClip(clip, rc.topLeft(), range); } } void KisSelectAllActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Select All")); if (!image->globalSelection()) { ap->applyCommand(new KisSetEmptyGlobalSelectionCommand(image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } struct SelectAll : public KisTransactionBasedCommand { SelectAll(KisImageSP image) : m_image(image) {} KisImageSP m_image; KUndo2Command* paint() override { KisSelectionSP selection = m_image->globalSelection(); KisSelectionTransaction transaction(selection->pixelSelection()); selection->pixelSelection()->clear(); selection->pixelSelection()->select(m_image->bounds()); return transaction.endAndTake(); } }; ap->applyCommand(new SelectAll(image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisDeselectActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KUndo2Command *cmd = new KisDeselectActiveSelectionCommand(view->selection(), image); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisReselectActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KUndo2Command *cmd = new KisReselectActiveSelectionCommand(view->activeNode(), image); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisFillActionFactory::run(const QString &fillSource, KisViewManager *view) { KisNodeSP node = view->activeNode(); if (!node || !node->hasEditablePaintDevice()) return; KisSelectionSP selection = view->selection(); QRect selectedRect = selection ? selection->selectedRect() : view->image()->bounds(); Q_UNUSED(selectedRect); KisPaintDeviceSP filled = node->paintDevice()->createCompositionSourceDevice(); Q_UNUSED(filled); bool usePattern = false; bool useBgColor = false; if (fillSource.contains("pattern")) { usePattern = true; } else if (fillSource.contains("bg")) { useBgColor = true; } KisProcessingApplicator applicator(view->image(), node, KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Flood Fill Layer")); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(view->image(), node, view->canvasResourceProvider()->resourceManager()); if (!fillSource.contains("opacity")) { resources->setOpacity(1.0); } KisProcessingVisitorSP visitor = new FillProcessingVisitor(resources->image()->projection(), QPoint(0, 0), // start position selection, resources, false, // fast mode usePattern, true, // fill only selection, + false, 0, // feathering radius 0, // sizemod 80, // threshold, false, // use unmerged useBgColor); applicator.applyVisitor(visitor, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.end(); view->canvasResourceProvider()->slotPainting(); } void KisClearActionFactory::run(KisViewManager *view) { // XXX: "Add saving of XML data for Clear action" view->canvasBase()->toolProxy()->deleteSelection(); } void KisImageResizeToSelectionActionFactory::run(KisViewManager *view) { // XXX: "Add saving of XML data for Image Resize To Selection action" KisSelectionSP selection = view->selection(); if (!selection) return; view->image()->cropImage(selection->selectedExactRect()); } void KisCutCopyActionFactory::run(bool willCut, bool makeSharpClip, KisViewManager *view) { KisImageSP image = view->image(); if (!image) return; bool haveShapesSelected = view->selectionManager()->haveShapesSelected(); if (haveShapesSelected) { // XXX: "Add saving of XML data for Cut/Copy of shapes" KisImageBarrierLocker locker(image); if (willCut) { view->canvasBase()->toolProxy()->cut(); } else { view->canvasBase()->toolProxy()->copy(); } } else { KisNodeSP node = view->activeNode(); if (!node) return; KisSelectionSP selection = view->selection(); if (selection.isNull()) return; { KisImageBarrierLocker locker(image); KisPaintDeviceSP dev = node->paintDevice(); if (!dev) { dev = node->projection(); } if (!dev) { view->showFloatingMessage( i18nc("floating message when cannot copy from a node", "Cannot copy pixels from this type of layer "), QIcon(), 3000, KisFloatingMessage::Medium); return; } if (dev->exactBounds().isEmpty()) { view->showFloatingMessage( i18nc("floating message when copying empty selection", "Selection is empty: no pixels were copied "), QIcon(), 3000, KisFloatingMessage::Medium); return; } KisTimeRange range; KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (channel) { const int currentTime = image->animationInterface()->currentTime(); range = channel->affectedFrames(currentTime); } ActionHelper::copyFromDevice(view, dev, makeSharpClip, range); } KUndo2Command *command = 0; if (willCut && node->hasEditablePaintDevice()) { struct ClearSelection : public KisTransactionBasedCommand { ClearSelection(KisNodeSP node, KisSelectionSP sel) : m_node(node), m_sel(sel) {} KisNodeSP m_node; KisSelectionSP m_sel; KUndo2Command* paint() override { KisSelectionSP cutSelection = m_sel; // Shrinking the cutting area was previously used // for getting seamless cut-paste. Now we use makeSharpClip // instead. // QRect originalRect = cutSelection->selectedExactRect(); // static const int preciseSelectionThreshold = 16; // // if (originalRect.width() > preciseSelectionThreshold || // originalRect.height() > preciseSelectionThreshold) { // cutSelection = new KisSelection(*m_sel); // delete cutSelection->flatten(); // // KisSelectionFilter* filter = new KisShrinkSelectionFilter(1, 1, false); // // QRect processingRect = filter->changeRect(originalRect); // filter->process(cutSelection->pixelSelection(), processingRect); // } KisTransaction transaction(m_node->paintDevice()); m_node->paintDevice()->clearSelection(cutSelection); m_node->setDirty(cutSelection->selectedRect()); return transaction.endAndTake(); } }; command = new ClearSelection(node, selection); } KUndo2MagicString actionName = willCut ? kundo2_i18n("Cut") : kundo2_i18n("Copy"); KisProcessingApplicator *ap = beginAction(view, actionName); if (command) { ap->applyCommand(command, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); } KisOperationConfiguration config(id()); config.setProperty("will-cut", willCut); endAction(ap, config.toXML()); } } void KisCopyMergedActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; if (!view->blockUntilOperationsFinished(image)) return; image->barrierLock(); KisPaintDeviceSP dev = image->root()->projection(); ActionHelper::copyFromDevice(view, dev); image->unlock(); KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Copy Merged")); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisInvertSelectionOperation::runFromXML(KisViewManager* view, const KisOperationConfiguration& config) { KisSelectionFilter* filter = new KisInvertSelectionFilter(); runFilter(filter, view, config); } void KisSelectionToVectorActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (selection->hasShapeSelection()) { view->showFloatingMessage(i18nc("floating message", "Selection is already in a vector format "), QIcon(), 2000, KisFloatingMessage::Low); return; } if (!selection->outlineCacheValid()) { view->image()->addSpontaneousJob(new KisUpdateOutlineJob(selection, false, Qt::transparent)); if (!view->blockUntilOperationsFinished(view->image())) { return; } } QPainterPath selectionOutline = selection->outlineCache(); QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(selectionOutline)); shape->setShapeId(KoPathShapeId); /** * Mark a shape that it belongs to a shape selection */ if(!shape->userData()) { shape->setUserData(new KisShapeSelectionMarker); } KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Convert to Vector Selection")); ap->applyCommand(view->canvasBase()->shapeController()->addShape(shape, 0), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisSelectionToRasterActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (!selection->hasShapeSelection()) { view->showFloatingMessage(i18nc("floating message", "Selection is already in a raster format "), QIcon(), 2000, KisFloatingMessage::Low); return; } KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Convert to Vector Selection")); struct RasterizeSelection : public KisTransactionBasedCommand { RasterizeSelection(KisSelectionSP sel) : m_sel(sel) {} KisSelectionSP m_sel; KUndo2Command* paint() override { // just create an empty transaction: it will rasterize the // selection and emit the necessary signals KisTransaction transaction(m_sel->pixelSelection()); return transaction.endAndTake(); } }; ap->applyCommand(new RasterizeSelection(selection), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisShapesToVectorSelectionActionFactory::run(KisViewManager* view) { const QList originalShapes = view->canvasBase()->shapeManager()->selection()->selectedShapes(); bool hasSelectionShapes = false; QList clonedShapes; Q_FOREACH (KoShape *shape, originalShapes) { if (dynamic_cast(shape->userData())) { hasSelectionShapes = true; continue; } clonedShapes << shape->cloneShape(); } if (clonedShapes.isEmpty()) { if (hasSelectionShapes) { view->showFloatingMessage(i18nc("floating message", "The shape already belongs to a selection"), QIcon(), 2000, KisFloatingMessage::Low); } return; } KisSelectionToolHelper helper(view->canvasBase(), kundo2_i18n("Convert shapes to vector selection")); helper.addSelectionShapes(clonedShapes); } void KisSelectionToShapeActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (!selection->outlineCacheValid()) { return; } QPainterPath selectionOutline = selection->outlineCache(); QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(selectionOutline)); shape->setShapeId(KoPathShapeId); KoColor fgColor = view->canvasBase()->resourceManager()->resource(KoCanvasResourceProvider::ForegroundColor).value(); KoShapeStrokeSP border(new KoShapeStroke(1.0, fgColor.toQColor())); shape->setStroke(border); KUndo2Command *cmd = view->canvasBase()->shapeController()->addShapeDirect(shape, 0); KisProcessingApplicator::runSingleCommandStroke(view->image(), cmd); } void KisStrokeSelectionActionFactory::run(KisViewManager *view, StrokeSelectionOptions params) { KisImageWSP image = view->image(); if (!image) { return; } KisSelectionSP selection = view->selection(); if (!selection) { return; } int size = params.lineSize; KisPixelSelectionSP pixelSelection = selection->projection(); if (!pixelSelection->outlineCacheValid()) { pixelSelection->recalculateOutlineCache(); } QPainterPath outline = pixelSelection->outlineCache(); QColor color = params.color.toQColor(); KisNodeSP currentNode = view->canvasResourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentKritaNode).value(); if (!currentNode->inherits("KisShapeLayer") && currentNode->paintDevice()) { KoCanvasResourceProvider * rManager = view->canvasResourceProvider()->resourceManager(); KisToolShapeUtils::StrokeStyle strokeStyle = KisToolShapeUtils::StrokeStyleForeground; KisToolShapeUtils::FillStyle fillStyle = params.fillStyle(); KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polyline"), image, currentNode, rManager , strokeStyle, fillStyle); helper.setFGColorOverride(params.color); helper.setSelectionOverride(0); QPen pen(Qt::red, size); pen.setJoinStyle(Qt::RoundJoin); if (fillStyle != KisToolShapeUtils::FillStyleNone) { helper.paintPainterPathQPenFill(outline, pen, params.fillColor); } else { helper.paintPainterPathQPen(outline, pen, params.fillColor); } } else if (currentNode->inherits("KisShapeLayer")) { QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(outline)); shape->setShapeId(KoPathShapeId); KoShapeStrokeSP border(new KoShapeStroke(size, color)); shape->setStroke(border); KUndo2Command *cmd = view->canvasBase()->shapeController()->addShapeDirect(shape, 0); KisProcessingApplicator::runSingleCommandStroke(view->image(), cmd); } image->setModified(); } void KisStrokeBrushSelectionActionFactory::run(KisViewManager *view, StrokeSelectionOptions params) { KisImageWSP image = view->image(); if (!image) { return; } KisSelectionSP selection = view->selection(); if (!selection) { return; } KisPixelSelectionSP pixelSelection = selection->projection(); if (!pixelSelection->outlineCacheValid()) { pixelSelection->recalculateOutlineCache(); } KisNodeSP currentNode = view->canvasResourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentKritaNode).value(); if (!currentNode->inherits("KisShapeLayer") && currentNode->paintDevice()) { KoCanvasResourceProvider * rManager = view->canvasResourceProvider()->resourceManager(); QPainterPath outline = pixelSelection->outlineCache(); KisToolShapeUtils::StrokeStyle strokeStyle = KisToolShapeUtils::StrokeStyleForeground; KisToolShapeUtils::FillStyle fillStyle = KisToolShapeUtils::FillStyleNone; KoColor color = params.color; KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polyline"), image, currentNode, rManager, strokeStyle, fillStyle); helper.setFGColorOverride(color); helper.setSelectionOverride(0); helper.paintPainterPath(outline); image->setModified(); } } diff --git a/libs/ui/forms/wdgdlgfilelayer.ui b/libs/ui/forms/wdgdlgfilelayer.ui index a21628b7d3..cd79479956 100644 --- a/libs/ui/forms/wdgdlgfilelayer.ui +++ b/libs/ui/forms/wdgdlgfilelayer.ui @@ -1,106 +1,150 @@ WdgDlgFileLayer 0 0 400 - 167 + 303 + + + 0 + 0 + + 400 0 &Layer Name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter txtLayerName File: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter txtLayerName - + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 9 + true + + + + <html><head/><body><p>Warning: Krita uses a relative path to store the location of the file in the .kra. If you move the .kra but not the file, the file layer may be broken.</p></body></html> + + + Qt::AutoText + + + false + + + Qt::AlignJustify|Qt::AlignTop + + + true + + + + + + 0 + 0 + + Scaling Options No Scaling true Scale to Image Size Adapt to Image Resolution (ppi) - - QLineEdit - QLineEdit -
klineedit.h
-
KisFileNameRequester QWidget
kis_file_name_requester.h
1
diff --git a/libs/ui/kis_node_filter_proxy_model.cpp b/libs/ui/kis_node_filter_proxy_model.cpp index e426ab1b7a..20dbc9bcf3 100644 --- a/libs/ui/kis_node_filter_proxy_model.cpp +++ b/libs/ui/kis_node_filter_proxy_model.cpp @@ -1,171 +1,183 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_node_filter_proxy_model.h" #include +#include + #include "kis_node.h" #include "kis_node_model.h" #include "kis_node_manager.h" #include "kis_signal_compressor.h" #include "kis_image.h" struct KisNodeFilterProxyModel::Private { Private() : nodeModel(0), activeNodeCompressor(1000, KisSignalCompressor::FIRST_INACTIVE) {} KisNodeModel *nodeModel; KisNodeSP pendingActiveNode; KisNodeSP activeNode; - QSet acceptedLabels; + QSet acceptedColorLabels; + boost::optional activeTextFilter; KisSignalCompressor activeNodeCompressor; bool isUpdatingFilter = false; bool checkIndexAllowedRecursively(QModelIndex srcIndex); }; KisNodeFilterProxyModel::KisNodeFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent), m_d(new Private) { connect(&m_d->activeNodeCompressor, SIGNAL(timeout()), SLOT(slotUpdateCurrentNodeFilter()), Qt::QueuedConnection); } KisNodeFilterProxyModel::~KisNodeFilterProxyModel() { } void KisNodeFilterProxyModel::setNodeModel(KisNodeModel *model) { m_d->nodeModel = model; setSourceModel(model); } bool KisNodeFilterProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (m_d->isUpdatingFilter && role == KisNodeModel::ActiveRole) { return false; } return QSortFilterProxyModel::setData(index, value, role); } bool KisNodeFilterProxyModel::Private::checkIndexAllowedRecursively(QModelIndex srcIndex) { if (!srcIndex.isValid()) return false; KisNodeSP node = nodeModel->nodeFromIndex(srcIndex); - if (node == activeNode || - acceptedLabels.contains(node->colorLabelIndex())) { + const bool nodeTextFilterMatch = (!activeTextFilter || node->name().contains(activeTextFilter.get(), Qt::CaseInsensitive)); + + // directParentTextFilterMatch -- There's an argument to be made that searching for a parent name should show + // all of the direct children of said text-search. For now, it will remain unused. + const bool directParentTextFilterMatch = (!activeTextFilter || (node->parent() && node->parent()->name().contains(activeTextFilter.get(), Qt::CaseInsensitive))); + Q_UNUSED(directParentTextFilterMatch); + const bool nodeColorMatch = (acceptedColorLabels.count() == 0 || acceptedColorLabels.contains(node->colorLabelIndex())); + if ( node == activeNode || + ( nodeColorMatch && nodeTextFilterMatch )) { return true; } bool result = false; const int numChildren = srcIndex.model()->rowCount(srcIndex); for (int i = 0; i < numChildren; i++) { QModelIndex child = nodeModel->index(i, 0, srcIndex); if (checkIndexAllowedRecursively(child)) { result = true; break; } } return result; } bool KisNodeFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { KIS_ASSERT_RECOVER(m_d->nodeModel) { return true; } const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); if (!index.isValid()) return false; KisNodeSP node = m_d->nodeModel->nodeFromIndex(index); return !node || - m_d->acceptedLabels.isEmpty() || + (m_d->acceptedColorLabels.isEmpty() && !m_d->activeTextFilter) || m_d->checkIndexAllowedRecursively(index); } KisNodeSP KisNodeFilterProxyModel::nodeFromIndex(const QModelIndex &index) const { KIS_ASSERT_RECOVER(m_d->nodeModel) { return 0; } QModelIndex srcIndex = mapToSource(index); return m_d->nodeModel->nodeFromIndex(srcIndex); } QModelIndex KisNodeFilterProxyModel::indexFromNode(KisNodeSP node) const { KIS_ASSERT_RECOVER(m_d->nodeModel) { return QModelIndex(); } QModelIndex srcIndex = m_d->nodeModel->indexFromNode(node); return mapFromSource(srcIndex); } -void KisNodeFilterProxyModel::setAcceptedLabels(const QList &value) +void KisNodeFilterProxyModel::setAcceptedLabels(const QSet &value) +{ + m_d->acceptedColorLabels = value; + invalidateFilter(); +} + +void KisNodeFilterProxyModel::setTextFilter(const QString &text) { -#if QT_VERSION >= QT_VERSION_CHECK(5,14,0) - m_d->acceptedLabels = QSet(value.begin(), value.end()); -#else - m_d->acceptedLabels = QSet::fromList(value); -#endif + m_d->activeTextFilter = !text.isEmpty() ? boost::make_optional(text) : boost::none; invalidateFilter(); } void KisNodeFilterProxyModel::setActiveNode(KisNodeSP node) { // NOTE: the current node might change due to beginRemoveRows, in such case // we must ensure we don't trigger recursive model invalidation. // the new node may temporary become null when the last layer // of the document in removed m_d->pendingActiveNode = node; m_d->activeNodeCompressor.start(); } void KisNodeFilterProxyModel::slotUpdateCurrentNodeFilter() { m_d->activeNode = m_d->pendingActiveNode; /** * During the filter update the model might emit "current changed" signals, * which (in their turn) will issue setData(..., KisNodeModel::ActiveRole) * call, leading to a double recursion. Which, obviously, crashes Krita. * * Right now, just blocking the KisNodeModel::ActiveRole call is the * most obvious solution for the problem. */ m_d->isUpdatingFilter = true; invalidateFilter(); m_d->isUpdatingFilter = false; } void KisNodeFilterProxyModel::unsetDummiesFacade() { m_d->nodeModel->setDummiesFacade(0, 0, 0, 0, 0); m_d->pendingActiveNode = 0; m_d->activeNode = 0; } diff --git a/libs/ui/kis_node_filter_proxy_model.h b/libs/ui/kis_node_filter_proxy_model.h index f7a56af662..d72359aa77 100644 --- a/libs/ui/kis_node_filter_proxy_model.h +++ b/libs/ui/kis_node_filter_proxy_model.h @@ -1,62 +1,63 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_NODE_FILTER_PROXY_MODEL_H #define __KIS_NODE_FILTER_PROXY_MODEL_H #include #include #include "kis_types.h" #include "kritaui_export.h" class KisNodeModel; class KisNodeDummy; class KisNodeManager; class KRITAUI_EXPORT KisNodeFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT public: KisNodeFilterProxyModel(QObject *parent); ~KisNodeFilterProxyModel() override; void setNodeModel(KisNodeModel *model); bool setData(const QModelIndex &index, const QVariant &value, int role) override; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; - void setAcceptedLabels(const QList &value); + void setAcceptedLabels(const QSet &value); + void setTextFilter(const QString &text); KisNodeSP nodeFromIndex(const QModelIndex &index) const; QModelIndex indexFromNode(KisNodeSP node) const; void unsetDummiesFacade(); public Q_SLOTS: void setActiveNode(KisNodeSP node); private Q_SLOTS: void slotUpdateCurrentNodeFilter(); private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_NODE_FILTER_PROXY_MODEL_H */ diff --git a/libs/ui/kis_painting_assistant.cc b/libs/ui/kis_painting_assistant.cc index 2eeb97c970..ebadb81628 100644 --- a/libs/ui/kis_painting_assistant.cc +++ b/libs/ui/kis_painting_assistant.cc @@ -1,977 +1,977 @@ /* * Copyright (c) 2008,2011 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "kis_painting_assistant.h" #include "kis_coordinates_converter.h" #include "kis_debug.h" #include "kis_dom_utils.h" #include #include "kis_tool.h" #include "kis_config.h" #include #include #include #include #include #include #include Q_GLOBAL_STATIC(KisPaintingAssistantFactoryRegistry, s_instance) struct KisPaintingAssistantHandle::Private { QList assistants; char handle_type; }; KisPaintingAssistantHandle::KisPaintingAssistantHandle(double x, double y) : QPointF(x, y), d(new Private) { } KisPaintingAssistantHandle::KisPaintingAssistantHandle(QPointF p) : QPointF(p), d(new Private) { } KisPaintingAssistantHandle::KisPaintingAssistantHandle(const KisPaintingAssistantHandle& rhs) : QPointF(rhs) , KisShared() , d(new Private) { dbgUI << "KisPaintingAssistantHandle ctor"; } KisPaintingAssistantHandle& KisPaintingAssistantHandle::operator=(const QPointF & pt) { setX(pt.x()); setY(pt.y()); return *this; } void KisPaintingAssistantHandle::setType(char type) { d->handle_type = type; } char KisPaintingAssistantHandle::handleType() const { return d->handle_type; } KisPaintingAssistant *KisPaintingAssistantHandle::chiefAssistant() const { return !d->assistants.isEmpty() ? d->assistants.first() : 0; } KisPaintingAssistantHandle::~KisPaintingAssistantHandle() { Q_ASSERT(d->assistants.empty()); delete d; } void KisPaintingAssistantHandle::registerAssistant(KisPaintingAssistant* assistant) { Q_ASSERT(!d->assistants.contains(assistant)); d->assistants.append(assistant); } void KisPaintingAssistantHandle::unregisterAssistant(KisPaintingAssistant* assistant) { d->assistants.removeOne(assistant); Q_ASSERT(!d->assistants.contains(assistant)); } bool KisPaintingAssistantHandle::containsAssistant(KisPaintingAssistant* assistant) const { return d->assistants.contains(assistant); } void KisPaintingAssistantHandle::mergeWith(KisPaintingAssistantHandleSP handle) { if(this->handleType()== HandleType::NORMAL || handle.data()->handleType()== HandleType::SIDE) { return; } Q_FOREACH (KisPaintingAssistant* assistant, handle->d->assistants) { if (!assistant->handles().contains(this)) { assistant->replaceHandle(handle, this); } } } void KisPaintingAssistantHandle::uncache() { Q_FOREACH (KisPaintingAssistant* assistant, d->assistants) { assistant->uncache(); } } struct KisPaintingAssistant::Private { Private(); explicit Private(const Private &rhs); KisPaintingAssistantHandleSP reuseOrCreateHandle(QMap &handleMap, KisPaintingAssistantHandleSP origHandle, KisPaintingAssistant *q, bool registerAssistant = true); QList handles, sideHandles; KisPaintingAssistantHandleSP topLeft, bottomLeft, topRight, bottomRight, topMiddle, bottomMiddle, rightMiddle, leftMiddle; // share everything except handles between the clones struct SharedData { QString id; QString name; bool isSnappingActive; bool outlineVisible; KisCanvas2* m_canvas = 0; QPixmapCache::Key cached; QRect cachedRect; // relative to boundingRect().topLeft() struct TranslationInvariantTransform { qreal m11, m12, m21, m22; TranslationInvariantTransform() { } TranslationInvariantTransform(const QTransform& t) : m11(t.m11()), m12(t.m12()), m21(t.m21()), m22(t.m22()) { } bool operator==(const TranslationInvariantTransform& b) { return m11 == b.m11 && m12 == b.m12 && m21 == b.m21 && m22 == b.m22; } } cachedTransform; QColor assistantGlobalColorCache = QColor(Qt::red); // color to paint with if a custom color is not set bool useCustomColor = false; QColor assistantCustomColor = KisConfig(true).defaultAssistantsColor(); }; QSharedPointer s; }; KisPaintingAssistant::Private::Private() : s(new SharedData) { } KisPaintingAssistant::Private::Private(const Private &rhs) : s(rhs.s) { } KisPaintingAssistantHandleSP KisPaintingAssistant::Private::reuseOrCreateHandle(QMap &handleMap, KisPaintingAssistantHandleSP origHandle, KisPaintingAssistant *q, bool registerAssistant) { KisPaintingAssistantHandleSP mappedHandle = handleMap.value(origHandle); if (!mappedHandle) { if (origHandle) { dbgUI << "handle not found in the map, creating a new one..."; mappedHandle = KisPaintingAssistantHandleSP(new KisPaintingAssistantHandle(*origHandle)); dbgUI << "done"; mappedHandle->setType(origHandle->handleType()); handleMap.insert(origHandle, mappedHandle); } else { dbgUI << "orig handle is null, not doing anything"; mappedHandle = KisPaintingAssistantHandleSP(); } } if (mappedHandle && registerAssistant) { mappedHandle->registerAssistant(q); } return mappedHandle; } bool KisPaintingAssistant::useCustomColor() { return d->s->useCustomColor; } void KisPaintingAssistant::setUseCustomColor(bool useCustomColor) { d->s->useCustomColor = useCustomColor; } void KisPaintingAssistant::setAssistantCustomColor(QColor color) { d->s->assistantCustomColor = color; } QColor KisPaintingAssistant::assistantCustomColor() { return d->s->assistantCustomColor; } void KisPaintingAssistant::setAssistantGlobalColorCache(const QColor &color) { d->s->assistantGlobalColorCache = color; } QColor KisPaintingAssistant::effectiveAssistantColor() const { return d->s->useCustomColor ? d->s->assistantCustomColor : d->s->assistantGlobalColorCache; } KisPaintingAssistant::KisPaintingAssistant(const QString& id, const QString& name) : d(new Private) { d->s->id = id; d->s->name = name; d->s->isSnappingActive = true; d->s->outlineVisible = true; } KisPaintingAssistant::KisPaintingAssistant(const KisPaintingAssistant &rhs, QMap &handleMap) : d(new Private(*(rhs.d))) { dbgUI << "creating handles..."; Q_FOREACH (const KisPaintingAssistantHandleSP origHandle, rhs.d->handles) { d->handles << d->reuseOrCreateHandle(handleMap, origHandle, this); } Q_FOREACH (const KisPaintingAssistantHandleSP origHandle, rhs.d->sideHandles) { d->sideHandles << d->reuseOrCreateHandle(handleMap, origHandle, this); } #define _REUSE_H(name) d->name = d->reuseOrCreateHandle(handleMap, rhs.d->name, this, /* registerAssistant = */ false) _REUSE_H(topLeft); _REUSE_H(bottomLeft); _REUSE_H(topRight); _REUSE_H(bottomRight); _REUSE_H(topMiddle); _REUSE_H(bottomMiddle); _REUSE_H(rightMiddle); _REUSE_H(leftMiddle); #undef _REUSE_H dbgUI << "done"; } bool KisPaintingAssistant::isSnappingActive() const { return d->s->isSnappingActive; } void KisPaintingAssistant::setSnappingActive(bool set) { d->s->isSnappingActive = set; } void KisPaintingAssistant::drawPath(QPainter& painter, const QPainterPath &path, bool isSnappingOn) { QColor paintingColor = effectiveAssistantColor(); if (!isSnappingOn) { paintingColor.setAlpha(0.2 * paintingColor.alpha()); } painter.save(); QPen pen_a(paintingColor, 2); pen_a.setCosmetic(true); painter.setPen(pen_a); painter.drawPath(path); painter.restore(); } void KisPaintingAssistant::drawPreview(QPainter& painter, const QPainterPath &path) { painter.save(); QPen pen_a(effectiveAssistantColor(), 1); pen_a.setStyle(Qt::SolidLine); pen_a.setCosmetic(true); painter.setPen(pen_a); painter.drawPath(path); painter.restore(); } void KisPaintingAssistant::initHandles(QList _handles) { Q_ASSERT(d->handles.isEmpty()); d->handles = _handles; Q_FOREACH (KisPaintingAssistantHandleSP handle, _handles) { handle->registerAssistant(this); } } KisPaintingAssistant::~KisPaintingAssistant() { Q_FOREACH (KisPaintingAssistantHandleSP handle, d->handles) { handle->unregisterAssistant(this); } if(!d->sideHandles.isEmpty()) { Q_FOREACH (KisPaintingAssistantHandleSP handle, d->sideHandles) { handle->unregisterAssistant(this); } } delete d; } const QString& KisPaintingAssistant::id() const { return d->s->id; } const QString& KisPaintingAssistant::name() const { return d->s->name; } void KisPaintingAssistant::replaceHandle(KisPaintingAssistantHandleSP _handle, KisPaintingAssistantHandleSP _with) { Q_ASSERT(d->handles.contains(_handle)); d->handles.replace(d->handles.indexOf(_handle), _with); Q_ASSERT(!d->handles.contains(_handle)); _handle->unregisterAssistant(this); _with->registerAssistant(this); } void KisPaintingAssistant::addHandle(KisPaintingAssistantHandleSP handle, HandleType type) { Q_ASSERT(!d->handles.contains(handle)); if (HandleType::SIDE == type) { d->sideHandles.append(handle); } else { d->handles.append(handle); } handle->registerAssistant(this); handle.data()->setType(type); } QPointF KisPaintingAssistant::viewportConstrainedEditorPosition(const KisCoordinatesConverter* converter, const QSize editorSize) { QPointF editorDocumentPos = getEditorPosition(); QPointF editorWidgetPos = converter->documentToWidgetTransform().map(editorDocumentPos); QSizeF canvasSize = converter->getCanvasWidgetSize(); const int padding = 16; editorWidgetPos.rx() = qBound(0.0, editorWidgetPos.x(), canvasSize.width() - (editorSize.width() + padding)); editorWidgetPos.ry() = qBound(0.0, editorWidgetPos.y(), canvasSize.height() - (editorSize.height() + padding)); return converter->widgetToDocument(editorWidgetPos); } void KisPaintingAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool useCache, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { Q_UNUSED(updateRect); Q_UNUSED(previewVisible); findPerspectiveAssistantHandleLocation(); if (!useCache) { gc.save(); drawCache(gc, converter, assistantVisible); gc.restore(); return; } const QRect bound = boundingRect(); if (bound.isEmpty()) { return; } const QTransform transform = converter->documentToWidgetTransform(); const QRect widgetBound = transform.mapRect(bound); const QRect paintRect = transform.mapRect(bound).intersected(gc.viewport()); if (paintRect.isEmpty()) return; QPixmap cached; bool found = QPixmapCache::find(d->s->cached, &cached); if (!(found && d->s->cachedTransform == transform && d->s->cachedRect.translated(widgetBound.topLeft()).contains(paintRect))) { const QRect cacheRect = gc.viewport().adjusted(-100, -100, 100, 100).intersected(widgetBound); Q_ASSERT(!cacheRect.isEmpty()); if (cached.isNull() || cached.size() != cacheRect.size()) { cached = QPixmap(cacheRect.size()); } cached.fill(Qt::transparent); QPainter painter(&cached); painter.setRenderHint(QPainter::Antialiasing); painter.setWindow(cacheRect); drawCache(painter, converter, assistantVisible); painter.end(); d->s->cachedTransform = transform; d->s->cachedRect = cacheRect.translated(-widgetBound.topLeft()); d->s->cached = QPixmapCache::insert(cached); } gc.drawPixmap(paintRect, cached, paintRect.translated(-widgetBound.topLeft() - d->s->cachedRect.topLeft())); if (canvas) { d->s->m_canvas = canvas; } } void KisPaintingAssistant::uncache() { d->s->cached = QPixmapCache::Key(); } QRect KisPaintingAssistant::boundingRect() const { QRectF r; Q_FOREACH (KisPaintingAssistantHandleSP h, handles()) { r = r.united(QRectF(*h, QSizeF(1,1))); } return r.adjusted(-2, -2, 2, 2).toAlignedRect(); } bool KisPaintingAssistant::isAssistantComplete() const { return true; } void KisPaintingAssistant::transform(const QTransform &transform) { Q_FOREACH(KisPaintingAssistantHandleSP handle, handles()) { if (handle->chiefAssistant() != this) continue; *handle = transform.map(*handle); } Q_FOREACH(KisPaintingAssistantHandleSP handle, sideHandles()) { if (handle->chiefAssistant() != this) continue; *handle = transform.map(*handle); } uncache(); } QByteArray KisPaintingAssistant::saveXml(QMap &handleMap) { QByteArray data; QXmlStreamWriter xml(&data); xml.writeStartDocument(); xml.writeStartElement("assistant"); xml.writeAttribute("type",d->s->id); xml.writeAttribute("active", QString::number(d->s->isSnappingActive)); xml.writeAttribute("useCustomColor", QString::number(d->s->useCustomColor)); xml.writeAttribute("customColor", KisDomUtils::qColorToQString(d->s->assistantCustomColor)); saveCustomXml(&xml); // if any specific assistants have custom XML data to save to // write individual handle data xml.writeStartElement("handles"); Q_FOREACH (const KisPaintingAssistantHandleSP handle, d->handles) { int id = handleMap.size(); if (!handleMap.contains(handle)){ handleMap.insert(handle, id); } id = handleMap.value(handle); xml.writeStartElement("handle"); xml.writeAttribute("id", QString::number(id)); xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); xml.writeEndElement(); } xml.writeEndElement(); if (!d->sideHandles.isEmpty()) { // for vanishing points only - xml.writeStartElement("sidehandles"); - QMap sideHandleMap; - Q_FOREACH (KisPaintingAssistantHandleSP handle, d->sideHandles) { - int id = sideHandleMap.size(); - sideHandleMap.insert(handle, id); - xml.writeStartElement("sidehandle"); - xml.writeAttribute("id", QString::number(id)); - xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); - xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); - xml.writeEndElement(); + xml.writeStartElement("sidehandles"); + QMap sideHandleMap; + Q_FOREACH (KisPaintingAssistantHandleSP handle, d->sideHandles) { + int id = sideHandleMap.size(); + sideHandleMap.insert(handle, id); + xml.writeStartElement("sidehandle"); + xml.writeAttribute("id", QString::number(id)); + xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); + xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); + xml.writeEndElement(); } } xml.writeEndElement(); xml.writeEndDocument(); return data; } void KisPaintingAssistant::saveCustomXml(QXmlStreamWriter* xml) { Q_UNUSED(xml); } void KisPaintingAssistant::loadXml(KoStore* store, QMap &handleMap, QString path) { int id = 0; double x = 0.0, y = 0.0; store->open(path); QByteArray data = store->read(store->size()); QXmlStreamReader xml(data); QMap sideHandleMap; while (!xml.atEnd()) { switch (xml.readNext()) { case QXmlStreamReader::StartElement: if (xml.name() == "assistant") { QStringRef active = xml.attributes().value("active"); setSnappingActive( (active != "0") ); // load custom shared assistant properties if ( xml.attributes().hasAttribute("useCustomColor")) { QStringRef useCustomColor = xml.attributes().value("useCustomColor"); bool usingColor = false; if (useCustomColor.toString() == "1") { usingColor = true; } setUseCustomColor(usingColor); } if ( xml.attributes().hasAttribute("customColor")) { QStringRef customColor = xml.attributes().value("customColor"); setAssistantCustomColor( KisDomUtils::qStringToQColor(customColor.toString()) ); } } loadCustomXml(&xml); if (xml.name() == "handle") { QString strId = xml.attributes().value("id").toString(), strX = xml.attributes().value("x").toString(), strY = xml.attributes().value("y").toString(); if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { id = strId.toInt(); x = strX.toDouble(); y = strY.toDouble(); if (!handleMap.contains(id)) { handleMap.insert(id, new KisPaintingAssistantHandle(x, y)); } } addHandle(handleMap.value(id), HandleType::NORMAL); } else if (xml.name() == "sidehandle") { QString strId = xml.attributes().value("id").toString(), strX = xml.attributes().value("x").toString(), strY = xml.attributes().value("y").toString(); if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { id = strId.toInt(); x = strX.toDouble(); y = strY.toDouble(); if (!sideHandleMap.contains(id)) { sideHandleMap.insert(id, new KisPaintingAssistantHandle(x, y)); } } addHandle(sideHandleMap.value(id), HandleType::SIDE); } break; default: break; } } store->close(); } bool KisPaintingAssistant::loadCustomXml(QXmlStreamReader* xml) { Q_UNUSED(xml); return true; } void KisPaintingAssistant::saveXmlList(QDomDocument& doc, QDomElement& assistantsElement,int count) { if (d->s->id == "ellipse"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "ellipse"); assistantElement.setAttribute("filename", QString("ellipse%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "spline"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "spline"); assistantElement.setAttribute("filename", QString("spline%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "perspective"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "perspective"); assistantElement.setAttribute("filename", QString("perspective%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "vanishing point"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "vanishing point"); assistantElement.setAttribute("filename", QString("vanishing point%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "infinite ruler"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "infinite ruler"); assistantElement.setAttribute("filename", QString("infinite ruler%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "parallel ruler"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "parallel ruler"); assistantElement.setAttribute("filename", QString("parallel ruler%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "concentric ellipse"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "concentric ellipse"); assistantElement.setAttribute("filename", QString("concentric ellipse%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "fisheye-point"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "fisheye-point"); assistantElement.setAttribute("filename", QString("fisheye-point%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "ruler"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "ruler"); assistantElement.setAttribute("filename", QString("ruler%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "conjugate"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "conjugate"); assistantElement.setAttribute("filename", QString("conjugate%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } } void KisPaintingAssistant::findPerspectiveAssistantHandleLocation() { QList hHandlesList; QList vHandlesList; uint vHole = 0,hHole = 0; KisPaintingAssistantHandleSP oppHandle; if (d->handles.size() == 4 && d->s->id == "perspective") { //get the handle opposite to the first handle oppHandle = oppHandleOne(); //Sorting handles into two list, X sorted and Y sorted into hHandlesList and vHandlesList respectively. Q_FOREACH (const KisPaintingAssistantHandleSP handle,d->handles) { hHandlesList.append(handle); hHole = hHandlesList.size() - 1; vHandlesList.append(handle); vHole = vHandlesList.size() - 1; /* sort handles on the basis of X-coordinate */ while(hHole > 0 && hHandlesList.at(hHole -1).data()->x() > handle.data()->x()) { #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) hHandlesList.swapItemsAt(hHole - 1, hHole); #else hHandlesList.swap(hHole - 1, hHole); #endif hHole = hHole - 1; } /* sort handles on the basis of Y-coordinate */ while(vHole > 0 && vHandlesList.at(vHole -1).data()->y() > handle.data()->y()) { #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) vHandlesList.swapItemsAt(vHole-1, vHole); #else vHandlesList.swap(vHole-1, vHole); #endif vHole = vHole - 1; } } /* give the handles their respective positions */ if(vHandlesList.at(0).data()->x() > vHandlesList.at(1).data()->x()) { d->topLeft = vHandlesList.at(1); d->topRight= vHandlesList.at(0); } else { d->topLeft = vHandlesList.at(0); d->topRight = vHandlesList.at(1); } if(vHandlesList.at(2).data()->x() > vHandlesList.at(3).data()->x()) { d->bottomLeft = vHandlesList.at(3); d->bottomRight = vHandlesList.at(2); } else { d->bottomLeft= vHandlesList.at(2); d->bottomRight = vHandlesList.at(3); } /* find if the handles that should be opposite are actually oppositely positioned */ if (( (d->topLeft == d->handles.at(0).data() && d->bottomRight == oppHandle) || (d->topLeft == oppHandle && d->bottomRight == d->handles.at(0).data()) || (d->topRight == d->handles.at(0).data() && d->bottomLeft == oppHandle) || (d->topRight == oppHandle && d->bottomLeft == d->handles.at(0).data()) ) ) {} else { if(hHandlesList.at(0).data()->y() > hHandlesList.at(1).data()->y()) { d->topLeft = hHandlesList.at(1); d->bottomLeft= hHandlesList.at(0); } else { d->topLeft = hHandlesList.at(0); d->bottomLeft = hHandlesList.at(1); } if(hHandlesList.at(2).data()->y() > hHandlesList.at(3).data()->y()) { d->topRight = hHandlesList.at(3); d->bottomRight = hHandlesList.at(2); } else { d->topRight= hHandlesList.at(2); d->bottomRight = hHandlesList.at(3); } } /* Setting the middle handles as needed */ if(!d->bottomMiddle && !d->topMiddle && !d->leftMiddle && !d->rightMiddle) { d->bottomMiddle = new KisPaintingAssistantHandle((d->bottomLeft.data()->x() + d->bottomRight.data()->x())*0.5, (d->bottomLeft.data()->y() + d->bottomRight.data()->y())*0.5); d->topMiddle = new KisPaintingAssistantHandle((d->topLeft.data()->x() + d->topRight.data()->x())*0.5, (d->topLeft.data()->y() + d->topRight.data()->y())*0.5); d->rightMiddle= new KisPaintingAssistantHandle((d->topRight.data()->x() + d->bottomRight.data()->x())*0.5, (d->topRight.data()->y() + d->bottomRight.data()->y())*0.5); d->leftMiddle= new KisPaintingAssistantHandle((d->bottomLeft.data()->x() + d->topLeft.data()->x())*0.5, (d->bottomLeft.data()->y() + d->topLeft.data()->y())*0.5); addHandle(d->rightMiddle.data(), HandleType::SIDE); addHandle(d->leftMiddle.data(), HandleType::SIDE); addHandle(d->bottomMiddle.data(), HandleType::SIDE); addHandle(d->topMiddle.data(), HandleType::SIDE); } else { d->bottomMiddle.data()->operator =(QPointF((d->bottomLeft.data()->x() + d->bottomRight.data()->x())*0.5, (d->bottomLeft.data()->y() + d->bottomRight.data()->y())*0.5)); d->topMiddle.data()->operator =(QPointF((d->topLeft.data()->x() + d->topRight.data()->x())*0.5, (d->topLeft.data()->y() + d->topRight.data()->y())*0.5)); d->rightMiddle.data()->operator =(QPointF((d->topRight.data()->x() + d->bottomRight.data()->x())*0.5, (d->topRight.data()->y() + d->bottomRight.data()->y())*0.5)); d->leftMiddle.data()->operator =(QPointF((d->bottomLeft.data()->x() + d->topLeft.data()->x())*0.5, (d->bottomLeft.data()->y() + d->topLeft.data()->y())*0.5)); } } } KisPaintingAssistantHandleSP KisPaintingAssistant::oppHandleOne() { QPointF intersection(0,0); if((QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(1).data()->toPoint()).intersect(QLineF(d->handles.at(2).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::NoIntersection) && (QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(1).data()->toPoint()).intersect(QLineF(d->handles.at(2).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::UnboundedIntersection)) { return d->handles.at(1); } else if((QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(2).data()->toPoint()).intersect(QLineF(d->handles.at(1).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::NoIntersection) && (QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(2).data()->toPoint()).intersect(QLineF(d->handles.at(1).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::UnboundedIntersection)) { return d->handles.at(2); } else { return d->handles.at(3); } } KisPaintingAssistantHandleSP KisPaintingAssistant::topLeft() { return d->topLeft; } const KisPaintingAssistantHandleSP KisPaintingAssistant::topLeft() const { return d->topLeft; } KisPaintingAssistantHandleSP KisPaintingAssistant::bottomLeft() { return d->bottomLeft; } const KisPaintingAssistantHandleSP KisPaintingAssistant::bottomLeft() const { return d->bottomLeft; } KisPaintingAssistantHandleSP KisPaintingAssistant::topRight() { return d->topRight; } const KisPaintingAssistantHandleSP KisPaintingAssistant::topRight() const { return d->topRight; } KisPaintingAssistantHandleSP KisPaintingAssistant::bottomRight() { return d->bottomRight; } const KisPaintingAssistantHandleSP KisPaintingAssistant::bottomRight() const { return d->bottomRight; } KisPaintingAssistantHandleSP KisPaintingAssistant::topMiddle() { return d->topMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::topMiddle() const { return d->topMiddle; } KisPaintingAssistantHandleSP KisPaintingAssistant::bottomMiddle() { return d->bottomMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::bottomMiddle() const { return d->bottomMiddle; } KisPaintingAssistantHandleSP KisPaintingAssistant::rightMiddle() { return d->rightMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::rightMiddle() const { return d->rightMiddle; } KisPaintingAssistantHandleSP KisPaintingAssistant::leftMiddle() { return d->leftMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::leftMiddle() const { return d->leftMiddle; } const QList& KisPaintingAssistant::handles() const { return d->handles; } QList KisPaintingAssistant::handles() { return d->handles; } const QList& KisPaintingAssistant::sideHandles() const { return d->sideHandles; } QList KisPaintingAssistant::sideHandles() { return d->sideHandles; } bool KisPaintingAssistant::areTwoPointsClose(const QPointF& pointOne, const QPointF& pointTwo) { int m_handleSize = 16; QRectF handlerect(pointTwo - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize)); return handlerect.contains(pointOne); } KisPaintingAssistantHandleSP KisPaintingAssistant::closestCornerHandleFromPoint(QPointF point) { if (!d->s->m_canvas) { return 0; } if (areTwoPointsClose(point, pixelToView(topLeft()->toPoint()))) { return topLeft(); } else if (areTwoPointsClose(point, pixelToView(topRight()->toPoint()))) { return topRight(); } else if (areTwoPointsClose(point, pixelToView(bottomLeft()->toPoint()))) { return bottomLeft(); } else if (areTwoPointsClose(point, pixelToView(bottomRight()->toPoint()))) { return bottomRight(); } return 0; } QPointF KisPaintingAssistant::pixelToView(const QPoint pixelCoords) const { QPointF documentCoord = d->s->m_canvas->image()->pixelToDocument(pixelCoords); return d->s->m_canvas->viewConverter()->documentToView(documentCoord); } double KisPaintingAssistant::norm2(const QPointF& p) { return p.x() * p.x() + p.y() * p.y(); } QList KisPaintingAssistant::cloneAssistantList(const QList &list) { QMap handleMap; QList clonedList; for (auto i = list.begin(); i != list.end(); ++i) { clonedList << (*i)->clone(handleMap); } return clonedList; } /* * KisPaintingAssistantFactory classes */ KisPaintingAssistantFactory::KisPaintingAssistantFactory() { } KisPaintingAssistantFactory::~KisPaintingAssistantFactory() { } KisPaintingAssistantFactoryRegistry::KisPaintingAssistantFactoryRegistry() { } KisPaintingAssistantFactoryRegistry::~KisPaintingAssistantFactoryRegistry() { Q_FOREACH (const QString &id, keys()) { delete get(id); } dbgRegistry << "deleting KisPaintingAssistantFactoryRegistry "; } KisPaintingAssistantFactoryRegistry* KisPaintingAssistantFactoryRegistry::instance() { return s_instance; } diff --git a/libs/ui/processing/fill_processing_visitor.cpp b/libs/ui/processing/fill_processing_visitor.cpp index 4711ca2fa1..5e100d8fdc 100644 --- a/libs/ui/processing/fill_processing_visitor.cpp +++ b/libs/ui/processing/fill_processing_visitor.cpp @@ -1,148 +1,151 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "fill_processing_visitor.h" #include #include #include #include #include "lazybrush/kis_colorize_mask.h" FillProcessingVisitor::FillProcessingVisitor(KisPaintDeviceSP refPaintDevice, const QPoint &startPoint, KisSelectionSP selection, KisResourcesSnapshotSP resources, bool useFastMode, bool usePattern, bool selectionOnly, + bool useSelectionAsBoundary, int feather, int sizemod, int fillThreshold, bool unmerged, bool useBgColor) : m_refPaintDevice(refPaintDevice), m_startPoint(startPoint), m_selection(selection), m_useFastMode(useFastMode), m_selectionOnly(selectionOnly), + m_useSelectionAsBoundary(useSelectionAsBoundary), m_usePattern(usePattern), m_resources(resources), m_feather(feather), m_sizemod(sizemod), m_fillThreshold(fillThreshold), m_unmerged(unmerged), m_useBgColor(useBgColor) { } void FillProcessingVisitor::visitExternalLayer(KisExternalLayer *layer, KisUndoAdapter *undoAdapter) { Q_UNUSED(layer); Q_UNUSED(undoAdapter); } void FillProcessingVisitor::visitNodeWithPaintDevice(KisNode *node, KisUndoAdapter *undoAdapter) { KisPaintDeviceSP device = node->paintDevice(); Q_ASSERT(device); ProgressHelper helper(node); fillPaintDevice(device, undoAdapter, helper); } void FillProcessingVisitor::fillPaintDevice(KisPaintDeviceSP device, KisUndoAdapter *undoAdapter, ProgressHelper &helper) { QRect fillRect = m_resources->image()->bounds(); if (!device->defaultBounds()->wrapAroundMode() && !fillRect.contains(m_startPoint)) { return; } if (m_selectionOnly) { KisPaintDeviceSP filledDevice = device->createCompositionSourceDevice(); KisFillPainter fillPainter(filledDevice); fillPainter.setProgress(helper.updater()); if (m_usePattern) { fillPainter.fillRect(fillRect, m_resources->currentPattern(), m_resources->fillTransform()); } else if (m_useBgColor) { fillPainter.fillRect(fillRect, m_resources->currentBgColor(), m_resources->opacity()); } else { fillPainter.fillRect(fillRect, m_resources->currentFgColor(), m_resources->opacity()); } QVector dirtyRect = fillPainter.takeDirtyRegion(); KisPainter painter(device, m_selection); painter.beginTransaction(); m_resources->setupPainter(&painter); Q_FOREACH (const QRect &rc, dirtyRect) { painter.bitBlt(rc.topLeft(), filledDevice, rc); } painter.endTransaction(undoAdapter); } else { QPoint startPoint = m_startPoint; if (device->defaultBounds()->wrapAroundMode()) { startPoint = KisWrappedRect::ptToWrappedPt(startPoint, device->defaultBounds()->imageBorderRect()); } KisFillPainter fillPainter(device, m_selection); fillPainter.beginTransaction(); m_resources->setupPainter(&fillPainter); fillPainter.setProgress(helper.updater()); fillPainter.setSizemod(m_sizemod); fillPainter.setFeather(m_feather); fillPainter.setFillThreshold(m_fillThreshold); fillPainter.setCareForSelection(true); + fillPainter.setUseSelectionAsBoundary((m_selection.isNull() || m_selection->hasNonEmptyPixelSelection()) ? m_useSelectionAsBoundary : false); fillPainter.setWidth(fillRect.width()); fillPainter.setHeight(fillRect.height()); fillPainter.setUseCompositioning(!m_useFastMode); KisPaintDeviceSP sourceDevice = m_unmerged ? device : m_refPaintDevice; if (m_usePattern) { fillPainter.fillPattern(startPoint.x(), startPoint.y(), sourceDevice, m_resources->fillTransform()); } else { fillPainter.fillColor(startPoint.x(), startPoint.y(), sourceDevice); } fillPainter.endTransaction(undoAdapter); } } void FillProcessingVisitor::visitColorizeMask(KisColorizeMask *mask, KisUndoAdapter *undoAdapter) { // we fill only the coloring project so the user can work // with the mask like with a usual paint layer ProgressHelper helper(mask); fillPaintDevice(mask->coloringProjection(), undoAdapter, helper); } diff --git a/libs/ui/processing/fill_processing_visitor.h b/libs/ui/processing/fill_processing_visitor.h index 62f8820346..f4a7cbb575 100644 --- a/libs/ui/processing/fill_processing_visitor.h +++ b/libs/ui/processing/fill_processing_visitor.h @@ -1,70 +1,72 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __FILL_PROCESSING_VISITOR_H #define __FILL_PROCESSING_VISITOR_H #include #include #include #include #include class KRITAUI_EXPORT FillProcessingVisitor : public KisSimpleProcessingVisitor { public: FillProcessingVisitor( KisPaintDeviceSP referencePaintDevice, const QPoint &startPoint, KisSelectionSP selection, KisResourcesSnapshotSP resources, bool useFastMode, bool usePattern, bool selectionOnly, + bool useSelectionAsBoundary, int feather, int sizemod, int fillThreshold, bool unmerged, bool m_useBgColor); private: void visitNodeWithPaintDevice(KisNode *node, KisUndoAdapter *undoAdapter) override; void visitExternalLayer(KisExternalLayer *layer, KisUndoAdapter *undoAdapter) override; void visitColorizeMask(KisColorizeMask *mask, KisUndoAdapter *undoAdapter) override; void fillPaintDevice(KisPaintDeviceSP device, KisUndoAdapter *undoAdapter, ProgressHelper &helper); private: KisPaintDeviceSP m_refPaintDevice; QPoint m_startPoint; KisSelectionSP m_selection; bool m_useFastMode; bool m_selectionOnly; + bool m_useSelectionAsBoundary; bool m_usePattern; KisResourcesSnapshotSP m_resources; int m_feather; int m_sizemod; int m_fillThreshold; bool m_unmerged; bool m_useBgColor; }; #endif /* __FILL_PROCESSING_VISITOR_H */ diff --git a/libs/ui/tests/fill_processing_visitor_test.cpp b/libs/ui/tests/fill_processing_visitor_test.cpp index be1f4f5284..f6d64beccd 100644 --- a/libs/ui/tests/fill_processing_visitor_test.cpp +++ b/libs/ui/tests/fill_processing_visitor_test.cpp @@ -1,149 +1,150 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "fill_processing_visitor_test.h" #include #include "kis_undo_stores.h" #include "kis_processing_applicator.h" #include "testutil.h" #include "qimage_based_test.h" #include "stroke_testing_utils.h" #include #include "kis_canvas_resource_provider.h" #include #include class FillProcessingVisitorTester : public TestUtil::QImageBasedTest { public: FillProcessingVisitorTester() : QImageBasedTest("fill_processing") { } void test(const QString &testname, bool haveSelection, bool usePattern, bool selectionOnly) { KisSurrogateUndoStore *undoStore = new KisSurrogateUndoStore(); KisImageSP image = createImage(undoStore); if (haveSelection) { addGlobalSelection(image); } image->initialRefreshGraph(); QVERIFY(checkLayersInitial(image)); KisNodeSP fillNode = findNode(image->root(), "paint1"); KoCanvasResourceProvider *manager = utils::createResourceManager(image, fillNode); KoPatternSP newPattern(new KoPattern(TestUtil::fetchDataFileLazy("HR_SketchPaper_01.pat"))); newPattern->load(KisGlobalResourcesInterface::instance()); Q_ASSERT(newPattern->valid()); QVariant v; v.setValue(newPattern); manager->setResource(KisCanvasResourceProvider::CurrentPattern, v); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, fillNode, manager); KisProcessingVisitorSP visitor = new FillProcessingVisitor(0, QPoint(100,100), image->globalSelection(), resources, false, // useFastMode usePattern, selectionOnly, + false, 10, 10, 10, true /* use the current device (unmerged) */, false); KisProcessingApplicator applicator(image, fillNode, KisProcessingApplicator::NONE); applicator.applyVisitor(visitor); applicator.end(); image->waitForDone(); QVERIFY(checkOneLayer(image, fillNode, testname, 500)); undoStore->undo(); image->waitForDone(); QVERIFY(checkLayersInitial(image)); } }; void FillProcessingVisitorTest::testFillColorNoSelection() { FillProcessingVisitorTester tester; tester.test("fill_color_no_selection", false, false, false); } void FillProcessingVisitorTest::testFillPatternNoSelection() { FillProcessingVisitorTester tester; tester.test("fill_pattern_no_selection", false, true, false); } void FillProcessingVisitorTest::testFillColorHaveSelection() { FillProcessingVisitorTester tester; tester.test("fill_color_have_selection", true, false, false); } void FillProcessingVisitorTest::testFillPatternHaveSelection() { FillProcessingVisitorTester tester; tester.test("fill_pattern_have_selection", true, true, false); } void FillProcessingVisitorTest::testFillColorNoSelectionSelectionOnly() { FillProcessingVisitorTester tester; tester.test("fill_color_no_selection_selection_only", false, false, true); } void FillProcessingVisitorTest::testFillPatternNoSelectionSelectionOnly() { FillProcessingVisitorTester tester; tester.test("fill_pattern_no_selection_selection_only", false, true, true); } void FillProcessingVisitorTest::testFillColorHaveSelectionSelectionOnly() { FillProcessingVisitorTester tester; tester.test("fill_color_have_selection_selection_only", true, false, true); } void FillProcessingVisitorTest::testFillPatternHaveSelectionSelectionOnly() { FillProcessingVisitorTester tester; tester.test("fill_pattern_have_selection_selection_only", true, true, true); } QTEST_MAIN(FillProcessingVisitorTest) diff --git a/libs/ui/widgets/kis_color_filter_combo.cpp b/libs/ui/widgets/kis_color_filter_combo.cpp index c9d3432145..4c493df896 100644 --- a/libs/ui/widgets/kis_color_filter_combo.cpp +++ b/libs/ui/widgets/kis_color_filter_combo.cpp @@ -1,427 +1,449 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_color_filter_combo.h" #include #include #include #include #include #include #include #include #include #include #include #include "kis_node_view_color_scheme.h" #include "kis_debug.h" #include "kis_icon_utils.h" #include "krita_utils.h" #include "kis_node.h" enum AdditionalRoles { OriginalLabelIndex = Qt::UserRole + 1000 }; struct LabelFilteringModel : public QSortFilterProxyModel { LabelFilteringModel(QObject *parent) : QSortFilterProxyModel(parent) {} bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); const int labelIndex = index.data(OriginalLabelIndex).toInt(); return labelIndex < 0 || m_acceptedLabels.contains(labelIndex); } void setAcceptedLabels(const QSet &value) { m_acceptedLabels = value; invalidateFilter(); } private: QSet m_acceptedLabels; }; class ComboEventFilter : public QObject { public: ComboEventFilter(KisColorFilterCombo *parent) : m_parent(parent), m_buttonPressed(false) {} protected: bool eventFilter(QObject *obj, QEvent *event) override { if (event->type() == QEvent::Leave) { m_buttonPressed = false; } else if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *mevent = static_cast(event); m_buttonPressed = mevent->button() == Qt::LeftButton; } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent *mevent = static_cast(event); QModelIndex index = m_parent->view()->indexAt(mevent->pos()); if (!index.isValid()) return false; /** * We should eat the first event that arrives exactly when * the drop down appears on screen. */ if (!m_buttonPressed) return true; const bool toUncheckedState = index.data(Qt::CheckStateRole) == Qt::Checked; if (toUncheckedState) { m_parent->model()->setData(index, Qt::Unchecked, Qt::CheckStateRole); } else { m_parent->model()->setData(index, Qt::Checked, Qt::CheckStateRole); } if (index.data(OriginalLabelIndex).toInt() == -1) { for (int i = 0; i < m_parent->model()->rowCount(); i++) { const QModelIndex &other = m_parent->model()->index(i, 0); if (other.data(OriginalLabelIndex) != -1) { m_parent->model()->setData(other, toUncheckedState ? Qt::Unchecked : Qt::Checked, Qt::CheckStateRole); } } } else { bool prevChecked = false; bool checkedVaries = false; QModelIndex allLabelsIndex; for (int i = 0; i < m_parent->model()->rowCount(); i++) { const QModelIndex &other = m_parent->model()->index(i, 0); if (other.data(OriginalLabelIndex) != -1) { const bool currentChecked = other.data(Qt::CheckStateRole) == Qt::Checked; if (i == 0) { prevChecked = currentChecked; } else { if (prevChecked != currentChecked) { checkedVaries = true; break; } } } else { allLabelsIndex = other; } } const bool allLabelsIndexShouldBeChecked = prevChecked && !checkedVaries; if (allLabelsIndexShouldBeChecked != (allLabelsIndex.data(Qt::CheckStateRole) == Qt::Checked)) { m_parent->model()->setData(allLabelsIndex, allLabelsIndexShouldBeChecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole); } } emit m_parent->selectedColorsChanged(); m_buttonPressed = false; return true; } return QObject::eventFilter(obj, event); } private: KisColorFilterCombo *m_parent; bool m_buttonPressed; }; class FullSizedListView : public QListView { public: QSize sizeHint() const override { return contentsSize(); } }; class PopupComboBoxStyle : public QProxyStyle { public: PopupComboBoxStyle(QStyle *baseStyle = nullptr) : QProxyStyle(baseStyle) {} int styleHint(QStyle::StyleHint hint, const QStyleOption *option, const QWidget *widget, QStyleHintReturn *returnData) const override { // This flag makes ComboBox popup float ontop of its parent ComboBox, like in Fusion style. // Only when this hint is set will Qt respect combobox popup size hints, otherwise the popup // can never exceed the width of its parent ComboBox, like in Breeze style. if (hint == QStyle::SH_ComboBox_Popup) { return true; } return QProxyStyle::styleHint(hint, option, widget, returnData); } }; struct KisColorFilterCombo::Private { LabelFilteringModel *filteringModel; /** * if the combobox is in the filter mode * (when no colors are selected) * it will show the filter icon ("view-filter") * otherwise it will show tag icon ("tag") */ bool filterMode {true}; /** * If the combobox is in the circle mode, * it will show the selected colors as circle * otherwise it will show it in a rectangle */ bool circleMode {true}; }; KisColorFilterCombo::KisColorFilterCombo(QWidget *parent, bool filterMode, bool circleMode) : QComboBox(parent), m_d(new Private) { m_d->filterMode = filterMode; m_d->circleMode = circleMode; QStandardItemModel *newModel = new QStandardItemModel(this); setModel(newModel); QStyle* newStyle = QStyleFactory::create(style()->objectName()); // proxy style steals the ownership of the style and deletes it later PopupComboBoxStyle *proxyStyle = new PopupComboBoxStyle(newStyle); proxyStyle->setParent(this); setStyle(proxyStyle); setView(new FullSizedListView); m_eventFilters.append(new ComboEventFilter(this)); m_eventFilters.append(new ComboEventFilter(this)); view()->installEventFilter(m_eventFilters[0]); view()->viewport()->installEventFilter(m_eventFilters[1]); KisNodeViewColorScheme scm; QStandardItem* item = new QStandardItem(i18nc("combo box: show all layers", "All")); item->setCheckable(true); item->setCheckState(Qt::Unchecked); item->setData(QColor(Qt::transparent), Qt::BackgroundColorRole); item->setData(int(-1), OriginalLabelIndex); item->setData(QSize(30, scm.rowHeight()), Qt::SizeHintRole); newModel->appendRow(item); int labelIndex = 0; foreach (const QColor &color, scm.allColorLabels()) { const QString title = color.alpha() > 0 ? "" : i18nc("combo box: select all layers without a label", "No Label"); QStandardItem* item = new QStandardItem(title); item->setCheckable(true); item->setCheckState(Qt::Unchecked); item->setData(color, Qt::BackgroundColorRole); item->setData(labelIndex, OriginalLabelIndex); item->setData(QSize(30, scm.rowHeight()), Qt::SizeHintRole); newModel->appendRow(item); labelIndex++; } m_d->filteringModel = new LabelFilteringModel(this); QAbstractItemModel *originalModel = model(); originalModel->setParent(m_d->filteringModel); m_d->filteringModel->setSourceModel(originalModel); setModel(m_d->filteringModel); } KisColorFilterCombo::~KisColorFilterCombo() { qDeleteAll(m_eventFilters); } void collectAvailableLabels(KisNodeSP root, QSet *labels) { labels->insert(root->colorLabelIndex()); KisNodeSP node = root->firstChild(); while (node) { collectAvailableLabels(node, labels); node = node->nextSibling(); } } void KisColorFilterCombo::updateAvailableLabels(KisNodeSP rootNode) { QSet labels; if (!rootNode.isNull()) { collectAvailableLabels(rootNode, &labels); } updateAvailableLabels(labels); } void KisColorFilterCombo::updateAvailableLabels(const QSet &labels) { m_d->filteringModel->setAcceptedLabels(labels); } void KisColorFilterCombo::setModes(bool filterMode, bool circleMode) { m_d->filterMode = filterMode; m_d->circleMode = circleMode; } QList KisColorFilterCombo::selectedColors() const { QList colors; for (int i = 0; i < model()->rowCount(); i++) { const QModelIndex &other = model()->index(i, 0); const int label = other.data(OriginalLabelIndex).toInt(); if (label != -1 && other.data(Qt::CheckStateRole) == Qt::Checked) { colors << label; } } return colors; } +void KisColorFilterCombo::paintColorPie(QStylePainter &painter, const QPalette& palette, const QList &selectedColors, const QRect &rect, const int &baseSize) +{ + KisNodeViewColorScheme scm; + const QPen oldPen = painter.pen(); + const QBrush oldBrush = painter.brush(); + const int border = 0; + QColor shadowColor = palette.shadow().color(); + shadowColor.setAlpha(64); + + QRect pieRect(0, 0, baseSize - 2 * border, baseSize - 2 * border); + pieRect.moveCenter(rect.center()); + + if (selectedColors.size() == 1) { + const int currentLabel = selectedColors.first(); + const QColor currentColor = scm.colorLabel(currentLabel); + const QBrush brush = QBrush(currentColor); + painter.setBrush(brush); + painter.setPen(QPen(shadowColor, 1)); + + if (currentColor.alpha() > 0) { + painter.drawEllipse(rect); + } else if (currentLabel == 0) { + QColor white = Qt::white; + QColor grey = QColor(220,220,220); + painter.setBrush(QBrush(shadowColor)); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawEllipse(rect); + const int step = 16 * 360 / 4; + const int checkerSteps = 4; + + for (int i = 0; i < checkerSteps; i++) { + QBrush checkerBrush = QBrush((i % 2) ? grey : white); + painter.setPen(Qt::NoPen); + painter.setBrush(checkerBrush); + painter.drawPie(pieRect, step * i, step); + } + + } + } else { + const int numColors = selectedColors.size(); + const int step = 16 * 360 / numColors; + + painter.setPen(QPen(shadowColor, 1)); + painter.setBrush(QColor(0,0,0,0)); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawEllipse(rect); + for (int i = 0; i < numColors; i++) { + QColor color = scm.colorLabel(selectedColors[i]); + QBrush brush = color.alpha() > 0 ? QBrush(color) : QBrush(Qt::black, Qt::Dense4Pattern); + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + + painter.drawPie(pieRect, step * i, step); + } + } + + painter.setPen(oldPen); + painter.setBrush(oldBrush); +} + + void KisColorFilterCombo::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QStylePainter painter(this); painter.setPen(palette().color(QPalette::Text)); // draw the combobox frame, focusrect and selected etc. QStyleOptionComboBox opt; initStyleOption(&opt); painter.drawComplexControl(QStyle::CC_ComboBox, opt); + { - KisNodeViewColorScheme scm; const QRect editRect = style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this); const int size = qMin(editRect.width(), editRect.height()); const QList selectedColors = this->selectedColors(); if (selectedColors.size() == 0 || selectedColors.size() == model()->rowCount() - 1) { QIcon icon = KisIconUtils::loadIcon(m_d->filterMode ? "view-filter" : "tag"); QPixmap pixmap = icon.pixmap(QSize(size, size), !isEnabled() ? QIcon::Disabled : QIcon::Normal); painter.drawPixmap(editRect.right() - size, editRect.top(), pixmap); - } else if (selectedColors.size() == 1) { - const int currentLabel = selectedColors.first(); - QColor currentColor = scm.colorLabel(currentLabel); - - if (currentColor.alpha() > 0) { - painter.fillRect(editRect, currentColor); - } else if (currentLabel == 0) { - QPen oldPen = painter.pen(); - - const int border = 4; - QRect crossRect(0, 0, size - 2 * border, size - 2 * border); - crossRect.moveCenter(editRect.center()); - - QColor shade = opt.palette.dark().color(); - painter.setPen(QPen(shade, 2)); - painter.drawLine(crossRect.topLeft(), crossRect.bottomRight()); - painter.drawLine(crossRect.bottomLeft(), crossRect.topRight()); - } } else { const int numColors = selectedColors.size(); if (m_d->circleMode) { - // show all colors in a circle - - const int border = 0; - QRect pieRect(0, 0, size - 2 * border, size - 2 * border); - pieRect.moveCenter(editRect.center()); - - const int step = 16 * 360 / numColors; - - int currentAngle = 0; - - //painter.save(); // optimize out! - painter.setRenderHint(QPainter::Antialiasing); - - for (int i = 0; i < numColors; i++) { - QColor color = scm.colorLabel(selectedColors[i]); - QBrush brush = color.alpha() > 0 ? QBrush(color) : QBrush(Qt::black, Qt::Dense4Pattern); - painter.setPen(color); - painter.setBrush(brush); - - painter.drawPie(pieRect, currentAngle, step); - currentAngle += step; - } + KisColorFilterCombo::paintColorPie(painter, opt.palette, selectedColors, editRect, size ); } else { // show all colors in a rectangle + KisNodeViewColorScheme scm; int oneColorWidth = editRect.width()/numColors; int currentWidth = 0; for (int i = 0; i < numColors; i++) { QColor color = scm.colorLabel(selectedColors[i]); QBrush brush = color.alpha() > 0 ? QBrush(color) : QBrush(Qt::black, Qt::Dense4Pattern); painter.setPen(color); painter.setBrush(brush); if (i == numColors - 1) { // last color; let's fill up painter.fillRect(currentWidth, editRect.top(), editRect.width() - currentWidth, editRect.height(), brush); } else { painter.fillRect(currentWidth, editRect.top(), oneColorWidth, editRect.height(), brush); } currentWidth += oneColorWidth; } } - //painter.restore(); // optimize out! } } // draw the icon and text //painter.drawControl(QStyle::CE_ComboBoxLabel, opt); } QSize KisColorFilterCombo::minimumSizeHint() const { return sizeHint(); } QSize KisColorFilterCombo::sizeHint() const { QStyleOptionComboBox opt; initStyleOption(&opt); const QStyleOption *baseOption = qstyleoption_cast(&opt); const int arrowSize = style()->pixelMetric(QStyle::PM_ScrollBarExtent, baseOption, this); const QSize originalHint = QComboBox::sizeHint(); QSize sh(3 * arrowSize, originalHint.height()); return sh; } diff --git a/libs/ui/widgets/kis_color_filter_combo.h b/libs/ui/widgets/kis_color_filter_combo.h index c145a498ca..8503ec8f0a 100644 --- a/libs/ui/widgets/kis_color_filter_combo.h +++ b/libs/ui/widgets/kis_color_filter_combo.h @@ -1,57 +1,61 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_COLOR_FILTER_COMBO_H #define __KIS_COLOR_FILTER_COMBO_H #include #include +#include #include "kritaui_export.h" #include "kis_types.h" class ComboEventFilter; class KRITAUI_EXPORT KisColorFilterCombo : public QComboBox { Q_OBJECT public: KisColorFilterCombo(QWidget *parent, bool filterMode = true, bool circleMode = true); ~KisColorFilterCombo() override; void updateAvailableLabels(KisNodeSP rootNode); void updateAvailableLabels(const QSet &labels); void setModes(bool filterMode, bool circleMode); QSize minimumSizeHint() const override; QSize sizeHint() const override; QList selectedColors() const; Q_SIGNALS: void selectedColorsChanged(); +public: + static void paintColorPie(QStylePainter &painter, const QPalette& palette, const QList &selectedColors, const QRect &rect, const int &baseSize); + private: void paintEvent(QPaintEvent *event) override; private: struct Private; const QScopedPointer m_d; QList m_eventFilters; }; #endif /* __KIS_COLOR_FILTER_COMBO_H */ diff --git a/libs/ui/widgets/kis_color_label_button.cpp b/libs/ui/widgets/kis_color_label_button.cpp new file mode 100644 index 0000000000..f0d8268ad1 --- /dev/null +++ b/libs/ui/widgets/kis_color_label_button.cpp @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "kis_color_label_button.h" + +#include +#include +#include +#include + + +#include "kis_global.h" +#include "kis_debug.h" +#include "krita_container_utils.h" + +struct KisColorLabelButton::Private +{ + const QColor m_color; + const uint m_sizeSquared; + KisColorLabelButton::SelectionIndicationType selectionVis; + + Private(QColor color, uint sizeSquared) + : m_color(color) + , m_sizeSquared(sizeSquared) + , selectionVis(KisColorLabelButton::FillIn) + { + + } + + Private(const Private& rhs) + : m_color(rhs.m_color) + , m_sizeSquared(rhs.m_sizeSquared) + { + + } +}; + +KisColorLabelButton::KisColorLabelButton(QColor color, uint sizeSquared, QWidget *parent) : QAbstractButton(parent), m_d(new Private(color, sizeSquared)) +{ + setCheckable(true); + setChecked(true); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); +} + +KisColorLabelButton::~KisColorLabelButton() {} + +void KisColorLabelButton::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + QStylePainter painter(this); + QStyleOptionButton styleOption; + styleOption.initFrom(this); + + const bool darkTheme = styleOption.palette.window().color().value() < 128; + + if (isDown() || isChecked()){ + styleOption.state |= QStyle::State_On; + } + + QRect fillRect = kisGrowRect(rect(), -2); + QRect outlineRect = kisGrowRect(fillRect, -1); + + const QColor shadowColor = styleOption.palette.window().color().darker(darkTheme ? 128 : 200); + const QBrush shadowBrush = QBrush(shadowColor); + const QBrush bgBrush = QBrush(styleOption.palette.window().color()); + + if (!isEnabled()) { + fillRect -= QMargins(sizeHint().width() / 4, sizeHint().height() / 4, sizeHint().width() / 4, sizeHint().height() / 4); + } else { + fillRect = kisGrowRect(fillRect, -3); + } + + if (m_d->m_color.alpha() > 0) { + QColor fillColor = m_d->m_color; + + if ((!isChecked() || !isEnabled()) && (m_d->selectionVis == FillIn)) { + fillColor.setAlpha(32); + } else if ((!isChecked() || !isEnabled()) && (m_d->selectionVis == Outline)) { + if ((styleOption.state & QStyle::State_MouseOver) == 0) { + fillColor.setAlpha(192); + } + } + + if ((isEnabled() && isChecked() && m_d->selectionVis == FillIn) || + m_d->selectionVis == Outline) { + painter.fillRect(kisGrowRect(fillRect, 1), shadowBrush); + painter.fillRect(fillRect, bgBrush); + } + + QBrush brush = QBrush(fillColor); + painter.fillRect(fillRect, brush); + + if ((isEnabled() && (m_d->selectionVis == FillIn)) || + (isChecked() && (m_d->selectionVis == Outline))) { + const QRect& shadowRect = outlineRect; + painter.setPen(QPen(shadowColor, 4)); + painter.drawRect(shadowRect); + + painter.setPen(QPen(bgBrush.color(), 2)); + painter.drawRect(outlineRect); + + painter.setPen(QPen(m_d->m_color, 2)); + painter.drawRect(outlineRect); + } + + } else { + QColor white = QColor(255,255,255); + QColor grey = QColor(200,200,200); + QColor xOverlayColor = QColor(100,100,100); + QColor outlineColor = grey; + + if ((!isChecked() || !isEnabled()) && (m_d->selectionVis == FillIn)) { + white.setAlpha(32); + grey.setAlpha(32); + + if (darkTheme) { + xOverlayColor = styleOption.palette.window().color().lighter(130); + } else { + xOverlayColor = xOverlayColor.lighter(190); + } + + } else if ((!isChecked() || !isEnabled()) && (m_d->selectionVis == Outline)) { + if ((styleOption.state & QStyle::State_MouseOver) == 0) { + white.setAlpha(192); + xOverlayColor.setAlpha(192); + if (darkTheme) { + grey = grey.darker(110); + } else { + grey = grey.lighter(110); + } + grey.setAlpha(192); + } + } + + QBrush whiteBrush = QBrush(white); + QBrush greyBrush = QBrush(grey); + + QRect upperLeftGrey = fillRect - QMargins(0, 0, fillRect.size().width() / 2, fillRect.size().height() /2); + QRect lowerRightGrey = fillRect - QMargins(fillRect.size().width() / 2, fillRect.size().height() / 2, 0, 0); + QRect xOverlay = kisGrowRect(fillRect, (m_d->m_sizeSquared / 8) * -1); + + if (isEnabled() && ((isChecked() && m_d->selectionVis == FillIn) || + m_d->selectionVis == Outline)) { + painter.fillRect(kisGrowRect(fillRect, 1), shadowBrush); + painter.fillRect(fillRect, bgBrush); + } + + painter.fillRect(fillRect, whiteBrush); + + painter.fillRect(upperLeftGrey, greyBrush); + painter.fillRect(lowerRightGrey, greyBrush); + + painter.setPen(QPen(xOverlayColor, 2)); + painter.drawLine(xOverlay.topLeft(), xOverlay.bottomRight()); + painter.drawLine(xOverlay.bottomLeft(), xOverlay.topRight()); + + if ((isEnabled() && (m_d->selectionVis == FillIn)) || + (isChecked() && (m_d->selectionVis == Outline))) { + const QRect& shadowRect = outlineRect; + painter.setPen(QPen(shadowColor, 4)); + painter.drawRect(shadowRect); + + painter.setPen(QPen(bgBrush.color(), 2)); + painter.drawRect(outlineRect); + + painter.setPen(QPen(outlineColor, 2)); + painter.drawRect(outlineRect); + } + + } +} + +void KisColorLabelButton::enterEvent(QEvent *event) { + Q_UNUSED(event); + update(); +} + +void KisColorLabelButton::leaveEvent(QEvent *event) { + Q_UNUSED(event); + update(); +} + +QSize KisColorLabelButton::sizeHint() const +{ + return QSize(m_d->m_sizeSquared,m_d->m_sizeSquared); +} + +void KisColorLabelButton::setSelectionVisType(KisColorLabelButton::SelectionIndicationType type) +{ + m_d->selectionVis = type; +} + +void KisColorLabelButton::nextCheckState() +{ + KisColorLabelFilterGroup* colorLabelFilterGroup = dynamic_cast(group()); + + if (!colorLabelFilterGroup || (colorLabelFilterGroup->countCheckedViableButtons() > colorLabelFilterGroup->minimumRequiredChecked() || !isChecked())) { + setChecked(!isChecked()); + } else { + setChecked(isChecked()); + } +} + +KisColorLabelFilterGroup::KisColorLabelFilterGroup(QObject *parent) + : QButtonGroup(parent) + , minimumCheckedButtons(1) +{ +} + +KisColorLabelFilterGroup::~KisColorLabelFilterGroup() +{ +} + +QList KisColorLabelFilterGroup::viableButtons() const { + QList viableButtons; + + Q_FOREACH( int index, viableColorLabels ) { + viableButtons.append(button(index)); + } + + return viableButtons; +} + +void KisColorLabelFilterGroup::setViableLabels(const QSet &labels) { + setAllVisibility(false); + disableAll(); + QSet removed = viableColorLabels.subtract(labels); + + viableColorLabels = labels; + + if (viableColorLabels.count() > 1) { + setAllVisibility(true); + Q_FOREACH( int index, viableColorLabels) { + if (button(index)) { + button(index)->setEnabled(true); + } + } + } + + Q_FOREACH( int index, removed ) { + button(index)->setChecked(true); + } +} + +void KisColorLabelFilterGroup::setViableLabels(const QList &viableLabels) { + setViableLabels(QSet::fromList(viableLabels)); +} + +QSet KisColorLabelFilterGroup::getActiveLabels() const { + QSet checkedLabels = QSet(); + + Q_FOREACH( int index, viableColorLabels ) { + if (button(index)->isChecked()) { + checkedLabels.insert(index); + } + } + + return checkedLabels.count() == viableColorLabels.count() ? QSet() : checkedLabels; +} + +QList KisColorLabelFilterGroup::checkedViableButtons() const { + QList checkedButtons = viableButtons(); + + KritaUtils::filterContainer(checkedButtons, [](QAbstractButton* btn){ + return (btn->isChecked()); + }); + + return checkedButtons; +} + +int KisColorLabelFilterGroup::countCheckedViableButtons() const { + return checkedViableButtons().count(); +} + +int KisColorLabelFilterGroup::countViableButtons() const { + return viableColorLabels.count(); +} + +void KisColorLabelFilterGroup::setMinimumRequiredChecked(int checkedBtns) +{ + minimumCheckedButtons = checkedBtns; +} + +int KisColorLabelFilterGroup::minimumRequiredChecked() +{ + return minimumCheckedButtons; +} + +void KisColorLabelFilterGroup::reset() { + Q_FOREACH( QAbstractButton* btn, viableButtons() ) { + btn->setChecked(true); + } +} + +void KisColorLabelFilterGroup::disableAll() { + Q_FOREACH( QAbstractButton* btn, buttons() ) { + btn->setDisabled(true); + } +} + +void KisColorLabelFilterGroup::setAllVisibility(const bool vis) +{ + Q_FOREACH( QAbstractButton* btn, buttons() ) { + btn->setVisible(vis); + } +} + +KisColorLabelMouseDragFilter::KisColorLabelMouseDragFilter(QObject* parent) : QObject(parent) +{ + lastKnownMousePosition = QPoint(0,0); + currentState = Idle; +} + +bool KisColorLabelMouseDragFilter::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) { + QMouseEvent* mouseEvent = static_cast(event); + + currentState = WaitingForDragLeave; + lastKnownMousePosition = mouseEvent->globalPos(); + + return true; + + } else if (event->type() == QEvent::MouseButtonRelease) { + QMouseEvent* mouseEvent = static_cast(event); + QAbstractButton* startingButton = static_cast(obj); + + //If we never left, toggle the original button. + if( currentState == WaitingForDragLeave ) { + if ( startingButton->group() && (mouseEvent->modifiers() & Qt::SHIFT)) { + KisColorLabelFilterGroup* const group = static_cast(startingButton->group()); + const QList viableCheckedButtons = group->checkedViableButtons(); + + const int buttonsEnabled = viableCheckedButtons.count(); + const bool shouldChangeIsolation = (buttonsEnabled == 1) && (viableCheckedButtons.first() == startingButton); + const bool shouldIsolate = (buttonsEnabled != 1) || !shouldChangeIsolation; + + Q_FOREACH(QAbstractButton* otherBtn, group->viableButtons()) { + if (otherBtn == startingButton){ + startingButton->setChecked(true); + } else { + otherBtn->setChecked(!shouldIsolate); + } + } + + } else { + startingButton->click(); + } + } + + currentState = Idle; + lastKnownMousePosition = mouseEvent->globalPos(); + + return true; + + } else if (event->type() == QEvent::MouseMove) { + + if (currentState == WaitingForDragLeave) { + QMouseEvent* mouseEvent = static_cast(event); + QWidget* firstClicked = static_cast(obj); + const QPointF localPosition = mouseEvent->localPos(); + + if (!firstClicked->rect().contains(localPosition.x(), localPosition.y())) { + QAbstractButton* btn = static_cast(obj); + btn->click(); + + checkSlideOverNeighborButtons(mouseEvent, btn); + + currentState = WaitingForDragEnter; + } + + lastKnownMousePosition = mouseEvent->globalPos(); + + return true; + + } else if (currentState == WaitingForDragEnter) { + QMouseEvent* mouseEvent = static_cast(event); + QAbstractButton* startingButton = static_cast(obj); + const QPoint currentPosition = mouseEvent->globalPos(); + + checkSlideOverNeighborButtons(mouseEvent, startingButton); + + lastKnownMousePosition = currentPosition; + + return true; + } + + } + + return false; +} + +void KisColorLabelMouseDragFilter::checkSlideOverNeighborButtons(QMouseEvent* mouseEvent, QAbstractButton* startingButton) +{ + const QPoint currentPosition = mouseEvent->globalPos(); + + if (startingButton->group()) { + QList allButtons = startingButton->group()->buttons(); + + Q_FOREACH(QAbstractButton* button, allButtons) { + const QRect bounds = QRect(button->mapToGlobal(QPoint(0,0)), button->size()); + const QPoint upperLeft = QPoint(qMin(lastKnownMousePosition.x(), currentPosition.x()), qMin(lastKnownMousePosition.y(), currentPosition.y())); + const QPoint lowerRight = QPoint(qMax(lastKnownMousePosition.x(), currentPosition.x()), qMax(lastKnownMousePosition.y(), currentPosition.y())); + const QRect mouseMovement = QRect(upperLeft, lowerRight); + if( bounds.intersects(mouseMovement) && !bounds.contains(lastKnownMousePosition)) { + button->click(); + } + } + } +} diff --git a/libs/ui/widgets/kis_color_label_button.h b/libs/ui/widgets/kis_color_label_button.h new file mode 100644 index 0000000000..b588587f8a --- /dev/null +++ b/libs/ui/widgets/kis_color_label_button.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef KISCOLORLABELBUTTON_H +#define KISCOLORLABELBUTTON_H + +#include +#include +#include + +#include "kritaui_export.h" + +class KRITAUI_EXPORT KisColorLabelButton : public QAbstractButton +{ + Q_OBJECT +public: + enum SelectionIndicationType { + FillIn, + Outline + }; + + KisColorLabelButton(QColor color, uint sizeSquared = 32, QWidget *parent = nullptr); + ~KisColorLabelButton(); + + void paintEvent(QPaintEvent* event) override; + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; + QSize sizeHint() const override; + void setSelectionVisType( SelectionIndicationType type ); + + virtual void nextCheckState() override; + +private: + struct Private; + const QScopedPointer m_d; +}; + + +class KRITAUI_EXPORT KisColorLabelFilterGroup : public QButtonGroup { + Q_OBJECT +public: + KisColorLabelFilterGroup(QObject* parent); + ~KisColorLabelFilterGroup(); + + QList viableButtons() const; + void setViableLabels(const QSet &buttons); + void setViableLabels(const QList &viableLabels); + QSet getActiveLabels() const; + + QList checkedViableButtons() const; + int countCheckedViableButtons() const; + int countViableButtons() const; + + void setMinimumRequiredChecked( int checkedBtns ); + int minimumRequiredChecked(); + +public Q_SLOTS: + void reset(); + void setAllVisibility(const bool vis); + +private: + void disableAll(); + QSet viableColorLabels; + int minimumCheckedButtons; + +}; + +class KRITAUI_EXPORT KisColorLabelMouseDragFilter : public QObject { + enum State{ + Idle, + WaitingForDragLeave, //Waiting for mouse to exit first clicked while the mouse button is down. + WaitingForDragEnter //Waiting for mouse to slide across buttons within the same button group. + }; + + State currentState; + QPoint lastKnownMousePosition; + +public: + KisColorLabelMouseDragFilter(QObject *parent = nullptr); + +protected: + bool eventFilter(QObject *obj, QEvent *event); + void checkSlideOverNeighborButtons(QMouseEvent* mouseEvent, class QAbstractButton* startingButton); +}; + + +#endif // KISCOLORLABELBUTTON_H diff --git a/libs/ui/widgets/kis_color_label_selector_widget.cpp b/libs/ui/widgets/kis_color_label_selector_widget.cpp index c27f60783d..ee2225ad70 100644 --- a/libs/ui/widgets/kis_color_label_selector_widget.cpp +++ b/libs/ui/widgets/kis_color_label_selector_widget.cpp @@ -1,340 +1,169 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_color_label_selector_widget.h" #include "kis_debug.h" #include "kis_global.h" #include #include #include +#include -#include #include #include -#include -#include - -#include -#include +#include +#include +#include "kis_color_label_button.h" #include "kis_node_view_color_scheme.h" -struct KisColorLabelSelectorWidget::Private +struct Private { Private(KisColorLabelSelectorWidget *_q) - : q(_q), - xMenuOffset(0), - yCenteringOffset(0), - realItemSize(0), - realItemSpacing(0), - hoveringItem(-1), - selectedItem(0) + : q(_q) + , buttonSize(26) { } KisColorLabelSelectorWidget *q; QVector colors; - - - const int minHeight = 12 + 4; - const int minSpacing = 1; - const int maxSpacing = 3; - const int border = 2; - - int xMenuOffset; - int yCenteringOffset; - int realItemSize; - int realItemSpacing; - - int hoveringItem; - int selectedItem; - - QRect itemRect(int index) const; - int indexFromPos(const QPoint &pos); - void updateItem(int index); - - int widthForHeight(int height, int spacing) const; - int heightForWidth(int width, int spacing) const; - void updateItemSizes(const QSize &widgetSize); + QButtonGroup* colorButtonGroup; + QSpacerItem* menuAlignmentOffset; + const int buttonSize; }; KisColorLabelSelectorWidget::KisColorLabelSelectorWidget(QWidget *parent) - : QWidget(parent), - m_d(new Private(this)) + : QWidget(parent) + , m_d(new Private(this)) { KisNodeViewColorScheme scm; m_d->colors = scm.allColorLabels(); - setMouseTracking(true); + + QHBoxLayout *layout = new QHBoxLayout(this); + + this->setLayout(layout); + layout->setContentsMargins(0,0,0,0); + layout->setSpacing(0); + layout->setAlignment(Qt::AlignLeft); + m_d->menuAlignmentOffset = new QSpacerItem(0,0); + layout->addItem(m_d->menuAlignmentOffset); + + { + m_d->colorButtonGroup = new QButtonGroup(this); + m_d->colorButtonGroup->setExclusive(true); + + for (int id = 0; id < m_d->colors.count(); id++) { + KisColorLabelButton* btn = new KisColorLabelButton(m_d->colors[id], m_d->buttonSize, this); + btn->setChecked(false); + btn->setSelectionVisType(KisColorLabelButton::Outline); + m_d->colorButtonGroup->addButton(btn, id); + layout->addWidget(btn); + } + + connect(m_d->colorButtonGroup, SIGNAL(buttonToggled(int,bool)), this, SLOT(groupButtonChecked(int,bool))); + } } -KisColorLabelSelectorWidget::~KisColorLabelSelectorWidget() -{ +KisColorLabelSelectorWidget::~KisColorLabelSelectorWidget(){ } int KisColorLabelSelectorWidget::currentIndex() const { - return m_d->selectedItem; + return m_d->colorButtonGroup->checkedId(); } -void KisColorLabelSelectorWidget::setCurrentIndex(int index) +QSize KisColorLabelSelectorWidget::sizeHint() const { - if (index == m_d->selectedItem) return; + return QSize(calculateMenuOffset() + m_d->buttonSize * m_d->colors.count(), m_d->buttonSize); +} - const int oldItem = m_d->selectedItem; - m_d->selectedItem = index; - m_d->updateItem(oldItem); - m_d->updateItem(m_d->selectedItem); - m_d->hoveringItem = index; +void KisColorLabelSelectorWidget::resizeEvent(QResizeEvent *e) { + int menuOffset = calculateMenuOffset(); - emit currentIndexChanged(m_d->selectedItem); -} + m_d->menuAlignmentOffset->changeSize(menuOffset, height()); + layout()->invalidate(); -QSize KisColorLabelSelectorWidget::minimumSizeHint() const -{ - return QSize(m_d->widthForHeight(m_d->minHeight, m_d->minSpacing), m_d->minHeight); -} + QMenu *menu = qobject_cast(parent()); -QSize KisColorLabelSelectorWidget::sizeHint() const -{ - const int preferredHeight = 22 + 2 * m_d->border; - return QSize(m_d->widthForHeight(preferredHeight, m_d->maxSpacing), preferredHeight); + if(menu) { + menu->resize(menu->width() + menuOffset, menu->height()); + } + + QWidget::resizeEvent(e); } -void KisColorLabelSelectorWidget::resizeEvent(QResizeEvent *e) +int KisColorLabelSelectorWidget::calculateMenuOffset() const { - m_d->xMenuOffset = 0; - bool hasWideItems = false; QMenu *menu = qobject_cast(parent()); + bool hasCheckable = false; + bool hasIcon = false; + + int menuOffset = 0; + if (menu) { Q_FOREACH(QAction *action, menu->actions()) { - if (action->isCheckable() || - !action->icon().isNull()) { + hasCheckable |= action->isCheckable(); + hasIcon |= action->icon().isNull(); + hasWideItems |= (hasCheckable || hasIcon); - hasWideItems = true; + if (hasWideItems) { break; } } } if (hasWideItems) { QStyleOption opt; opt.init(this); // some copy-pasted code from QFusionStyle style - const int hmargin = style()->pixelMetric(QStyle::PM_MenuHMargin, &opt, this); - const int icone = style()->pixelMetric(QStyle::PM_SmallIconSize, &opt, this); - m_d->xMenuOffset = hmargin + icone + 6; - } - - m_d->updateItemSizes(e->size()); - QWidget::resizeEvent(e); -} - -int KisColorLabelSelectorWidget::Private::widthForHeight(int height, int spacing) const -{ - return height * colors.size() + spacing * (colors.size() - 1) + 2 * border + xMenuOffset; -} - -int KisColorLabelSelectorWidget::Private::heightForWidth(int width, int spacing) const -{ - const int numItems = colors.size(); - return qRound(qreal(width - spacing * (numItems - 1) - 2 * border - xMenuOffset) / numItems); -} - -void KisColorLabelSelectorWidget::Private::updateItemSizes(const QSize &widgetSize) -{ - const int height = qBound(minHeight, - heightForWidth(widgetSize.width(), minSpacing), - widgetSize.height()); - - const int size = height - 2 * border; - const int numItems = colors.size(); - - const int rest = widgetSize.width() - size * numItems - 2 * border - xMenuOffset; - const int spacing = qBound(minSpacing, - rest / (numItems - 1), - maxSpacing); - - realItemSize = size; - realItemSpacing = spacing; - yCenteringOffset = qMax(0, (widgetSize.height() - height) / 2); -} - -QRect KisColorLabelSelectorWidget::Private::itemRect(int index) const -{ - const int x = xMenuOffset + border + index * realItemSize + index * realItemSpacing; - const int y = border + yCenteringOffset; - - return QRect(x, y, realItemSize, realItemSize); -} - -int KisColorLabelSelectorWidget::Private::indexFromPos(const QPoint &pos) -{ - const int x = pos.x() - border - xMenuOffset; - const int y = pos.y() - border - yCenteringOffset; - if (y < 0 || y >= realItemSize || x < 0) return -1; - int idx = (x + realItemSpacing) / (realItemSize + realItemSpacing); - - if (idx < 0 || idx >= colors.size()) { - idx = -1; + const int hMargin = style()->pixelMetric(QStyle::PM_MenuHMargin, &opt, this); + const int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, &opt, this); + menuOffset = (hMargin + iconSize + 6); } - return idx; -} - -void KisColorLabelSelectorWidget::Private::updateItem(int index) -{ - if (index >= 0 && index < colors.size()) { - q->update(kisGrowRect(itemRect(index), border)); - } + return menuOffset; } -enum State { - NORMAL = 0, - HOVER, - CHECKED, - DISABLED -}; - -void drawToolButton(QWidget *widget, const QRect &rc, State state, const QColor &color, int border) +void KisColorLabelSelectorWidget::groupButtonChecked(int index, bool state) { - QStylePainter p(widget); - QStyleOption opt; - opt.initFrom(widget); - opt.rect = kisGrowRect(rc, border); - - switch (state) { - case DISABLED: - case NORMAL: - opt.state &= ~QStyle::State_Raised; - break; - case HOVER: - opt.state |= QStyle::State_Raised; - break; - case CHECKED: - opt.state |= QStyle::State_On; - break; - }; - - - if (opt.state & (QStyle::State_Sunken | QStyle::State_On | QStyle::State_Raised)) { - - const QRect borderRect = kisGrowRect(rc, 1); - p.setPen(QPen(opt.palette.text().color(), 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); - p.drawRect(borderRect); - } - - const int offset = qMax(1, rc.height() / 10); - const QRect colorBlobRect = kisGrowRect(rc, -offset); - if (color.alpha() > 0) { - QColor fillColor = color; - - if (state == DISABLED) { - fillColor.setHsl(0, 0, color.lightness()); - } - - p.fillRect(colorBlobRect, fillColor); - } else { - - // draw an X for no color for the first item - QRect crossRect = kisGrowRect(colorBlobRect, -offset); - - QColor shade = opt.palette.text().color(); - p.setPen(QPen(shade, 2)); - p.drawLine(crossRect.topLeft(), crossRect.bottomRight()); - p.drawLine(crossRect.bottomLeft(), crossRect.topRight()); + if (state == true) { + emit currentIndexChanged(index); } } -void KisColorLabelSelectorWidget::paintEvent(QPaintEvent *e) +void KisColorLabelSelectorWidget::setCurrentIndex(int index) { - QWidget::paintEvent(e); - if (isEnabled()) { - for (int i = 0; i < m_d->colors.size(); i++) { - if (i == m_d->selectedItem || i == m_d->hoveringItem) { - continue; - } - drawToolButton(this, m_d->itemRect(i), NORMAL, m_d->colors[i], m_d->border); + if (index == -1) { + QAbstractButton* btn = m_d->colorButtonGroup->checkedButton(); + if (btn) { + btn->group()->setExclusive(false); + btn->setChecked(false); + btn->group()->setExclusive(true); } - - if (m_d->selectedItem >= 0) { - drawToolButton(this, m_d->itemRect(m_d->selectedItem), CHECKED, m_d->colors[m_d->selectedItem], m_d->border); - } - - if (m_d->hoveringItem >= 0 && m_d->hoveringItem != m_d->selectedItem) { - drawToolButton(this, m_d->itemRect(m_d->hoveringItem), HOVER, m_d->colors[m_d->hoveringItem], m_d->border); + } else if (index != m_d->colorButtonGroup->checkedId()) { + QAbstractButton* btn = m_d->colorButtonGroup->button(index); + if (btn) { + btn->setChecked(true); } - } else { - for (int i = 0; i < m_d->colors.size(); i++) { - drawToolButton(this, m_d->itemRect(i), DISABLED, m_d->colors[i], m_d->border); - } - } -} - -void KisColorLabelSelectorWidget::keyPressEvent(QKeyEvent *e) -{ - if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Up) { - int newItem = (m_d->selectedItem + 1) % m_d->colors.size(); - setCurrentIndex(newItem); - } else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Down) { - int newItem = m_d->selectedItem < 0 ? m_d->colors.size() - 1 : - (m_d->selectedItem + m_d->colors.size() - 1) % m_d->colors.size(); - setCurrentIndex(newItem); - } - - QWidget::keyPressEvent(e); -} - -void KisColorLabelSelectorWidget::mousePressEvent(QMouseEvent *e) -{ - QWidget::mousePressEvent(e); -} - -void KisColorLabelSelectorWidget::mouseReleaseEvent(QMouseEvent *e) -{ - const int newItem = m_d->indexFromPos(e->pos()); - - if (newItem >= 0 && - (e->button() == Qt::LeftButton || - e->button() == Qt::RightButton)) { - - setCurrentIndex(newItem); } - QWidget::mouseReleaseEvent(e); -} - -void KisColorLabelSelectorWidget::mouseMoveEvent(QMouseEvent *e) -{ - const int oldItem = m_d->hoveringItem; - m_d->hoveringItem = m_d->indexFromPos(e->pos()); - m_d->updateItem(oldItem); - m_d->updateItem(m_d->hoveringItem); - update(); - QWidget::mouseMoveEvent(e); -} - -void KisColorLabelSelectorWidget::leaveEvent(QEvent *e) -{ - const int oldItem = m_d->hoveringItem; - m_d->hoveringItem = -1; - m_d->updateItem(oldItem); - QWidget::leaveEvent(e); + emit currentIndexChanged(index); } diff --git a/libs/ui/widgets/kis_color_label_selector_widget.h b/libs/ui/widgets/kis_color_label_selector_widget.h index 4acbb55234..a16c381345 100644 --- a/libs/ui/widgets/kis_color_label_selector_widget.h +++ b/libs/ui/widgets/kis_color_label_selector_widget.h @@ -1,62 +1,54 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_COLOR_LABEL_SELECTOR_WIDGET_H #define __KIS_COLOR_LABEL_SELECTOR_WIDGET_H #include #include #include "kritaui_export.h" class KRITAUI_EXPORT KisColorLabelSelectorWidget : public QWidget { Q_OBJECT public: KisColorLabelSelectorWidget(QWidget *parent); ~KisColorLabelSelectorWidget() override; - QSize minimumSizeHint() const override; - QSize sizeHint() const override; - int currentIndex() const; + QSize sizeHint() const; + void resizeEvent(QResizeEvent* e) override; + + int calculateMenuOffset() const; + public Q_SLOTS: + void groupButtonChecked(int index, bool state); void setCurrentIndex(int index); Q_SIGNALS: void currentIndexChanged(int index); -protected: - - void resizeEvent(QResizeEvent *e) override; - void paintEvent(QPaintEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void leaveEvent(QEvent *e) override; - private: - struct Private; - const QScopedPointer m_d; + class Private* m_d; }; #endif /* __KIS_COLOR_LABEL_SELECTOR_WIDGET_H */ diff --git a/libs/ui/widgets/kis_layer_filter_widget.cpp b/libs/ui/widgets/kis_layer_filter_widget.cpp new file mode 100644 index 0000000000..954deb7bab --- /dev/null +++ b/libs/ui/widgets/kis_layer_filter_widget.cpp @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "kis_layer_filter_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kis_debug.h" +#include "kis_node.h" +#include "kis_global.h" +#include "kis_icon_utils.h" + +#include "kis_color_filter_combo.h" +#include "kis_color_label_button.h" +#include "kis_color_label_selector_widget.h" +#include "kis_node_view_color_scheme.h" + +#include "KisMouseClickEater.h" + +KisLayerFilterWidget::KisLayerFilterWidget(QWidget *parent) : QWidget(parent) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + setLayout(layout); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + textFilter = new QLineEdit(this); + textFilter->setPlaceholderText(i18n("Filter by name...")); + textFilter->setMinimumWidth(255); + textFilter->setMinimumHeight(28); + textFilter->setClearButtonEnabled(true); + + connect(textFilter, SIGNAL(textChanged(QString)), this, SIGNAL(filteringOptionsChanged())); + connect(textFilter, &QLineEdit::returnPressed, [this]() { + QMenu* menu = dynamic_cast(parentWidget()); + if (menu) { + menu->close(); + } + }); + + KisNodeViewColorScheme colorScheme; + + QWidget* buttonContainer = new QWidget(this); + MouseClickIgnore* mouseEater = new MouseClickIgnore(this); + buttonContainer->setToolTip(i18n("Filter by color label...")); + buttonContainer->installEventFilter(mouseEater); + buttonEventFilter = new KisColorLabelMouseDragFilter(buttonContainer); + { + QHBoxLayout *subLayout = new QHBoxLayout(buttonContainer); + buttonContainer->setLayout(subLayout); + subLayout->setContentsMargins(0,0,0,0); + subLayout->setSpacing(0); + subLayout->setAlignment(Qt::AlignLeft); + buttonGroup = new KisColorLabelFilterGroup(buttonContainer); + buttonGroup->setExclusive(false); + QVector colors = colorScheme.allColorLabels(); + + for (int id = 0; id < colors.count(); id++) { + KisColorLabelButton* btn = new KisColorLabelButton(colors[id], 28, buttonContainer); + buttonGroup->addButton(btn, id); + btn->installEventFilter(buttonEventFilter); + subLayout->addWidget(btn); + } + + connect(buttonGroup, SIGNAL(buttonToggled(int,bool)), this, SIGNAL(filteringOptionsChanged())); + } + + resetButton = new QPushButton(i18n("Reset Filters"), this); + resetButton->setMinimumHeight(28); + connect(resetButton, &QPushButton::clicked, [this](){ + this->reset(); + }); + + + layout->addWidget(textFilter); + layout->addWidget(buttonContainer); + layout->addWidget(resetButton); +} + +void KisLayerFilterWidget::scanUsedColorLabels(KisNodeSP node, QSet &colorLabels) +{ + if (node->parent()) { + colorLabels.insert(node->colorLabelIndex()); + } + + KisNodeSP child = node->firstChild(); + while(child) { + scanUsedColorLabels(child, colorLabels); + child = child->nextSibling(); + } +} + +void KisLayerFilterWidget::updateColorLabels(KisNodeSP root) +{ + QSet colorLabels; + + scanUsedColorLabels(root, colorLabels); + buttonGroup->setViableLabels(colorLabels); +} + +bool KisLayerFilterWidget::isCurrentlyFiltering() const +{ + const bool isFilteringText = hasTextFilter(); + const bool isFilteringColors = buttonGroup->getActiveLabels().count() > 0; + + return isFilteringText || isFilteringColors; +} + +bool KisLayerFilterWidget::hasTextFilter() const +{ + return !textFilter->text().isEmpty(); +} + +QSet KisLayerFilterWidget::getActiveColors() const +{ + QSet activeColors = buttonGroup->getActiveLabels(); + + return activeColors; +} + +QString KisLayerFilterWidget::getTextFilter() const +{ + return textFilter->text(); +} + +int KisLayerFilterWidget::getDesiredMinimumWidth() const { + return qMax(textFilter->minimumWidth(), buttonGroup->countViableButtons() * 32); +} + +int KisLayerFilterWidget::getDesiredMinimumHeight() const { + QList viableButtons = buttonGroup->viableButtons(); + if (viableButtons.count() > 1) { + return viableButtons[0]->sizeHint().height() + textFilter->minimumHeight() + resetButton->minimumHeight(); + } else { + return textFilter->minimumHeight() + resetButton->minimumHeight(); + } +} + +void KisLayerFilterWidget::reset() +{ + textFilter->clear(); + buttonGroup->reset(); + filteringOptionsChanged(); +} + +QSize KisLayerFilterWidget::sizeHint() const +{ + return QSize(getDesiredMinimumWidth(), getDesiredMinimumHeight()); +} + +void KisLayerFilterWidget::showEvent(QShowEvent *show) +{ + QMenu *parentMenu = dynamic_cast(parentWidget()); + + if (parentMenu) { + const int widthBefore = parentMenu->width(); + const int rightEdgeThreshold = 5; + + //Fake resize event needs to be made to register change in widget menu size. + //Not doing this will cause QMenu to not resize properly! + resize(sizeHint()); + + adjustSize(); + QResizeEvent event = QResizeEvent(sizeHint(), parentMenu->size()); + + parentMenu->resize(sizeHint()); + parentMenu->adjustSize(); + qApp->sendEvent(parentMenu, &event); + + QScreen *screen = QGuiApplication::screenAt(parentMenu->mapToGlobal(parentMenu->pos())); + QRect screenGeometry = screen ? screen->geometry() : parentMenu->parentWidget()->window()->geometry(); + + const bool onRightEdge = (parentMenu->pos().x() + widthBefore + rightEdgeThreshold) > screenGeometry.width(); + const int widthAfter = parentMenu->width(); + + + if (onRightEdge) { + if (widthAfter > widthBefore) { + const QRect newGeo = kisEnsureInRect( parentMenu->geometry(), screenGeometry ); + const int xShift = newGeo.x() - parentMenu->pos().x(); + parentMenu->move(parentMenu->pos().x() + xShift, parentMenu->pos().y() + 0); + } else { + const int xShift = widthBefore - widthAfter; + parentMenu->move(parentMenu->pos().x() + xShift, parentMenu->pos().y() + 0); + } + } + } + QWidget::showEvent(show); +} + +KisLayerFilterWidgetToolButton::KisLayerFilterWidgetToolButton(QWidget *parent) + : QToolButton(parent) +{ + m_textFilter = false; + m_selectedColors = QList(); +} + +KisLayerFilterWidgetToolButton::KisLayerFilterWidgetToolButton(const KisLayerFilterWidgetToolButton &rhs) + : QToolButton(rhs.parentWidget()) + , m_textFilter(rhs.m_textFilter) + , m_selectedColors(rhs.m_selectedColors) +{ + +} + +void KisLayerFilterWidgetToolButton::setSelectedColors(QList colors) +{ + m_selectedColors = colors; +} + +void KisLayerFilterWidgetToolButton::setTextFilter(bool isTextFiltering) +{ + m_textFilter = isTextFiltering; +} + +void KisLayerFilterWidgetToolButton::paintEvent(QPaintEvent *paintEvent) +{ + KisNodeViewColorScheme colorScheme; + const bool validColorFilter = !(m_selectedColors.count() == 0 || m_selectedColors.count() == colorScheme.allColorLabels().count()); + + if (m_textFilter == false && !validColorFilter) + { + QToolButton::paintEvent(paintEvent); + } + else + { + QStylePainter paint(this); + QStyleOptionToolButton opt; + initStyleOption(&opt); + opt.icon = m_textFilter ? KisIconUtils::loadIcon("format-text-bold") : icon(); + paint.drawComplexControl(QStyle::CC_ToolButton, opt); + const QSize halfIconSize = this->iconSize() / 2; + const QSize halfButtonSize = this->size() / 2; + const QRect editRect = kisGrowRect(QRect(QPoint(halfButtonSize.width() - halfIconSize.width(), halfButtonSize.height() - halfIconSize.height()),this->iconSize()), -1); + const int size = qMin(editRect.width(), editRect.height()); + + if( validColorFilter ) + { + KisColorFilterCombo::paintColorPie(paint, opt.palette, m_selectedColors, editRect, size ); + if (m_textFilter) { + if (!opt.icon.isNull()) { + QRadialGradient radGradient = QRadialGradient(editRect.center(), size); + QColor shadowTransparent = palette().shadow().color(); + shadowTransparent.setAlpha(96); + radGradient.setColorAt(0.0f, shadowTransparent); + shadowTransparent.setAlpha(0); + radGradient.setColorAt(1.0f, shadowTransparent); + paint.setBrush(radGradient); + paint.setPen(Qt::NoPen); + paint.drawEllipse(editRect.center(), size, size); + opt.icon.paint(&paint, editRect); + } + } + } + } +} + +MouseClickIgnore::MouseClickIgnore(QObject *parent) + : QObject(parent) +{ +} + +bool MouseClickIgnore::eventFilter(QObject *obj, QEvent *event) +{ + if (obj && + (event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick || + event->type() == QEvent::MouseButtonRelease)) { + event->setAccepted(true); + return true; + } else { + return false; + } +} diff --git a/libs/ui/widgets/kis_layer_filter_widget.h b/libs/ui/widgets/kis_layer_filter_widget.h new file mode 100644 index 0000000000..7dbe94e317 --- /dev/null +++ b/libs/ui/widgets/kis_layer_filter_widget.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#ifndef KISLAYERFILTERWIDGET_H +#define KISLAYERFILTERWIDGET_H + +#include +#include +#include "kis_types.h" + +#include "kritaui_export.h" + + +class KRITAUI_EXPORT KisLayerFilterWidget : public QWidget +{ + Q_OBJECT +private: + class KisColorLabelMouseDragFilter *buttonEventFilter; + class QLineEdit *textFilter; + class KisColorLabelFilterGroup *buttonGroup; + class QPushButton *resetButton; + +public: + KisLayerFilterWidget(QWidget *parent = nullptr); + + static void scanUsedColorLabels(KisNodeSP node, QSet &colorLabels); + void updateColorLabels(KisNodeSP root); + + bool isCurrentlyFiltering() const; + bool hasTextFilter() const; + QSet getActiveColors() const; + QString getTextFilter() const; + + int getDesiredMinimumWidth() const; + int getDesiredMinimumHeight() const; + + void reset(); + + QSize sizeHint() const override; + + /* Show Event has to be overridden to + * correct for parent QMenu to properly + * resize. */ + void showEvent(QShowEvent *show) override; + +Q_SIGNALS: + void filteringOptionsChanged(); + +}; + +class KRITAUI_EXPORT KisLayerFilterWidgetToolButton : public QToolButton +{ + Q_OBJECT +public: + explicit KisLayerFilterWidgetToolButton(QWidget *parent = nullptr); + KisLayerFilterWidgetToolButton(const KisLayerFilterWidgetToolButton& rhs); + ~KisLayerFilterWidgetToolButton(){} + + void setSelectedColors(QList colors); + void setTextFilter(bool isTextFiltering); + + +private: + void paintEvent(QPaintEvent *paintEvent) override; + +private: + bool m_textFilter; + QList m_selectedColors; +}; + +class KRITAUI_EXPORT MouseClickIgnore : public QObject { + Q_OBJECT +public: + MouseClickIgnore(QObject *parent = nullptr); + bool eventFilter(QObject *obj, QEvent *event); + +}; + +#endif // KISLAYERFILTERWIDGET_H diff --git a/libs/widgetutils/CMakeLists.txt b/libs/widgetutils/CMakeLists.txt index 48499f117e..bff769c68b 100644 --- a/libs/widgetutils/CMakeLists.txt +++ b/libs/widgetutils/CMakeLists.txt @@ -1,143 +1,144 @@ add_subdirectory(tests) configure_file(xmlgui/config-xmlgui.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-xmlgui.h ) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/config) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/xmlgui) set(kritawidgetutils_LIB_SRCS WidgetUtilsDebug.cpp kis_icon_utils.cpp kis_action_registry.cpp KisActionsSnapshot.cpp KoGroupButton.cpp KoProgressProxy.cpp KoFakeProgressProxy.cpp KoProgressBar.cpp KoProgressUpdater.cpp KoUpdater.cpp KoUpdaterPrivate_p.cpp KoProperties.cpp KoFileDialog.cpp KisKineticScroller.cpp KoCheckerBoardPainter.cpp KoItemToolTip.cpp KisSqueezedComboBox.cpp KisDialogStateSaver.cpp KisPopupButton.cpp kis_cursor.cc kis_cursor_cache.cpp kis_double_parse_spin_box.cpp kis_double_parse_unit_spin_box.cpp kis_int_parse_spin_box.cpp kis_num_parser.cpp kis_slider_spin_box.cpp + kis_zoom_scrollbar.cpp kis_spin_box_unit_manager.cpp config/kcolorscheme.cpp config/kcolorschememanager.cpp config/khelpclient.cpp config/klanguagebutton.cpp config/krecentfilesaction.cpp config/kstandardaction.cpp xmlgui/KisShortcutsEditorItem.cpp xmlgui/KisShortcutEditWidget.cpp xmlgui/KisShortcutsEditorDelegate.cpp xmlgui/KisShortcutsDialog.cpp xmlgui/KisShortcutsDialog_p.cpp xmlgui/KisShortcutsEditor.cpp xmlgui/KisShortcutsEditor_p.cpp xmlgui/kshortcutschemeseditor.cpp xmlgui/kshortcutschemeshelper.cpp xmlgui/kaboutkdedialog_p.cpp xmlgui/kactioncategory.cpp xmlgui/kactioncollection.cpp xmlgui/kbugreport.cpp xmlgui/kcheckaccelerators.cpp xmlgui/kedittoolbar.cpp xmlgui/kgesture.cpp xmlgui/kgesturemap.cpp xmlgui/khelpmenu.cpp xmlgui/kkeysequencewidget.cpp xmlgui/kmainwindow.cpp xmlgui/kmenumenuhandler_p.cpp xmlgui/kshortcutwidget.cpp xmlgui/kswitchlanguagedialog_p.cpp xmlgui/ktoggletoolbaraction.cpp xmlgui/ktoolbar.cpp xmlgui/ktoolbarhandler.cpp xmlgui/kundoactions.cpp xmlgui/kxmlguibuilder.cpp xmlgui/kxmlguiclient.cpp xmlgui/kxmlguifactory.cpp xmlgui/kxmlguifactory_p.cpp xmlgui/kxmlguiversionhandler.cpp xmlgui/kxmlguiwindow.cpp ) if (HAVE_DBUS) set(kritawidgetutils_LIB_SRCS ${kritawidgetutils_LIB_SRCS} xmlgui/kmainwindowiface.cpp ) endif() ki18n_wrap_ui(kritawidgetutils_LIB_SRCS xmlgui/KisShortcutsDialog.ui xmlgui/kshortcutwidget.ui ) qt5_add_resources(kritawidgetutils_LIB_SRCS xmlgui/kxmlgui.qrc) add_library(kritawidgetutils SHARED ${kritawidgetutils_LIB_SRCS}) target_include_directories(kritawidgetutils PUBLIC $ $ ) generate_export_header(kritawidgetutils BASE_NAME kritawidgetutils) if (HAVE_DBUS) set (KRITA_WIDGET_UTILS_EXTRA_LIBS ${KRITA_WIDGET_UTILS_EXTRA_LIBS} Qt5::DBus) endif () if (APPLE) find_library(FOUNDATION_LIBRARY Foundation) set(KRITA_WIDGET_UTILS_EXTRA_LIBS ${KRITA_WIDGET_UTILS_EXTRA_LIBS} ${FOUNDATION_LIBRARY}) endif () target_link_libraries(kritawidgetutils PUBLIC Qt5::Widgets Qt5::Gui Qt5::Xml Qt5::Core KF5::ItemViews kritaglobal kritaresources PRIVATE Qt5::PrintSupport KF5::I18n KF5::ConfigCore KF5::CoreAddons KF5::ConfigGui KF5::GuiAddons KF5::WidgetsAddons KF5::WindowSystem kritaplugin ${KRITA_WIDGET_UTILS_EXTRA_LIBS} ) set_target_properties(kritawidgetutils PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritawidgetutils ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/widgetutils/kis_zoom_scrollbar.cpp b/libs/widgetutils/kis_zoom_scrollbar.cpp new file mode 100644 index 0000000000..850598908d --- /dev/null +++ b/libs/widgetutils/kis_zoom_scrollbar.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "kis_zoom_scrollbar.h" + +#include "kis_global.h" +#include "kis_debug.h" +#include +#include + +KisZoomableScrollBar::KisZoomableScrollBar(QWidget *parent) + : QScrollBar(parent) + , lastKnownPosition(0,0) + , accelerationAccumulator(0,0) + , scrollSubPixelAccumulator(0.0f) + , zoomThreshold(0.75f) + , catchTeleportCorrection(false) +{ +} + +KisZoomableScrollBar::KisZoomableScrollBar(Qt::Orientation orientation, QWidget *parent) + : KisZoomableScrollBar(parent) +{ + setOrientation(orientation); +} + +KisZoomableScrollBar::~KisZoomableScrollBar() +{ + +} + +QPoint KisZoomableScrollBar::barPosition() +{ + + float barPositionNormalized = (float)(value() - minimum()) / (float)(maximum() + pageStep() - minimum()); + QPoint barPosition = orientation() == Qt::Horizontal ? + QPoint(barPositionNormalized * width() * devicePixelRatio(), 0) : + QPoint(0, barPositionNormalized * height() * devicePixelRatio()); + + return mapToGlobal(QPoint(0,0)) + barPosition; +} + +bool KisZoomableScrollBar::catchTeleports(QMouseEvent *event) { + if (catchTeleportCorrection) { + catchTeleportCorrection = false; + event->accept(); + return true; + } + + return false; +} + +void KisZoomableScrollBar::handleWrap( const QPoint &accel, const QPoint &mouseCoord) +{ + QRect windowRect = window()->geometry(); + windowRect = kisGrowRect(windowRect, -2); + const int windowWidth = windowRect.width(); + const int windowHeight = windowRect.height(); + const int windowX = windowRect.x(); + const int windowY = windowRect.y(); + const bool xWrap = true; + const bool yWrap = true; + + if (!windowRect.contains(mouseCoord)) { + int x = mouseCoord.x(); + int y = mouseCoord.y(); + + if (x < windowX && xWrap ) { + x += (windowWidth - 2); + } else if (x > (windowX + windowWidth) && xWrap ) { + x -= (windowWidth - 2); + } + + if (y < windowY && yWrap) { + y += (windowHeight - 2); + } else if (y > (windowY + windowHeight) && yWrap) { + y -= (windowHeight - 2); + } + + QCursor::setPos(x, y); + lastKnownPosition = QPoint(x, y) - accel; + + //Important -- teleportation needs to caught to prevent high-acceleration + //values from QCursor::setPos being read in this event. + catchTeleportCorrection = true; + } +} + +void KisZoomableScrollBar::handleScroll(const QPoint &accel) +{ + const qreal sliderMovementPix = (orientation() == Qt::Horizontal) ? accel.x() * devicePixelRatio() : accel.y() * devicePixelRatio(); + const qreal zoomMovementPix = (orientation() == Qt::Horizontal) ? -accel.y() : -accel.x(); + const qreal documentLength = maximum() - minimum() + pageStep(); + const qreal widgetLength = (orientation() == Qt::Horizontal) ? width() * devicePixelRatio() : height() * devicePixelRatio(); + const qreal widgetThickness = (orientation() == Qt::Horizontal) ? height() * devicePixelRatio() : width() * devicePixelRatio(); + + const QVector2D perpendicularDirection = (orientation() == Qt::Horizontal) ? QVector2D(0, 1) : QVector2D(1, 0); + const float perpendicularity = QVector2D::dotProduct(perpendicularDirection.normalized(), accelerationAccumulator.normalized()); + + if (qAbs(perpendicularity) > zoomThreshold && zoomMovementPix != 0) { + zoom(qreal(zoomMovementPix) / qreal(widgetThickness * 2)); + } else if (sliderMovementPix != 0) { + const int currentPosition = sliderPosition(); + scrollSubPixelAccumulator += (documentLength) * (sliderMovementPix / widgetLength); + + setSliderPosition(currentPosition + scrollSubPixelAccumulator); + if (currentPosition + scrollSubPixelAccumulator > maximum() || + currentPosition + scrollSubPixelAccumulator < minimum()) { + overscroll(scrollSubPixelAccumulator); + } + + const int sign = (scrollSubPixelAccumulator > 0) - (scrollSubPixelAccumulator < 0); + scrollSubPixelAccumulator -= floor(abs(scrollSubPixelAccumulator)) * sign; + } +} + +void KisZoomableScrollBar::tabletEvent(QTabletEvent *event) { + if ( event->type() == QTabletEvent::TabletMove && isSliderDown() ) { + QPoint globalMouseCoord = mapToGlobal(event->pos()); + QPoint accel = globalMouseCoord - lastKnownPosition; + accelerationAccumulator += QVector2D(accel); + + if( accelerationAccumulator.length() > 5) { + accelerationAccumulator = accelerationAccumulator.normalized(); + } + + handleScroll(accel); + lastKnownPosition = globalMouseCoord; + event->accept(); + } else { + + if (event->type() == QTabletEvent::TabletPress) { + QPoint globalMouseCoord = mapToGlobal(event->pos()); + lastKnownPosition = globalMouseCoord; + setSliderDown(true); + event->accept(); + } else { + QScrollBar::tabletEvent(event); + } + } +} + +void KisZoomableScrollBar::mousePressEvent(QMouseEvent *event) +{ + const bool wasSliderDownBefore = isSliderDown(); + QScrollBar::mousePressEvent(event); + + if( isSliderDown() && !wasSliderDownBefore ){ + lastKnownPosition = mapToGlobal(event->pos()); + accelerationAccumulator = QVector2D(0,0); + QPoint worldPosition = mapToGlobal(event->pos()); + QPoint barPosition = this->barPosition(); + initialPositionRelativeToBar = worldPosition - barPosition; + setCursor(Qt::BlankCursor); + } + +} + + +void KisZoomableScrollBar::mouseMoveEvent(QMouseEvent *event) +{ + if (isSliderDown()) { + QPoint globalMouseCoord = mapToGlobal(event->pos()); + + QPoint accel = globalMouseCoord - lastKnownPosition; + accelerationAccumulator += QVector2D(accel); + + if (catchTeleports(event)){ + return; + } + + if( accelerationAccumulator.length() > 5 ) { + accelerationAccumulator = accelerationAccumulator.normalized(); + } + + handleScroll(accel); + lastKnownPosition = globalMouseCoord; + handleWrap(accel, mapToGlobal(event->pos())); + event->accept(); + } else { + QScrollBar::mouseMoveEvent(event); + } +} + +void KisZoomableScrollBar::mouseReleaseEvent(QMouseEvent *event) +{ + const QPoint maximumCoordinates = mapToGlobal(QPoint(width() * devicePixelRatio(), height() * devicePixelRatio())); + const QPoint minimumCoordinates = mapToGlobal(QPoint(0,0)); + const QPoint desiredCoordinates = barPosition() + initialPositionRelativeToBar; + QPoint cursorPosition = QPoint( + qMax(minimumCoordinates.x(), qMin(maximumCoordinates.x(), desiredCoordinates.x())), + qMax(minimumCoordinates.y(), qMin(maximumCoordinates.y(), desiredCoordinates.y())) + ); + QCursor::setPos(cursorPosition); + setCursor(Qt::ArrowCursor); + QScrollBar::mouseReleaseEvent(event); +} + +void KisZoomableScrollBar::wheelEvent(QWheelEvent *event) { + const int delta = (event->angleDelta().y() / 8) * singleStep() * -1; + const int currentPosition = sliderPosition(); + + if (currentPosition + delta > maximum() || currentPosition + delta < minimum()){ + overscroll(delta); + } + + QScrollBar::wheelEvent(event); +} + +void KisZoomableScrollBar::setZoomDeadzone(float value) +{ + zoomThreshold = value; +} diff --git a/libs/widgetutils/kis_zoom_scrollbar.h b/libs/widgetutils/kis_zoom_scrollbar.h new file mode 100644 index 0000000000..06fa7d7dd5 --- /dev/null +++ b/libs/widgetutils/kis_zoom_scrollbar.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Eoin O'Neill + * Copyright (c) 2020 Emmet O'Neill + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + +#ifndef KIS_ZOOM_SCROLLBAR_H +#define KIS_ZOOM_SCROLLBAR_H + +#include +#include + +#include + +class KRITAWIDGETUTILS_EXPORT KisZoomableScrollBar : public QScrollBar +{ + Q_OBJECT + +private: + QPoint initialPositionRelativeToBar; + QPoint lastKnownPosition; + QVector2D accelerationAccumulator; + qreal scrollSubPixelAccumulator; + qreal zoomThreshold; + bool catchTeleportCorrection = false; + +public: + KisZoomableScrollBar(QWidget* parent = 0); + KisZoomableScrollBar(Qt::Orientation orientation, QWidget * parent = 0); + ~KisZoomableScrollBar(); + + QPoint barPosition(); + + //Catch for teleportation from one side of the screen to the other. + bool catchTeleports(QMouseEvent* event); + + //Window-space wrapping for mouse dragging. Allows for blender-like + //infinite mouse scrolls. + void handleWrap(const QPoint &accel, const QPoint &globalMouseCoord); + + //Scroll based on a mouse acceleration value. + void handleScroll(const QPoint &accel); + + void tabletEvent(QTabletEvent *event) override; + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + + virtual void wheelEvent(QWheelEvent *event) override; + + void setZoomDeadzone(float value); + +Q_SIGNALS: + void zoom(qreal delta); + void overscroll(int delta); +}; + +#endif // KIS_ZOOM_SCROLLBAR_H diff --git a/packaging/linux/snap/snapcraft.yaml b/packaging/linux/snap/snapcraft.yaml index 64352f208d..d23dc215da 100644 --- a/packaging/linux/snap/snapcraft.yaml +++ b/packaging/linux/snap/snapcraft.yaml @@ -1,107 +1,107 @@ name: krita grade: stable adopt-info: krita base: core18 confinement: strict apps: krita: common-id: org.kde.krita command: usr/bin/krita extensions: - kde-neon plugs: - home - opengl - network - network-bind - removable-media layout: /usr/bin/ffmpeg: bind-file: $SNAP/usr/bin/ffmpeg parts: krita: plugin: cmake configflags: - "-DCMAKE_INSTALL_PREFIX=/usr" - "-DCMAKE_BUILD_TYPE=Release" - "-DENABLE_TESTING=OFF" - "-DBUILD_TESTING=OFF" - "-DHIDE_SAFE_ASSERTS=OFF" - "-DKDE_SKIP_TEST_SETTINGS=ON" - source: https://download.kde.org/stable/krita/4.2.9/krita-4.2.9.tar.xz + source: https://download.kde.org/stable/krita/4.3.0/krita-4.3.0.tar.xz # # Use these instead to build from the git source # source: https://anongit.kde.org/krita.git # source-type: git # source-branch: master parse-info: ["usr/share/metainfo/org.kde.krita.appdata.xml"] build-snaps: [kde-frameworks-5-core18-sdk] build-packages: - libboost-dev - libboost-system-dev - libeigen3-dev - libfftw3-dev - libglew-dev - libgsf-1-dev - libgsl-dev - libjpeg-dev # This is part of the sdk, but the build crashes if not included.. - libopencolorio-dev - libopenexr-dev - libopenimageio-dev - libquazip5-dev - libvc-dev - libx11-dev - libxi-dev - vc-dev parse-info: [usr/share/metainfo/org.kde.krita.appdata.xml] runtime: plugin: nil stage-packages: - libboost-system1.65.1 - libfftw3-double3 - libgsl23 - libgslcblas0 - libilmbase12 - libopencolorio1v5 - libopenexr22 - libquazip5-1 - libtinyxml2.6.2v5 - libxi6 - libyaml-cpp0.5v5 - zlib1g # Required for rendering animations - ffmpeg - libglu1-mesa - libslang2 prime: - "-usr/share/fonts/*" # libquazip5-1 pulls in Qt5 from bionic as a dependency. We don't # want it in our snap, however, because we get a newer Qt5 from the # kde-kf5 platform snap. - "-usr/lib/x86_64-linux-gnu/libQt5*" - "-usr/lib/x86_64-linux-gnu/libqt5*" # This part removes all the files in this snap which already exist in # connected content and base snaps. Since these files will be available # at runtime from the content and base snaps, they do not need to be # included in this snap itself. # # More info: https://snapcraft-utils-library.readthedocs.io/en/latest/lib/cleanup.html # cleanup: after: # Make this part run last; list all your other parts here - krita - runtime plugin: nil build-snaps: # List all content-snaps and base snaps you're using here - core18 - kde-frameworks-5-core18 override-prime: | set -eux for snap in "core18" "kde-frameworks-5-core18"; do # List all content-snaps and base snaps you're using here cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$SNAPCRAFT_PRIME/{}" \; done diff --git a/plugins/assistants/Assistants/kis_assistant_tool.cc b/plugins/assistants/Assistants/kis_assistant_tool.cc index a56481b05f..be50aa14cb 100644 --- a/plugins/assistants/Assistants/kis_assistant_tool.cc +++ b/plugins/assistants/Assistants/kis_assistant_tool.cc @@ -1,1294 +1,1294 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_global.h" #include "VanishingPointAssistant.h" #include "EditAssistantsCommand.h" #include #include "ConjugateAssistant.h" #include #include KisAssistantTool::KisAssistantTool(KoCanvasBase * canvas) : KisTool(canvas, KisCursor::arrowCursor()), m_canvas(dynamic_cast(canvas)), m_assistantDrag(0), m_newAssistant(0), m_optionsWidget(0) { Q_ASSERT(m_canvas); setObjectName("tool_assistanttool"); } KisAssistantTool::~KisAssistantTool() { } void KisAssistantTool::activate(ToolActivation toolActivation, const QSet &shapes) { KisTool::activate(toolActivation, shapes); m_canvas->paintingAssistantsDecoration()->activateAssistantsEditor(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_handleDrag = 0; m_internalMode = MODE_CREATION; m_assistantHelperYOffset = 10; m_handleSize = 17; m_canvas->paintingAssistantsDecoration()->setHandleSize(m_handleSize); if (m_optionsWidget) { m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } m_canvas->updateCanvas(); } void KisAssistantTool::deactivate() { m_canvas->paintingAssistantsDecoration()->deactivateAssistantsEditor(); m_canvas->updateCanvas(); KisTool::deactivate(); } void KisAssistantTool::beginPrimaryAction(KoPointerEvent *event) { setMode(KisTool::PAINT_MODE); m_origAssistantList = KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants()); bool newAssistantAllowed = true; KisPaintingAssistantsDecorationSP canvasDecoration = m_canvas->paintingAssistantsDecoration(); if (m_newAssistant) { m_internalMode = MODE_CREATION; *m_newAssistant->handles().back() = canvasDecoration->snapToGuide(event, QPointF(), false); // give conjugate assistant side handles once it is completed if (m_newAssistant->id() == "conjugate" && m_newAssistant->handles().size() == m_newAssistant->numHandles() && m_newAssistant->sideHandles().isEmpty()){ QList handles = m_newAssistant->handles(); const QLineF horizon = QLineF(*handles[0],*handles[1]); QLineF vertical = horizon.normalVector(); vertical.translate(*handles[2] - vertical.p1()) ; QPointF cov; horizon.intersect(vertical, &cov); QLineF distance = QLineF(*handles[2],cov); distance.setLength(distance.length() * 2.0); // Double it, we will ise both p1() and p2() const qreal length = QLineF(*handles[2],cov).length(); QLineF bar; bar= QLineF(distance.p1(), *handles[0]); bar.setLength(length); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar.setLength(length * 0.5); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar= QLineF(distance.p1(), *handles[1]); bar.setLength(length); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar.setLength(length * 0.5); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar= QLineF(distance.p2(), *handles[0]); bar.setLength(length); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar.setLength(length * 0.5); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar= QLineF(distance.p2(), *handles[1]); bar.setLength(length); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); bar.setLength(length * 0.5); m_newAssistant->addHandle(new KisPaintingAssistantHandle(bar.p2()), HandleType::SIDE); } if (m_newAssistant->handles().size() == m_newAssistant->numHandles()) { addAssistant(); } else { m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); } m_canvas->updateCanvas(); return; } m_handleDrag = 0; double minDist = 81.0; QPointF mousePos = m_canvas->viewConverter()->documentToView(canvasDecoration->snapToGuide(event, QPointF(), false));//m_canvas->viewConverter()->documentToView(event->point); // syncs the assistant handles to the handles reference we store in this tool // they can get out of sync with the way the actions and paintevents occur // we probably need to stop storing a reference in m_handles and call the assistants directly m_handles = m_canvas->paintingAssistantsDecoration()->handles(); Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { // find out which handle on all assistants is closest to the mouse position // vanishing points have "side handles", so make sure to include that { QList allAssistantHandles; allAssistantHandles.append(assistant->handles()); allAssistantHandles.append(assistant->sideHandles()); Q_FOREACH (const KisPaintingAssistantHandleSP handle, allAssistantHandles) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*handle)); if (dist < minDist) { minDist = dist; m_handleDrag = handle; assistantSelected(assistant); // whatever handle is the closest contains the selected assistant } } } if(m_handleDrag && assistant->id() == "perspective") { // Look for the handle which was pressed if (m_handleDrag == assistant->topLeft()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_dragStart = QPointF(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_internalMode = MODE_DRAGGING_NODE; } else if (m_handleDrag == assistant->topRight()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); } else if (m_handleDrag == assistant->bottomLeft()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); } else if (m_handleDrag == assistant->bottomRight()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); } else if (m_handleDrag == assistant->leftMiddle()) { m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_dragStart = QPointF((assistant->bottomLeft().data()->x()+assistant->topLeft().data()->x())*0.5, (assistant->bottomLeft().data()->y()+assistant->topLeft().data()->y())*0.5); m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->topLeft(), HandleType::NORMAL ); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomLeft(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->rightMiddle()) { m_dragStart = QPointF((assistant->topRight().data()->x()+assistant->bottomRight().data()->x())*0.5, (assistant->topRight().data()->y()+assistant->bottomRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->topRight(), HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomRight(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->topMiddle()) { m_dragStart = QPointF((assistant->topLeft().data()->x()+assistant->topRight().data()->x())*0.5, (assistant->topLeft().data()->y()+assistant->topRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->topRight(), HandleType::NORMAL); m_newAssistant->addHandle(assistant->topLeft(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->bottomMiddle()) { m_dragStart = QPointF((assistant->bottomLeft().data()->x()+assistant->bottomRight().data()->x())*0.5, (assistant->bottomLeft().data()->y()+assistant->bottomRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->bottomLeft(), HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomRight(), HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } m_snapIsRadial = false; } else if (m_handleDrag && assistant->handles().size()>1 && (assistant->id() == "ruler" || assistant->id() == "parallel ruler" || assistant->id() == "infinite ruler" || assistant->id() == "spline")){ if (m_handleDrag == assistant->handles()[0]) { m_dragStart = *assistant->handles()[1]; } else if (m_handleDrag == assistant->handles()[1]) { m_dragStart = *assistant->handles()[0]; } else if(assistant->handles().size()==4){ if (m_handleDrag == assistant->handles()[2]) { m_dragStart = *assistant->handles()[0]; } else if (m_handleDrag == assistant->handles()[3]) { m_dragStart = *assistant->handles()[1]; } } m_snapIsRadial = false; } else if (m_handleDrag && assistant->handles().size()>2 && (assistant->id() == "ellipse" || assistant->id() == "concentric ellipse" || assistant->id() == "fisheye-point" || assistant->id() == "conjugate")){ m_snapIsRadial = false; if (m_handleDrag == assistant->handles()[0]) { m_dragStart = *assistant->handles()[1]; } else if (m_handleDrag == assistant->handles()[1]) { m_dragStart = *assistant->handles()[0]; } else if (m_handleDrag == assistant->handles()[2]) { m_dragStart = assistant->getEditorPosition(); m_radius = QLineF(m_dragStart, *assistant->handles()[0]); m_snapIsRadial = true; } } else { m_dragStart = assistant->getEditorPosition(); m_snapIsRadial = false; } } if (m_handleDrag) { // TODO: Shift-press should now be handled using the alternate actions // if (event->modifiers() & Qt::ShiftModifier) { // m_handleDrag->uncache(); // m_handleDrag = m_handleDrag->split()[0]; // m_handles = m_canvas->view()->paintingAssistantsDecoration()->handles(); // } m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } m_assistantDrag.clear(); Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { AssistantEditorData editorShared; // shared position data between assistant tool and decoration const KisCoordinatesConverter *converter = m_canvas->coordinatesConverter(); // This code contains the click event behavior. QTransform initialTransform = converter->documentToWidgetTransform(); QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, editorShared.boundingSize)); // for UI editor widget controls with move, show, and delete -- disregard document transforms like rotating and mirroring. // otherwise the UI controls get awkward to use when they are at 45 degree angles or the order of controls gets flipped backwards QPointF uiMousePosition = initialTransform.map(canvasDecoration->snapToGuide(event, QPointF(), false)); QPointF iconMovePosition(actionsPosition + editorShared.moveIconPosition); QPointF iconSnapPosition(actionsPosition + editorShared.snapIconPosition); QPointF iconDeletePosition(actionsPosition + editorShared.deleteIconPosition); QRectF deleteRect(iconDeletePosition, QSizeF(editorShared.deleteIconSize, editorShared.deleteIconSize)); QRectF visibleRect(iconSnapPosition, QSizeF(editorShared.snapIconSize, editorShared.snapIconSize)); QRectF moveRect(iconMovePosition, QSizeF(editorShared.moveIconSize, editorShared.moveIconSize)); if (moveRect.contains(uiMousePosition)) { m_assistantDrag = assistant; m_cursorStart = event->point; m_currentAdjustment = QPointF(); m_internalMode = MODE_EDITING; assistantSelected(assistant); // whatever handle is the closest contains the selected assistant return; } if (deleteRect.contains(uiMousePosition)) { removeAssistant(assistant); if(m_canvas->paintingAssistantsDecoration()->assistants().isEmpty()) { m_internalMode = MODE_CREATION; } else m_internalMode = MODE_EDITING; m_canvas->updateCanvas(); return; } if (visibleRect.contains(uiMousePosition)) { newAssistantAllowed = false; assistant->setSnappingActive(!assistant->isSnappingActive()); // toggle assistant->uncache();//this updates the chache of the assistant, very important. assistantSelected(assistant); // whatever handle is the closest contains the selected assistant } } if (newAssistantAllowed==true){//don't make a new assistant when I'm just toogling visibility// QString key = m_options.availableAssistantsComboBox->model()->index( m_options.availableAssistantsComboBox->currentIndex(), 0 ).data(Qt::UserRole).toString(); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get(key)->createPaintingAssistant()); m_internalMode = MODE_CREATION; m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); if (m_newAssistant->numHandles() <= 1) { addAssistant(); } else { m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); } } if (m_newAssistant) { m_newAssistant->setAssistantGlobalColorCache(m_canvas->paintingAssistantsDecoration()->globalAssistantsColor()); } m_canvas->updateCanvas(); } void KisAssistantTool::continuePrimaryAction(KoPointerEvent *event) { KisPaintingAssistantsDecorationSP canvasDecoration = m_canvas->paintingAssistantsDecoration(); if (m_handleDrag) { *m_handleDrag = event->point; //ported from the gradient tool... we need to think about this more in the future. if (event->modifiers() == Qt::ShiftModifier && m_snapIsRadial) { QLineF dragRadius = QLineF(m_dragStart, event->point); dragRadius.setLength(m_radius.length()); *m_handleDrag = dragRadius.p2(); } else if (event->modifiers() == Qt::ShiftModifier ) { QPointF move = snapToClosestAxis(event->point - m_dragStart); *m_handleDrag = m_dragStart + move; } else { *m_handleDrag = canvasDecoration->snapToGuide(event, QPointF(), false); } m_handleDrag->uncache(); m_handleCombine = 0; if (!(event->modifiers() & Qt::ShiftModifier)) { double minDist = 49.0; QPointF mousePos = m_canvas->viewConverter()->documentToView(event->point); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { if (handle == m_handleDrag) continue; double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*handle)); if (dist < minDist) { minDist = dist; m_handleCombine = handle; } } } m_canvas->updateCanvas(); } else if (m_assistantDrag) { QPointF newAdjustment = canvasDecoration->snapToGuide(event, QPointF(), false) - m_cursorStart; if (event->modifiers() == Qt::ShiftModifier ) { newAdjustment = snapToClosestAxis(newAdjustment); } Q_FOREACH (KisPaintingAssistantHandleSP handle, m_assistantDrag->handles()) { *handle += (newAdjustment - m_currentAdjustment); } if (m_assistantDrag->id()== "vanishing point" || m_assistantDrag->id()== "conjugate"){ Q_FOREACH (KisPaintingAssistantHandleSP handle, m_assistantDrag->sideHandles()) { *handle += (newAdjustment - m_currentAdjustment); } } m_assistantDrag->uncache(); m_currentAdjustment = newAdjustment; m_canvas->updateCanvas(); } else { event->ignore(); } bool wasHiglightedNode = m_higlightedNode != 0; QPointF mousep = m_canvas->viewConverter()->documentToView(event->point); QList pAssistant= m_canvas->paintingAssistantsDecoration()->assistants(); Q_FOREACH (KisPaintingAssistantSP assistant, pAssistant) { if(assistant->id() == "perspective") { if ((m_higlightedNode = assistant->closestCornerHandleFromPoint(mousep))) { if (m_higlightedNode == m_selectedNode1 || m_higlightedNode == m_selectedNode2) { m_higlightedNode = 0; } else { m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas break; } } } //this following bit sets the translations for the vanishing-point handles. if(m_handleDrag && assistant->id() == "vanishing point" && assistant->sideHandles().size()==4) { //for inner handles, the outer handle gets translated. if (m_handleDrag == assistant->sideHandles()[0]) { QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[0]); qreal length = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]).length(); if (length < 2.0){ length = 2.0; } length += perspectiveline.length(); perspectiveline.setLength(length); *assistant->sideHandles()[1] = perspectiveline.p2(); } else if (m_handleDrag == assistant->sideHandles()[2]){ QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[2]); qreal length = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]).length(); if (length<2.0){ length=2.0; } length += perspectiveline.length(); perspectiveline.setLength(length); *assistant->sideHandles()[3] = perspectiveline.p2(); } // for outer handles, only the vanishing point is translated, but only if there's an intersection. else if (m_handleDrag == assistant->sideHandles()[1]|| m_handleDrag == assistant->sideHandles()[3]){ QPointF vanishingpoint(0,0); QLineF perspectiveline = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]); QLineF perspectiveline2 = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]); if (QLineF(perspectiveline2).intersect(QLineF(perspectiveline), &vanishingpoint) != QLineF::NoIntersection){ *assistant->handles()[0] = vanishingpoint; } }// and for the vanishing point itself, only the outer handles get translated. else if (m_handleDrag == assistant->handles()[0]){ QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[0]); QLineF perspectiveline2 = QLineF(*assistant->handles()[0], *assistant->sideHandles()[2]); qreal length = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]).length(); qreal length2 = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]).length(); if (length < 2.0) { length = 2.0; } if (length2 < 2.0) { length2=2.0; } length += perspectiveline.length(); length2 += perspectiveline2.length(); perspectiveline.setLength(length); perspectiveline2.setLength(length2); *assistant->sideHandles()[1] = perspectiveline.p2(); *assistant->sideHandles()[3] = perspectiveline2.p2(); } } if (m_handleDrag && assistant->id() == "conjugate" && assistant->handles().size() >= 3 && assistant->sideHandles().size() == 8) { QSharedPointer assis = qSharedPointerCast(assistant); QList hndl = assistant->handles(); QList side_hndl = assistant->sideHandles(); KisPaintingAssistantHandleSP vp_dragged; KisPaintingAssistantHandleSP vp_opp; QPointF vp_dragged_original_pos; bool vp_is_dragged = m_handleDrag == hndl[0] || m_handleDrag == hndl[1]; bool near_handle_is_dragged = m_handleDrag == side_hndl[0] || m_handleDrag == side_hndl[2] || m_handleDrag == side_hndl[4] || m_handleDrag == side_hndl[6] ; bool far_handle_is_dragged = m_handleDrag == side_hndl[1] || m_handleDrag == side_hndl[3] || m_handleDrag == side_hndl[5] || m_handleDrag == side_hndl[7] ; // A valid horizon line will be needed for later QLineF horizon; // Here we will attempt to get a valid horizon if (vp_is_dragged) { // If either VP got dragged, we need its original location to get a correct horizon // The side handle bars can be used to derive the original VP position QLineF arm_a, arm_b; if (m_handleDrag == hndl[0]) { vp_dragged = hndl[0]; vp_opp = hndl[1]; arm_a = QLineF(*side_hndl[0],*side_hndl[1]); arm_b = QLineF(*side_hndl[4],*side_hndl[5]); } else if (m_handleDrag == hndl[1]){ vp_dragged = hndl[1]; vp_opp = hndl[0]; arm_a = QLineF(*side_hndl[3],*side_hndl[2]); arm_b = QLineF(*side_hndl[6],*side_hndl[7]); } arm_a.intersect(arm_b, &vp_dragged_original_pos); horizon = QLineF(*vp_opp, vp_dragged_original_pos); } else { // First calcuate relevant VP's new position if any far handles got dragged if (far_handle_is_dragged ){ QLineF arm_a, arm_b; QPointF vp_new_pos; KisPaintingAssistantHandleSP vp_moved; if (m_handleDrag == side_hndl[1] || m_handleDrag == side_hndl[5]) { vp_moved = hndl[0]; arm_a = QLineF(*side_hndl[0],*side_hndl[1]); arm_b = QLineF(*side_hndl[4],*side_hndl[5]); } else if (m_handleDrag == side_hndl[3] || m_handleDrag == side_hndl[7]) { vp_moved = hndl[1]; arm_a = QLineF(*side_hndl[3],*side_hndl[2]); arm_b = QLineF(*side_hndl[6],*side_hndl[7]); } arm_a.intersect(arm_b, &vp_new_pos); *vp_moved = vp_new_pos; } horizon = QLineF(*hndl[0],*hndl[1]); } QLineF vertical; QPointF cov; vertical = horizon.normalVector(); vertical.translate(*hndl[2] - vertical.p1()); horizon.intersect(vertical,&cov); const qreal radius = horizon.length() / 2.0; const qreal gap = QLineF(horizon.center(),cov).length(); vertical.translate(cov - vertical.p1()); vertical.setLength(sqrt((radius*radius) - (gap*gap))); const QPointF sp = vertical.p2(); // Set both VP's new locations if (vp_is_dragged) { QPointF new_dragged_vp; QPointF new_opp_vp; QLineF dragged_arm; QLineF opp_arm; dragged_arm = QLineF(sp,*vp_dragged); dragged_arm.intersect(horizon, &new_dragged_vp); opp_arm = dragged_arm.normalVector(); opp_arm.translate(sp - opp_arm.p1()); opp_arm.intersect(horizon, &new_opp_vp); if (new_dragged_vp == new_opp_vp) { new_opp_vp = *vp_opp; } // Several contingencies to avoid edge-case if (new_dragged_vp == cov) { opp_arm = QLineF(sp,*vp_opp); opp_arm.intersect(horizon, &new_opp_vp); dragged_arm = opp_arm.normalVector(); dragged_arm.translate(sp - dragged_arm.p1()); dragged_arm.intersect(horizon, &new_dragged_vp); if (new_dragged_vp == cov) { new_dragged_vp = vp_dragged_original_pos; if (new_dragged_vp == cov) { QLineF dst = QLineF(cov,*vp_dragged); QLineF hor = QLineF(*vp_opp,cov); hor.setLength(dst.length()); hor.translate(cov-hor.p1()); new_dragged_vp = hor.p2(); } } } *vp_dragged = new_dragged_vp; *vp_opp = new_opp_vp; } // recalculate side handle bars if (vp_is_dragged || near_handle_is_dragged) { QLineF arm_a1; QLineF arm_b1; QLineF arm_a2; QLineF arm_b2; arm_a1 = QLineF(*hndl[0], *side_hndl[0]); arm_a1.setLength(QLineF(*side_hndl[0],*side_hndl[1]).length()); arm_a1.translate(*side_hndl[0] - arm_a1.p1()); *side_hndl[1] = arm_a1.p2(); arm_b1 = QLineF(*hndl[0], *side_hndl[4]); arm_b1.setLength(QLineF(*side_hndl[4],*side_hndl[5]).length()); arm_b1.translate(*side_hndl[4] - arm_b1.p1()); *side_hndl[5] = arm_b1.p2(); arm_a2 = QLineF(*hndl[1], *side_hndl[2]); arm_a2.setLength(QLineF(*side_hndl[2],*side_hndl[3]).length()); arm_a2.translate(*side_hndl[2] - arm_a2.p1()); *side_hndl[3] = arm_a2.p2(); arm_b2 = QLineF(*hndl[1], *side_hndl[6]); arm_b2.setLength(QLineF(*side_hndl[6],*side_hndl[7]).length()); arm_b2.translate(*side_hndl[6] - arm_b2.p1()); *side_hndl[7] = arm_b2.p2(); } } } if (wasHiglightedNode && !m_higlightedNode) { m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas } } void KisAssistantTool::endPrimaryAction(KoPointerEvent *event) { setMode(KisTool::HOVER_MODE); if (m_handleDrag || m_assistantDrag) { if (m_handleDrag) { if (!(event->modifiers() & Qt::ShiftModifier) && m_handleCombine) { m_handleCombine->mergeWith(m_handleDrag); m_handleCombine->uncache(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); } m_handleDrag = m_handleCombine = 0; } else { m_assistantDrag.clear(); } dbgUI << "creating undo command..."; KUndo2Command *command = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants())); m_canvas->viewManager()->undoAdapter()->addCommand(command); dbgUI << "done"; } else if(m_internalMode == MODE_DRAGGING_TRANSLATING_TWONODES) { addAssistant(); m_internalMode = MODE_CREATION; } else { event->ignore(); } m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas } void KisAssistantTool::addAssistant() { m_canvas->paintingAssistantsDecoration()->addAssistant(m_newAssistant); KisAbstractPerspectiveGrid* grid = dynamic_cast(m_newAssistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->addPerspectiveGrid(grid); } QList assistants = m_canvas->paintingAssistantsDecoration()->assistants(); KUndo2Command *addAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(assistants), EditAssistantsCommand::ADD, assistants.indexOf(m_newAssistant)); m_canvas->viewManager()->undoAdapter()->addCommand(addAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->paintingAssistantsDecoration()->setSelectedAssistant(m_newAssistant); updateToolOptionsUI(); // vanishing point assistant will get an extra option m_newAssistant.clear(); } void KisAssistantTool::removeAssistant(KisPaintingAssistantSP assistant) { QList assistants = m_canvas->paintingAssistantsDecoration()->assistants(); KisAbstractPerspectiveGrid* grid = dynamic_cast(assistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->removePerspectiveGrid(grid); } m_canvas->paintingAssistantsDecoration()->removeAssistant(assistant); KUndo2Command *removeAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants()), EditAssistantsCommand::REMOVE, assistants.indexOf(assistant)); m_canvas->viewManager()->undoAdapter()->addCommand(removeAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } void KisAssistantTool::assistantSelected(KisPaintingAssistantSP assistant) { m_canvas->paintingAssistantsDecoration()->setSelectedAssistant(assistant); updateToolOptionsUI(); } void KisAssistantTool::updateToolOptionsUI() { KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); bool hasActiveAssistant = m_selectedAssistant ? true : false; if (m_selectedAssistant) { bool isVanishingPointAssistant = m_selectedAssistant->id() == "vanishing point"; m_options.vanishingPointAngleSpinbox->setVisible(isVanishingPointAssistant); if (isVanishingPointAssistant) { QSharedPointer assis = qSharedPointerCast(m_selectedAssistant); m_options.vanishingPointAngleSpinbox->setValue(assis->referenceLineDensity()); } // load custom color settings from assistant (this happens when changing assistant m_options.useCustomAssistantColor->setChecked(m_selectedAssistant->useCustomColor()); m_options.customAssistantColorButton->setColor(m_selectedAssistant->assistantCustomColor()); double opacity = (double)m_selectedAssistant->assistantCustomColor().alpha()/(double)255.00 * (double)100.00 ; opacity = ceil(opacity); // helps keep the 0-100% slider from shifting m_options.customColorOpacitySlider->blockSignals(true); m_options.customColorOpacitySlider->setValue((double)opacity); m_options.customColorOpacitySlider->blockSignals(false); } else { m_options.vanishingPointAngleSpinbox->setVisible(false); // } // show/hide elements if an assistant is selected or not m_options.assistantsGlobalOpacitySlider->setVisible(hasActiveAssistant); m_options.assistantsColor->setVisible(hasActiveAssistant); m_options.globalColorLabel->setVisible(hasActiveAssistant); m_options.useCustomAssistantColor->setVisible(hasActiveAssistant); // hide custom color options if use custom color is not selected bool showCustomColorSettings = m_options.useCustomAssistantColor->isChecked() && hasActiveAssistant; m_options.customColorOpacitySlider->setVisible(showCustomColorSettings); m_options.customAssistantColorButton->setVisible(showCustomColorSettings); // disable global color settings if we are using the custom color m_options.assistantsGlobalOpacitySlider->setEnabled(!showCustomColorSettings); m_options.assistantsColor->setEnabled(!showCustomColorSettings); m_options.globalColorLabel->setEnabled(!showCustomColorSettings); } void KisAssistantTool::slotChangeVanishingPointAngle(double value) { if ( m_canvas->paintingAssistantsDecoration()->assistants().length() == 0) { return; } // get the selected assistant and change the angle value KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { bool isVanishingPointAssistant = m_selectedAssistant->id() == "vanishing point"; if (isVanishingPointAssistant) { QSharedPointer assis = qSharedPointerCast(m_selectedAssistant); assis->setReferenceLineDensity((float)value); } } m_canvas->canvasWidget()->update(); } void KisAssistantTool::mouseMoveEvent(KoPointerEvent *event) { if (m_newAssistant && m_internalMode == MODE_CREATION) { *m_newAssistant->handles().back() = event->point; } else if (m_newAssistant && m_internalMode == MODE_DRAGGING_TRANSLATING_TWONODES) { QPointF translate = event->point - m_dragEnd; m_dragEnd = event->point; m_selectedNode1.data()->operator = (QPointF(m_selectedNode1.data()->x(),m_selectedNode1.data()->y()) + translate); m_selectedNode2.data()->operator = (QPointF(m_selectedNode2.data()->x(),m_selectedNode2.data()->y()) + translate); } m_canvas->updateCanvas(); } void KisAssistantTool::paint(QPainter& _gc, const KoViewConverter &_converter) { QRectF canvasSize = QRectF(QPointF(0, 0), QSizeF(m_canvas->image()->size())); // show special display while a new assistant is in the process of being created if (m_newAssistant) { QColor assistantColor = m_newAssistant->effectiveAssistantColor(); assistantColor.setAlpha(80); m_newAssistant->drawAssistant(_gc, canvasSize, m_canvas->coordinatesConverter(), false, m_canvas, true, false); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_newAssistant->handles()) { QPainterPath path; path.addEllipse(QRectF(_converter.documentToView(*handle) - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize))); _gc.save(); _gc.setPen(Qt::NoPen); _gc.setBrush(assistantColor); _gc.drawPath(path); _gc.restore(); } } Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { QColor assistantColor = assistant->effectiveAssistantColor(); assistantColor.setAlpha(80); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { QRectF ellipse(_converter.documentToView(*handle) - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize)); // render handles differently if it is the one being dragged. if (handle == m_handleDrag || handle == m_handleCombine) { QPen stroke(assistantColor, 4); _gc.save(); _gc.setPen(stroke); _gc.setBrush(Qt::NoBrush); _gc.drawEllipse(ellipse); _gc.restore(); } } } } void KisAssistantTool::removeAllAssistants() { m_origAssistantList = m_canvas->paintingAssistantsDecoration()->assistants(); m_canvas->viewManager()->canvasResourceProvider()->clearPerspectiveGrids(); m_canvas->paintingAssistantsDecoration()->removeAll(); KUndo2Command *removeAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants())); m_canvas->viewManager()->undoAdapter()->addCommand(removeAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->updateCanvas(); m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } void KisAssistantTool::loadAssistants() { KoFileDialog dialog(m_canvas->viewManager()->mainWindow(), KoFileDialog::OpenFile, "OpenAssistant"); dialog.setCaption(i18n("Select an Assistant")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-assistant", "application/x-krita-assistant"); QString filename = dialog.filename(); if (filename.isEmpty()) return; if (!QFileInfo(filename).exists()) return; QFile file(filename); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); QXmlStreamReader xml(data); QMap handleMap; QMap sideHandleMap; KisPaintingAssistantSP assistant; bool errors = false; while (!xml.atEnd()) { switch (xml.readNext()) { case QXmlStreamReader::StartElement: if (xml.name() == "handle") { if (assistant && !xml.attributes().value("ref").isEmpty()) { KisPaintingAssistantHandleSP handle = handleMap.value(xml.attributes().value("ref").toString().toInt()); if (handle) { assistant->addHandle(handle, HandleType::NORMAL); } else { errors = true; } } else { QString strId = xml.attributes().value("id").toString(), strX = xml.attributes().value("x").toString(), strY = xml.attributes().value("y").toString(); if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { int id = strId.toInt(); double x = strX.toDouble(), y = strY.toDouble(); if (!handleMap.contains(id)) { handleMap.insert(id, new KisPaintingAssistantHandle(x, y)); } else { errors = true; } } else { errors = true; } } // for vanishing point assistant } else if (xml.name() == "sidehandle"){ // read in sidehandles if (!xml.attributes().value("id").isEmpty()) { - QString strId = xml.attributes().value("id").toString(), - strX = xml.attributes().value("x").toString(), - strY = xml.attributes().value("y").toString(); - if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { - int id = strId.toInt(); - double x = strX.toDouble(); - double y = strY.toDouble(); - if (!sideHandleMap.contains(id)) { - sideHandleMap.insert(id, new KisPaintingAssistantHandle(x,y)); - }} + QString strId = xml.attributes().value("id").toString(), + strX = xml.attributes().value("x").toString(), + strY = xml.attributes().value("y").toString(); + if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { + int id = strId.toInt(); + double x = strX.toDouble(); + double y = strY.toDouble(); + if (!sideHandleMap.contains(id)) { + sideHandleMap.insert(id, new KisPaintingAssistantHandle(x,y)); + }} } // addHandle to assistant if (!xml.attributes().value("ref").isEmpty() && assistant) { - KisPaintingAssistantHandleSP handle = sideHandleMap.value(xml.attributes().value("ref").toString().toInt()); - if (handle) { - assistant->addHandle(handle, HandleType::SIDE); - } + KisPaintingAssistantHandleSP handle = sideHandleMap.value(xml.attributes().value("ref").toString().toInt()); + if (handle) { + assistant->addHandle(handle, HandleType::SIDE); + } } } else if (xml.name() == "assistant") { const KisPaintingAssistantFactory* factory = KisPaintingAssistantFactoryRegistry::instance()->get(xml.attributes().value("type").toString()); if (factory) { if (assistant) { errors = true; assistant.clear(); } assistant = toQShared(factory->createPaintingAssistant()); } else { errors = true; } if (assistant) { // load custom shared assistant properties if (xml.attributes().hasAttribute("useCustomColor")) { QStringRef useCustomColor = xml.attributes().value("useCustomColor"); bool usingColor = false; if (useCustomColor.toString() == "1") { usingColor = true; } assistant->setUseCustomColor(usingColor); } if ( xml.attributes().hasAttribute("useCustomColor")) { QStringRef customColor = xml.attributes().value("customColor"); assistant->setAssistantCustomColor( KisDomUtils::qStringToQColor(customColor.toString()) ); } } } if (assistant) { assistant->loadCustomXml(&xml); } break; case QXmlStreamReader::EndElement: if (xml.name() == "assistant") { if (assistant) { if (assistant->handles().size() == assistant->numHandles()) { if (assistant->id() == "vanishing point" && sideHandleMap.empty()){ // Create side handles if the saved vp assistant doesn't have any. QPointF pos = *assistant->handles()[0]; assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(-70,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(-140,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(70,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(140,0)), HandleType::SIDE); } m_canvas->paintingAssistantsDecoration()->addAssistant(assistant); KisAbstractPerspectiveGrid* grid = dynamic_cast(assistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->addPerspectiveGrid(grid); } } else { errors = true; } assistant.clear(); } } break; default: break; } } if (assistant) { errors = true; assistant.clear(); } if (xml.hasError()) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), xml.errorString()); } if (errors) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Errors were encountered. Not all assistants were successfully loaded.")); } m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->updateCanvas(); } void KisAssistantTool::saveAssistants() { if (m_handles.isEmpty()) return; QByteArray data; QXmlStreamWriter xml(&data); xml.writeStartDocument(); xml.writeStartElement("paintingassistant"); xml.writeAttribute("color", KisDomUtils::qColorToQString( m_canvas->paintingAssistantsDecoration()->globalAssistantsColor())); // global color if no custom color used xml.writeStartElement("handles"); QMap handleMap; Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { int id = handleMap.size(); handleMap.insert(handle, id); xml.writeStartElement("handle"); //xml.writeAttribute("type", handle->handleType()); xml.writeAttribute("id", QString::number(id)); xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); xml.writeEndElement(); } xml.writeEndElement(); xml.writeStartElement("sidehandles"); QMap sideHandleMap; Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { - Q_FOREACH (KisPaintingAssistantHandleSP handle, assistant->sideHandles()) { - int id = sideHandleMap.size(); - sideHandleMap.insert(handle, id); - xml.writeStartElement("sidehandle"); - xml.writeAttribute("id", QString::number(id)); - xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); - xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); - xml.writeEndElement(); - } + Q_FOREACH (KisPaintingAssistantHandleSP handle, assistant->sideHandles()) { + int id = sideHandleMap.size(); + sideHandleMap.insert(handle, id); + xml.writeStartElement("sidehandle"); + xml.writeAttribute("id", QString::number(id)); + xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); + xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); + xml.writeEndElement(); + } } xml.writeStartElement("assistants"); Q_FOREACH (const KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { xml.writeStartElement("assistant"); xml.writeAttribute("type", assistant->id()); xml.writeAttribute("useCustomColor", QString::number(assistant->useCustomColor())); xml.writeAttribute("customColor", KisDomUtils::qColorToQString(assistant->assistantCustomColor())); // custom assistant properties like angle density on vanishing point assistant->saveCustomXml(&xml); // handle information xml.writeStartElement("handles"); Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) { xml.writeStartElement("handle"); xml.writeAttribute("ref", QString::number(handleMap.value(handle))); xml.writeEndElement(); } xml.writeEndElement(); if (!sideHandleMap.empty()) { - xml.writeStartElement("sidehandles"); - Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) { - xml.writeStartElement("sidehandle"); - xml.writeAttribute("ref", QString::number(sideHandleMap.value(handle))); - xml.writeEndElement(); - } - xml.writeEndElement(); + xml.writeStartElement("sidehandles"); + Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) { + xml.writeStartElement("sidehandle"); + xml.writeAttribute("ref", QString::number(sideHandleMap.value(handle))); + xml.writeEndElement(); + } + xml.writeEndElement(); } xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndElement(); xml.writeEndDocument(); KoFileDialog dialog(m_canvas->viewManager()->mainWindow(), KoFileDialog::SaveFile, "OpenAssistant"); dialog.setCaption(i18n("Save Assistant")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-assistant", "application/x-krita-assistant"); QString filename = dialog.filename(); if (filename.isEmpty()) return; QFile file(filename); file.open(QIODevice::WriteOnly); file.write(data); } QWidget *KisAssistantTool::createOptionWidget() { if (!m_optionsWidget) { m_optionsWidget = new QWidget; m_options.setupUi(m_optionsWidget); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_options.loadAssistantButton->setIcon(KisIconUtils::loadIcon("document-open")); m_options.saveAssistantButton->setIcon(KisIconUtils::loadIcon("document-save")); m_options.deleteAllAssistantsButton->setIcon(KisIconUtils::loadIcon("edit-delete")); QList assistants; Q_FOREACH (const QString& key, KisPaintingAssistantFactoryRegistry::instance()->keys()) { QString name = KisPaintingAssistantFactoryRegistry::instance()->get(key)->name(); assistants << KoID(key, name); } std::sort(assistants.begin(), assistants.end(), KoID::compareNames); Q_FOREACH(const KoID &id, assistants) { m_options.availableAssistantsComboBox->addItem(id.name(), id.id()); } connect(m_options.saveAssistantButton, SIGNAL(clicked()), SLOT(saveAssistants())); connect(m_options.loadAssistantButton, SIGNAL(clicked()), SLOT(loadAssistants())); connect(m_options.deleteAllAssistantsButton, SIGNAL(clicked()), SLOT(removeAllAssistants())); connect(m_options.assistantsColor, SIGNAL(changed(QColor)), SLOT(slotGlobalAssistantsColorChanged(QColor))); connect(m_options.assistantsGlobalOpacitySlider, SIGNAL(valueChanged(int)), SLOT(slotGlobalAssistantOpacityChanged())); connect(m_options.vanishingPointAngleSpinbox, SIGNAL(valueChanged(double)), this, SLOT(slotChangeVanishingPointAngle(double))); // initialize UI elements with existing data if possible if (m_canvas && m_canvas->paintingAssistantsDecoration()) { const QColor color = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor(); QColor opaqueColor = color; opaqueColor.setAlpha(255); m_options.assistantsColor->setColor(opaqueColor); m_options.customAssistantColorButton->setColor(opaqueColor); m_options.assistantsGlobalOpacitySlider->setValue(color.alphaF() * 100.0); } else { m_options.assistantsColor->setColor(QColor(176, 176, 176, 255)); // grey default for all assistants m_options.assistantsGlobalOpacitySlider->setValue(100); // 100% } m_options.assistantsGlobalOpacitySlider->setPrefix(i18n("Opacity: ")); m_options.assistantsGlobalOpacitySlider->setSuffix(" %"); // custom color of selected assistant m_options.customColorOpacitySlider->setValue(100); // 100% m_options.customColorOpacitySlider->setPrefix(i18n("Opacity: ")); m_options.customColorOpacitySlider->setSuffix(" %"); connect(m_options.useCustomAssistantColor, SIGNAL(clicked(bool)), this, SLOT(slotUpdateCustomColor())); connect(m_options.customAssistantColorButton, SIGNAL(changed(QColor)), this, SLOT(slotUpdateCustomColor())); connect(m_options.customColorOpacitySlider, SIGNAL(valueChanged(int)), SLOT(slotCustomOpacityChanged())); m_options.vanishingPointAngleSpinbox->setPrefix(i18n("Density: ")); m_options.vanishingPointAngleSpinbox->setSuffix(QChar(Qt::Key_degree)); m_options.vanishingPointAngleSpinbox->setRange(1.0, 180.0); m_options.vanishingPointAngleSpinbox->setSingleStep(1.0); m_options.vanishingPointAngleSpinbox->setVisible(false); } updateToolOptionsUI(); return m_optionsWidget; } void KisAssistantTool::slotGlobalAssistantsColorChanged(const QColor& setColor) { // color and alpha are stored separately, so we need to merge the values before sending it on int oldAlpha = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor().alpha(); QColor newColor = setColor; newColor.setAlpha(oldAlpha); m_canvas->paintingAssistantsDecoration()->setGlobalAssistantsColor(newColor); m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotGlobalAssistantOpacityChanged() { QColor newColor = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor(); qreal newOpacity = m_options.assistantsGlobalOpacitySlider->value() * 0.01 * 255.0; newColor.setAlpha(int(newOpacity)); m_canvas->paintingAssistantsDecoration()->setGlobalAssistantsColor(newColor); m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotUpdateCustomColor() { // get the selected assistant and change the angle value KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { m_selectedAssistant->setUseCustomColor(m_options.useCustomAssistantColor->isChecked()); // changing color doesn't keep alpha, so update that before we send it on QColor newColor = m_options.customAssistantColorButton->color(); newColor.setAlpha(m_selectedAssistant->assistantCustomColor().alpha()); m_selectedAssistant->setAssistantCustomColor(newColor); m_selectedAssistant->uncache(); } updateToolOptionsUI(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotCustomOpacityChanged() { KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { QColor newColor = m_selectedAssistant->assistantCustomColor(); qreal newOpacity = m_options.customColorOpacitySlider->value() * 0.01 * 255.0; newColor.setAlpha(int(newOpacity)); m_selectedAssistant->setAssistantCustomColor(newColor); m_selectedAssistant->uncache(); } // this forces the canvas to refresh to see the changes immediately m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index a4318146dd..4418654766 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,532 +1,551 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_time_based_item_model.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_animation_player.h" #include "kis_signal_compressor_with_param.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_animation_utils.h" #include "kis_keyframe_channel.h" #include "kis_processing_applicator.h" #include "KisImageBarrierLockerWithFeedback.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_command_utils.h" #include "KisPart.h" #include "kis_animation_cache_populator.h" struct KisTimeBasedItemModel::Private { Private() : animationPlayer(0) , numFramesOverride(0) , activeFrameIndex(0) , scrubInProgress(false) , scrubStartFrame(-1) {} KisImageWSP image; KisAnimationFrameCacheWSP framesCache; QPointer animationPlayer; QVector cachedFrames; int numFramesOverride; int activeFrameIndex; bool scrubInProgress; int scrubStartFrame; QScopedPointer > scrubbingCompressor; int baseNumFrames() const { auto imageSP = image.toStrongRef(); if (!imageSP) return 0; KisImageAnimationInterface *i = imageSP->animationInterface(); if (!i) return 1; return i->totalLength(); } int effectiveNumFrames() const { if (image.isNull()) return 0; return qMax(baseNumFrames(), numFramesOverride); } int framesPerSecond() { return image->animationInterface()->framerate(); } }; KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent) : QAbstractTableModel(parent) , m_d(new Private()) { KisConfig cfg(true); using namespace std::placeholders; std::function callback( std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1)); m_d->scrubbingCompressor.reset( new KisSignalCompressorWithParam(cfg.scrubbingUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE)); } KisTimeBasedItemModel::~KisTimeBasedItemModel() {} void KisTimeBasedItemModel::setImage(KisImageWSP image) { KisImageWSP oldImage = m_d->image; m_d->image = image; if (image) { KisImageAnimationInterface *ai = image->animationInterface(); connect(ai, SIGNAL(sigFramerateChanged()), SLOT(slotFramerateChanged())); connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); + connect(ai, SIGNAL(sigFullClipRangeChanged()), SLOT(slotClipRangeChanged())); } if (image != oldImage) { beginResetModel(); endResetModel(); } } void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache) { if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return; if (m_d->framesCache) { m_d->framesCache->disconnect(this); } m_d->framesCache = cache; if (m_d->framesCache) { connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged())); } } void KisTimeBasedItemModel::setAnimationPlayer(KisAnimationPlayer *player) { if (m_d->animationPlayer == player) return; if (m_d->animationPlayer) { m_d->animationPlayer->disconnect(this); } m_d->animationPlayer = player; if (m_d->animationPlayer) { connect(m_d->animationPlayer, SIGNAL(sigPlaybackStopped()), SLOT(slotPlaybackStopped())); connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged())); } } void KisTimeBasedItemModel::setLastVisibleFrame(int time) { - const int growThreshold = m_d->effectiveNumFrames() - 3; + const int growThreshold = m_d->effectiveNumFrames() - 1; const int growValue = time + 8; - const int shrinkThreshold = m_d->effectiveNumFrames() - 12; + const int shrinkThreshold = m_d->effectiveNumFrames() - 3; const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold)); - const bool canShrink = m_d->effectiveNumFrames() > m_d->baseNumFrames(); + const bool canShrink = m_d->baseNumFrames() < m_d->effectiveNumFrames(); if (time >= growThreshold) { beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1); m_d->numFramesOverride = growValue; endInsertColumns(); } else if (time < shrinkThreshold && canShrink) { beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1); m_d->numFramesOverride = shrinkValue; endRemoveColumns(); } } int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->effectiveNumFrames(); } QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const { switch (role) { case ActiveFrameRole: { return index.column() == m_d->activeFrameIndex; } } return QVariant(); } bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; switch (role) { case ActiveFrameRole: { setHeaderData(index.column(), Qt::Horizontal, value, role); break; } } return false; } QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: return section == m_d->activeFrameIndex; case FrameCachedRole: return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false; case FramesPerSecondRole: return m_d->framesPerSecond(); } } return QVariant(); } bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: if (value.toBool() && section != m_d->activeFrameIndex) { int prevFrame = m_d->activeFrameIndex; m_d->activeFrameIndex = section; scrubTo(m_d->activeFrameIndex, m_d->scrubInProgress); /** * Optimization Hack Alert: * * ideally, we should emit all four signals, but... The * point is this code is used in a tight loop during * playback, so it should run as fast as possible. To tell * the story short, commenting out these three lines makes * playback run 15% faster ;) */ if (m_d->scrubInProgress) { //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } else { emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } } } } return false; } bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes) { KisAnimationUtils::FrameItemList frameItems; { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indexes) { int time = index.column(); Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) { if (channel->keyframeAt(time)) { frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); } } } } if (frameItems.isEmpty()) return false; KisAnimationUtils::removeKeyframes(m_d->image, frameItems); return true; } KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand) { if (srcIndexes.isEmpty()) return 0; if (offset.isNull()) return 0; KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset); KisAnimationUtils::FrameItemList srcFrameItems; KisAnimationUtils::FrameItemList dstFrameItems; Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) { QModelIndex dstIndex = index( srcIndex.row() + offset.y(), srcIndex.column() + offset.x()); KisNodeSP srcNode = nodeAt(srcIndex); KisNodeSP dstNode = nodeAt(dstIndex); if (!srcNode || !dstNode) return 0; Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) { if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) { srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column()); dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column()); } } } KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0); if (srcFrameItems.isEmpty()) return 0; return KisAnimationUtils::createMoveKeyframesCommand(srcFrameItems, dstFrameItems, copyFrames, moveEmptyFrames, parentCommand); } bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indicesToRemove) { if (indicesToRemove.isEmpty()) return true; std::sort(indicesToRemove.begin(), indicesToRemove.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() > rhs.column(); }); const int minColumn = indicesToRemove.last().column(); KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size())); { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indicesToRemove) { QModelIndexList indicesToOffset; for (int column = index.column() + 1; column < columnCount(); column++) { indicesToOffset << this->index(index.row(), column); } createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand); } const int oldTime = m_d->image->animationInterface()->currentUITime(); const int newTime = minColumn; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } bool KisTimeBasedItemModel::mirrorFrames(QModelIndexList indexes) { QScopedPointer parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames"))); { KisImageBarrierLockerWithFeedback locker(m_d->image); QMap rowsList; Q_FOREACH (const QModelIndex &index, indexes) { rowsList[index.row()].append(index); } Q_FOREACH (int row, rowsList.keys()) { QModelIndexList &list = rowsList[row]; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false); std::sort(list.begin(), list.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() < rhs.column(); }); auto srcIt = list.begin(); auto dstIt = list.end(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false); --dstIt; QList channels = channelsAt(*srcIt).values(); while (srcIt < dstIt) { Q_FOREACH (KisKeyframeChannel *channel, channels) { channel->swapFrames(srcIt->column(), dstIt->column(), parentCommand.data()); } srcIt++; dstIt--; } } } KisProcessingApplicator::runSingleCommandStroke(m_d->image, new KisCommandUtils::SkipFirstRedoWrapper(parentCommand.take()), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } void KisTimeBasedItemModel::slotInternalScrubPreviewRequested(int time) { if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->displayFrame(time); } } void KisTimeBasedItemModel::setScrubState(bool active) { if (!m_d->scrubInProgress && active) { const int currentFrame = m_d->image->animationInterface()->currentUITime(); const bool hasCurrentFrameInCache = m_d->framesCache->frameStatus(currentFrame) == KisAnimationFrameCache::Cached; if(!hasCurrentFrameInCache) { KisPart::instance()->prioritizeFrameForCache(m_d->image, currentFrame); } m_d->scrubStartFrame = m_d->activeFrameIndex; m_d->scrubInProgress = true; } if (m_d->scrubInProgress && !active) { m_d->scrubInProgress = false; if (m_d->scrubStartFrame >= 0 && m_d->scrubStartFrame != m_d->activeFrameIndex) { scrubTo(m_d->activeFrameIndex, false); } m_d->scrubStartFrame = -1; } } +bool KisTimeBasedItemModel::isScrubbing() +{ + return m_d->scrubInProgress; +} + void KisTimeBasedItemModel::scrubTo(int time, bool preview) { if (m_d->animationPlayer && m_d->animationPlayer->isPlaying()) return; KIS_ASSERT_RECOVER_RETURN(m_d->image); if (preview) { if (m_d->animationPlayer) { m_d->scrubbingCompressor->start(time); } } else { m_d->image->animationInterface()->requestTimeSwitchWithUndo(time); } } void KisTimeBasedItemModel::slotCurrentTimeChanged(int time) { if (time != m_d->activeFrameIndex) { setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole); } } void KisTimeBasedItemModel::slotFramerateChanged() { emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); } +void KisTimeBasedItemModel::slotClipRangeChanged() +{ + if (m_d->image && m_d->image->animationInterface() ) { + const KisImageAnimationInterface* const interface = m_d->image->animationInterface(); + const int lastFrame = interface->playbackRange().end(); + if ( lastFrame > m_d->numFramesOverride) { + beginInsertColumns(QModelIndex(), m_d->numFramesOverride, interface->playbackRange().end()); + m_d->numFramesOverride = interface->playbackRange().end(); + endInsertColumns(); + } + } +} + void KisTimeBasedItemModel::slotCacheChanged() { const int numFrames = columnCount(); m_d->cachedFrames.resize(numFrames); for (int i = 0; i < numFrames; i++) { m_d->cachedFrames[i] = m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached; } emit headerDataChanged(Qt::Horizontal, 0, numFrames); } void KisTimeBasedItemModel::slotPlaybackFrameChanged() { if (!m_d->animationPlayer->isPlaying()) return; setData(index(0, m_d->animationPlayer->visibleFrame()), true, ActiveFrameRole); } void KisTimeBasedItemModel::slotPlaybackStopped() { setData(index(0, m_d->image->animationInterface()->currentUITime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::setPlaybackRange(const KisTimeRange &range) { if (m_d->image.isNull()) return; KisImageAnimationInterface *i = m_d->image->animationInterface(); i->setPlaybackRange(range); } bool KisTimeBasedItemModel::isPlaybackActive() const { return m_d->animationPlayer && m_d->animationPlayer->isPlaying(); } bool KisTimeBasedItemModel::isPlaybackPaused() const { return m_d->animationPlayer && m_d->animationPlayer->isPaused(); } void KisTimeBasedItemModel::stopPlayback() const { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->animationPlayer); m_d->animationPlayer->stop(); } int KisTimeBasedItemModel::currentTime() const { return m_d->image->animationInterface()->currentUITime(); } KisImageWSP KisTimeBasedItemModel::image() const { return m_d->image; } diff --git a/plugins/dockers/animation/kis_time_based_item_model.h b/plugins/dockers/animation/kis_time_based_item_model.h index 5e3dc7b33f..bb6d002257 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.h +++ b/plugins/dockers/animation/kis_time_based_item_model.h @@ -1,107 +1,109 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_TIME_BASED_ITEM_MODEL_H #define _KIS_TIME_BASED_ITEM_MODEL_H #include #include #include #include "kritaanimationdocker_export.h" #include "kis_types.h" class KisTimeRange; class KisAnimationPlayer; class KisKeyframeChannel; class KRITAANIMATIONDOCKER_EXPORT KisTimeBasedItemModel : public QAbstractTableModel { Q_OBJECT public: KisTimeBasedItemModel(QObject *parent); ~KisTimeBasedItemModel() override; void setImage(KisImageWSP image); void setFrameCache(KisAnimationFrameCacheSP cache); void setAnimationPlayer(KisAnimationPlayer *player); void setLastVisibleFrame(int time); int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override; bool removeFrames(const QModelIndexList &indexes); bool removeFramesAndOffset(QModelIndexList indicesToRemove); bool mirrorFrames(QModelIndexList indexes); void setScrubState(bool active); + bool isScrubbing(); void scrubTo(int time, bool preview); void setPlaybackRange(const KisTimeRange &range); bool isPlaybackActive() const; bool isPlaybackPaused() const; void stopPlayback() const; int currentTime() const; enum ItemDataRole { ActiveFrameRole = Qt::UserRole + 101, FrameExistsRole, SpecialKeyframeExists, FrameCachedRole, FrameEditableRole, FramesPerSecondRole, UserRole, FrameHasContent // is it an empty frame with nothing in it? }; protected: virtual KisNodeSP nodeAt(QModelIndex index) const = 0; virtual QMap channelsAt(QModelIndex index) const = 0; KisImageWSP image() const; KUndo2Command* createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); protected Q_SLOTS: void slotCurrentTimeChanged(int time); private Q_SLOTS: void slotFramerateChanged(); + void slotClipRangeChanged(); void slotCacheChanged(); void slotInternalScrubPreviewRequested(int time); void slotPlaybackFrameChanged(); void slotPlaybackStopped(); private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/onion_skins_docker.cpp b/plugins/dockers/animation/onion_skins_docker.cpp index 309c68c67b..2d58854ed9 100644 --- a/plugins/dockers/animation/onion_skins_docker.cpp +++ b/plugins/dockers/animation/onion_skins_docker.cpp @@ -1,271 +1,237 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "onion_skins_docker.h" #include "ui_onion_skins_docker.h" #include #include #include +#include #include "kis_icon_utils.h" #include "kis_image_config.h" #include "kis_onion_skin_compositor.h" #include "kis_signals_blocker.h" #include "kis_node_view_color_scheme.h" #include "KisViewManager.h" #include "kis_action_manager.h" #include "kis_action.h" #include #include "kis_equalizer_widget.h" +#include "kis_color_label_button.h" OnionSkinsDocker::OnionSkinsDocker(QWidget *parent) : QDockWidget(i18n("Onion Skins"), parent), ui(new Ui::OnionSkinsDocker), m_updatesCompressor(300, KisSignalCompressor::FIRST_ACTIVE), m_toggleOnionSkinsAction(0) { QWidget* mainWidget = new QWidget(this); setWidget(mainWidget); KisImageConfig config(true); ui->setupUi(mainWidget); mainWidget->setContentsMargins(10, 10, 10, 10); ui->doubleTintFactor->setMinimum(0); ui->doubleTintFactor->setMaximum(100); ui->doubleTintFactor->setPrefix(i18n("Tint: ")); ui->doubleTintFactor->setSuffix(i18n("%")); ui->btnBackwardColor->setToolTip(i18n("Tint color for past frames")); ui->btnForwardColor->setToolTip(i18n("Tint color for future frames")); QVBoxLayout *layout = ui->slidersLayout; m_equalizerWidget = new KisEqualizerWidget(10, this); connect(m_equalizerWidget, SIGNAL(sigConfigChanged()), &m_updatesCompressor, SLOT(start())); layout->addWidget(m_equalizerWidget, 1); connect(ui->btnBackwardColor, SIGNAL(changed(KoColor)), &m_updatesCompressor, SLOT(start())); connect(ui->btnForwardColor, SIGNAL(changed(KoColor)), &m_updatesCompressor, SLOT(start())); connect(ui->doubleTintFactor, SIGNAL(valueChanged(qreal)), &m_updatesCompressor, SLOT(start())); connect(&m_updatesCompressor, SIGNAL(timeout()), SLOT(changed())); { const bool isShown = config.showAdditionalOnionSkinsSettings(); ui->btnShowHide->setChecked(isShown); connect(ui->btnShowHide, SIGNAL(toggled(bool)), SLOT(slotShowAdditionalSettings(bool))); slotShowAdditionalSettings(isShown); } - // create colored checkboxes for onion skin filtering - KisNodeViewColorScheme scm; - QPalette filterColorPalette; - QPixmap iconPixmap(10, 10); - - //iconPixmap.fill(scm.colorLabel(0)); - //ui->colorFilter0_checkbox->setIcon(iconPixmap); // default(no) color - - iconPixmap.fill(scm.colorLabel(1)); - ui->colorFilter1_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(2)); - ui->colorFilter2_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(3)); - ui->colorFilter3_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(4)); - ui->colorFilter4_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(5)); - ui->colorFilter5_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(6)); - ui->colorFilter6_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(7)); - ui->colorFilter7_checkbox->setIcon(QIcon(iconPixmap)); - - iconPixmap.fill(scm.colorLabel(8)); - ui->colorFilter8_checkbox->setIcon(QIcon(iconPixmap)); - - - // assign click events to color filters and group checkbox - connect(ui->colorFilter0_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter1_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter2_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter3_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter4_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter5_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter6_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter7_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilter8_checkbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); - connect(ui->colorFilterGroupbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); + { + KisNodeViewColorScheme scm; + m_filterButtonGroup = new KisColorLabelFilterGroup(this); + m_dragFilter = new KisColorLabelMouseDragFilter(this); + m_filterButtonGroup->setExclusive(false); + m_filterButtonGroup->setMinimumRequiredChecked(0); + QWidget* filterButtonContainer = ui->colorFilterGroupbox; + QLayout* filterButtonLayout = ui->filterButtons; + filterButtonLayout->setSpacing(0); + QVector availableColors = scm.allColorLabels(); + QSet viableColors; + for (int i = 0; i < availableColors.count(); i++) { + KisColorLabelButton* colorLabelButton = new KisColorLabelButton(availableColors[i], 24, filterButtonContainer); + filterButtonLayout->addWidget(colorLabelButton); + m_filterButtonGroup->addButton(colorLabelButton, i); + colorLabelButton->installEventFilter(m_dragFilter); + viableColors << i; + } + + m_filterButtonGroup->setViableLabels(viableColors); + + connect(m_filterButtonGroup, SIGNAL(buttonToggled(int,bool)), this, SLOT(slotFilteredColorsChanged())); + connect(ui->colorFilterGroupbox, SIGNAL(toggled(bool)), this, SLOT(slotFilteredColorsChanged())); + connect(ui->resetFilter, SIGNAL(pressed()), m_filterButtonGroup, SLOT(reset()) ); + } loadSettings(); KisOnionSkinCompositor::instance()->configChanged(); // this mostly hides the checkboxes since no filtering is done by default slotFilteredColorsChanged(); resize(sizeHint()); } OnionSkinsDocker::~OnionSkinsDocker() { delete ui; } void OnionSkinsDocker::setCanvas(KoCanvasBase *canvas) { Q_UNUSED(canvas); } void OnionSkinsDocker::unsetCanvas() { } void OnionSkinsDocker::setViewManager(KisViewManager *view) { KisActionManager *actionManager = view->actionManager(); m_toggleOnionSkinsAction = actionManager->createAction("toggle_onion_skin"); connect(m_toggleOnionSkinsAction, SIGNAL(triggered()), SLOT(slotToggleOnionSkins())); slotUpdateIcons(); connect(view->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateIcons())); } void OnionSkinsDocker::slotToggleOnionSkins() { m_equalizerWidget->toggleMasterSwitch(); } void OnionSkinsDocker::slotFilteredColorsChanged() { // what colors are selected to filter?? - QList selectedFilterColors; - - if (ui->colorFilter0_checkbox->isChecked()) selectedFilterColors << 0; - if (ui->colorFilter1_checkbox->isChecked()) selectedFilterColors << 1; - if (ui->colorFilter2_checkbox->isChecked()) selectedFilterColors << 2; - if (ui->colorFilter3_checkbox->isChecked()) selectedFilterColors << 3; - if (ui->colorFilter4_checkbox->isChecked()) selectedFilterColors << 4; - if (ui->colorFilter5_checkbox->isChecked()) selectedFilterColors << 5; - if (ui->colorFilter6_checkbox->isChecked()) selectedFilterColors << 6; - if (ui->colorFilter7_checkbox->isChecked()) selectedFilterColors << 7; - if (ui->colorFilter8_checkbox->isChecked()) selectedFilterColors << 8; + QSet selectedFilterColors = m_filterButtonGroup->getActiveLabels(); // show all colors if the filter is off and ignore the checkboxes if(ui->colorFilterGroupbox->isChecked() == false) { selectedFilterColors.clear(); selectedFilterColors << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8; // show everything } - ui->colorFilter0_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter1_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter2_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter3_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter4_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter5_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter6_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter7_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); - ui->colorFilter8_checkbox->setVisible(ui->colorFilterGroupbox->isChecked()); + m_filterButtonGroup->setAllVisibility(ui->colorFilterGroupbox->isChecked()); + ui->resetFilter->setVisible(ui->colorFilterGroupbox->isChecked()); // existing code - KisOnionSkinCompositor::instance()->setColorLabelFilter(selectedFilterColors); + KisOnionSkinCompositor::instance()->setColorLabelFilter(QList::fromSet(selectedFilterColors)); KisOnionSkinCompositor::instance()->configChanged(); } void OnionSkinsDocker::slotUpdateIcons() { if (m_toggleOnionSkinsAction) { m_toggleOnionSkinsAction->setIcon(KisIconUtils::loadIcon("onion_skin_options")); } } void OnionSkinsDocker::slotShowAdditionalSettings(bool value) { ui->lblPrevColor->setVisible(value); ui->lblNextColor->setVisible(value); ui->btnBackwardColor->setVisible(value); ui->btnForwardColor->setVisible(value); ui->doubleTintFactor->setVisible(value); QIcon icon = KisIconUtils::loadIcon(value ? "arrow-down" : "arrow-up"); ui->btnShowHide->setIcon(icon); KisImageConfig(false).setShowAdditionalOnionSkinsSettings(value); } void OnionSkinsDocker::changed() { KisImageConfig config(false); KisEqualizerWidget::EqualizerValues v = m_equalizerWidget->getValues(); config.setNumberOfOnionSkins(v.maxDistance); for (int i = -v.maxDistance; i <= v.maxDistance; i++) { config.setOnionSkinOpacity(i, v.value[i] * 255.0 / 100.0); config.setOnionSkinState(i, v.state[i]); } config.setOnionSkinTintFactor(ui->doubleTintFactor->value() * 255.0 / 100.0); config.setOnionSkinTintColorBackward(ui->btnBackwardColor->color().toQColor()); config.setOnionSkinTintColorForward(ui->btnForwardColor->color().toQColor()); KisOnionSkinCompositor::instance()->configChanged(); } void OnionSkinsDocker::loadSettings() { KisImageConfig config(true); KisSignalsBlocker b(ui->doubleTintFactor, ui->btnBackwardColor, ui->btnForwardColor, m_equalizerWidget); ui->doubleTintFactor->setValue(qRound(config.onionSkinTintFactor() * 100.0 / 255)); KoColor bcol(KoColorSpaceRegistry::instance()->rgb8()); bcol.fromQColor(config.onionSkinTintColorBackward()); ui->btnBackwardColor->setColor(bcol); bcol.fromQColor(config.onionSkinTintColorForward()); ui->btnForwardColor->setColor(bcol); KisEqualizerWidget::EqualizerValues v; v.maxDistance = 10; for (int i = -v.maxDistance; i <= v.maxDistance; i++) { v.value.insert(i, qRound(config.onionSkinOpacity(i) * 100.0 / 255.0)); v.state.insert(i, config.onionSkinState(i)); } m_equalizerWidget->setValues(v); } diff --git a/plugins/dockers/animation/onion_skins_docker.h b/plugins/dockers/animation/onion_skins_docker.h index 7d7785abc3..198d016ac1 100644 --- a/plugins/dockers/animation/onion_skins_docker.h +++ b/plugins/dockers/animation/onion_skins_docker.h @@ -1,66 +1,69 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef ONION_SKINS_DOCKER_H #define ONION_SKINS_DOCKER_H #include #include #include "kis_signal_compressor.h" class KisAction; namespace Ui { class OnionSkinsDocker; } class KisEqualizerWidget; class OnionSkinsDocker : public QDockWidget, public KisMainwindowObserver { Q_OBJECT public: explicit OnionSkinsDocker(QWidget *parent = 0); ~OnionSkinsDocker() override; QString observerName() override { return "OnionSkinsDocker"; } void setCanvas(KoCanvasBase *canvas) override; void unsetCanvas() override; void setViewManager(KisViewManager *kisview) override; private: Ui::OnionSkinsDocker *ui; KisSignalCompressor m_updatesCompressor; KisEqualizerWidget *m_equalizerWidget; KisAction *m_toggleOnionSkinsAction; + class KisColorLabelFilterGroup *m_filterButtonGroup; + class KisColorLabelMouseDragFilter *m_dragFilter; + private: void loadSettings(); private Q_SLOTS: void changed(); void slotShowAdditionalSettings(bool value); void slotUpdateIcons(); void slotToggleOnionSkins(); void slotFilteredColorsChanged(); }; #endif // ONION_SKINS_DOCKER_H diff --git a/plugins/dockers/animation/onion_skins_docker.ui b/plugins/dockers/animation/onion_skins_docker.ui index e16b4003d5..2566179e03 100644 --- a/plugins/dockers/animation/onion_skins_docker.ui +++ b/plugins/dockers/animation/onion_skins_docker.ui @@ -1,258 +1,217 @@ OnionSkinsDocker 0 0 336 282 false Onion skin options 12 12 12 12 0 0 0 - Filter Frames by Color + Filter Onion Skins by Frame Color true false 4 0 0 0 0 - + + + + 0 + 0 + + - 0 + 65 0 - - None - - + - 16 - 16 + 16777215 + 16777215 - - - - - - - - - - - - - - - - - - - - - - - - - - + Reset - - - - - - - - - - - - - - - - - - - - + + + 0 + 0 + - - - - - + 6 Qt::Horizontal + true false true 50 0 Previous frames Qt::Horizontal 13 13 Next frames KisDoubleSliderSpinBox QWidget
kis_slider_spin_box.h
1
KisColorButton QPushButton
kis_color_button.h
diff --git a/plugins/dockers/animation/timeline_docker.cpp b/plugins/dockers/animation/timeline_docker.cpp index bb03f2a6b4..e4d9443610 100644 --- a/plugins/dockers/animation/timeline_docker.cpp +++ b/plugins/dockers/animation/timeline_docker.cpp @@ -1,603 +1,606 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * Copyright (c) 2020 Emmet O'Neill * Copyright (c) 2020 Eoin O'Neill * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_docker.h" #include #include "QHBoxLayout" #include "QVBoxLayout" #include "QFormLayout" #include "QLabel" #include "QPushButton" #include "QToolButton" #include "QMenu" #include "QWidgetAction" #include "krita_utils.h" #include "kis_canvas2.h" #include "kis_image.h" #include #include "KisViewManager.h" #include "kis_paint_layer.h" #include "KisDocument.h" #include "kis_dummies_facade.h" #include "kis_shape_controller.h" #include "kis_action.h" #include "kis_action_manager.h" #include "kis_animation_player.h" #include "kis_animation_utils.h" #include "kis_image_config.h" #include "kis_keyframe_channel.h" #include "kis_image.h" #include "timeline_frames_model.h" #include "timeline_frames_view.h" #include "kis_time_range.h" #include "kis_animation_frame_cache.h" #include "kis_image_animation_interface.h" #include "kis_signal_auto_connection.h" #include "kis_node_manager.h" #include "kis_transport_controls.h" #include "kis_int_parse_spin_box.h" #include "kis_slider_spin_box.h" #include "kis_signals_blocker.h" TimelineDockerTitleBar::TimelineDockerTitleBar(QWidget* parent) : KisUtilityTitleBar(new QLabel(i18n("Animation Timeline"), parent), parent) { // Transport Controls... transport = new KisTransportControls(this); widgetArea->addWidget(transport); widgetArea->addSpacing(SPACING_UNIT); // Frame Register... frameRegister = new KisIntParseSpinBox(this); frameRegister->setToolTip(i18n("Frame register")); frameRegister->setPrefix("# "); frameRegister->setRange(0, MAX_FRAMES); widgetArea->addWidget(frameRegister); widgetArea->addSpacing(SPACING_UNIT); { // Frame ops... QHBoxLayout *layout = new QHBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(0,0,0,0); btnAddKeyframe = new QToolButton(this); layout->addWidget(btnAddKeyframe); btnDuplicateKeyframe = new QToolButton(this); layout->addWidget(btnDuplicateKeyframe); btnRemoveKeyframe = new QToolButton(this); layout->addWidget(btnRemoveKeyframe); QWidget *widget = new QWidget(); widget->setLayout(layout); widgetArea->addWidget(widget); } widgetArea->addSpacing(SPACING_UNIT); // Drop Frames.. btnDropFrames = new QToolButton(this); widgetArea->addWidget(btnDropFrames); // Playback Speed.. sbSpeed = new KisSliderSpinBox(this); sbSpeed->setRange(25, 200); sbSpeed->setSingleStep(5); sbSpeed->setValue(100); sbSpeed->setPrefix("Speed: "); sbSpeed->setSuffix(" %"); sbSpeed->setToolTip(i18n("Preview playback speed")); widgetArea->addWidget(sbSpeed); widgetArea->addStretch(); { // Menus.. QHBoxLayout *layout = new QHBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(SPACING_UNIT,0,0,0); // Onion skins menu. btnOnionSkinsMenu = new QPushButton(KisIconUtils::loadIcon("onion_skin_options"), "", this); btnOnionSkinsMenu->setToolTip(i18n("Onion skins menu")); layout->addWidget(btnOnionSkinsMenu); // Audio menu.. btnAudioMenu = new QPushButton(KisIconUtils::loadIcon("audio-none"), "", this); btnOnionSkinsMenu->setToolTip(i18n("Audio menu")); btnAudioMenu->hide(); // (NOTE: Hidden for now while audio features develop.) layout->addWidget(btnAudioMenu); { // Settings menu.. btnSettingsMenu = new QToolButton(this); btnSettingsMenu->setIcon(KisIconUtils::loadIcon("configure")); btnSettingsMenu->setToolTip(i18n("Animation settings menu")); QWidget *settingsMenuWidget = new QWidget(this); settingsMenuWidget->setLayout(new QHBoxLayout(settingsMenuWidget)); QWidget *fields = new QWidget(settingsMenuWidget); QFormLayout *fieldsLayout = new QFormLayout(settingsMenuWidget); fields->setLayout(fieldsLayout); sbStartFrame = new KisIntParseSpinBox(settingsMenuWidget); sbStartFrame->setMaximum(10000); fieldsLayout->addRow(i18n("Clip Start: "), sbStartFrame); sbEndFrame = new KisIntParseSpinBox(settingsMenuWidget); sbEndFrame->setMaximum(10000); fieldsLayout->addRow(i18n("Clip End: "), sbEndFrame); sbFrameRate = new KisIntParseSpinBox(settingsMenuWidget); sbFrameRate->setMinimum(0); sbFrameRate->setMaximum(180); fieldsLayout->addRow(i18n("Frame Rate: "), sbFrameRate); QWidget *buttons = new QWidget(settingsMenuWidget); buttons->setLayout(new QVBoxLayout(settingsMenuWidget)); btnAutoFrame = new QToolButton(settingsMenuWidget); buttons->layout()->addWidget(btnAutoFrame); buttons->layout()->setAlignment(Qt::AlignTop); settingsMenuWidget->layout()->addWidget(fields); settingsMenuWidget->layout()->addWidget(buttons); layout->addWidget(btnSettingsMenu); QMenu *settingsPopMenu = new QMenu(this); QWidgetAction *settingsMenuAction = new QWidgetAction(this); settingsMenuAction->setDefaultWidget(settingsMenuWidget); settingsPopMenu->addAction(settingsMenuAction); btnSettingsMenu->setPopupMode(QToolButton::InstantPopup); btnSettingsMenu->setMenu(settingsPopMenu); } QWidget *widget = new QWidget(); widget->setLayout(layout); widgetArea->addWidget(widget); } } struct TimelineDocker::Private { Private(QWidget *parent) : framesModel(new TimelineFramesModel(parent)), framesView(new TimelineFramesView(parent)), titlebar(new TimelineDockerTitleBar(parent)), mainWindow(nullptr) { framesView->setModel(framesModel); } TimelineFramesModel *framesModel; TimelineFramesView *framesView; TimelineDockerTitleBar *titlebar; QPointer canvas; KisSignalAutoConnectionsStore canvasConnections; KisMainWindow *mainWindow; }; TimelineDocker::TimelineDocker() : QDockWidget(i18n("Animation Timeline")) , m_d(new Private(this)) { setWidget(m_d->framesView); setTitleBarWidget(m_d->titlebar); connect(m_d->titlebar->transport, SIGNAL(back()), this, SLOT(previousFrame())); connect(m_d->titlebar->transport, SIGNAL(stop()), this, SLOT(stop())); connect(m_d->titlebar->transport, SIGNAL(playPause()), this, SLOT(playPause())); connect(m_d->titlebar->transport, SIGNAL(forward()), this, SLOT(nextFrame())); connect(m_d->titlebar->frameRegister, SIGNAL(valueChanged(int)), SLOT(goToFrame(int))); connect(m_d->titlebar->sbStartFrame, SIGNAL(valueChanged(int)), SLOT(setStartFrame(int))); connect(m_d->titlebar->sbEndFrame, SIGNAL(valueChanged(int)), SLOT(setEndFrame(int))); connect(m_d->titlebar->sbFrameRate, SIGNAL(valueChanged(int)), SLOT(setFrameRate(int))); connect(m_d->titlebar->sbSpeed, SIGNAL(valueChanged(int)), SLOT(setPlaybackSpeed(int))); connect(m_d->titlebar->btnOnionSkinsMenu, &QPushButton::released, [this](){ if (m_d->mainWindow) { QDockWidget *docker = m_d->mainWindow->dockWidget("OnionSkinsDocker"); if (docker) { docker->setVisible(!docker->isVisible()); } } }); } TimelineDocker::~TimelineDocker() { } struct NodeManagerInterface : TimelineFramesModel::NodeManipulationInterface { NodeManagerInterface(KisNodeManager *manager) : m_manager(manager) {} KisLayerSP addPaintLayer() const override { return m_manager->createPaintLayer(); } void removeNode(KisNodeSP node) const override { m_manager->removeSingleNode(node); } bool setNodeProperties(KisNodeSP node, KisImageSP image, KisBaseNode::PropertyList properties) const override { return m_manager->trySetNodeProperties(node, image, properties); } private: KisNodeManager *m_manager; }; void TimelineDocker::setCanvas(KoCanvasBase * canvas) { if (m_d->canvas == canvas) return; + if (m_d->framesView) { + m_d->framesView->slotCanvasUpdate(canvas); + } + if (m_d->framesModel->hasConnectionToCanvas()) { m_d->canvasConnections.clear(); m_d->framesModel->setDummiesFacade(0, 0, 0); m_d->framesModel->setFrameCache(0); m_d->framesModel->setAnimationPlayer(0); m_d->framesModel->setNodeManipulationInterface(0); } if (m_d->canvas) { m_d->canvas->disconnectCanvasObserver(this); m_d->canvas->animationPlayer()->disconnect(this); if(m_d->canvas->image()) { m_d->canvas->image()->animationInterface()->disconnect(this); } } m_d->canvas = dynamic_cast(canvas); setEnabled(m_d->canvas != 0); if(m_d->canvas) { KisDocument *doc = static_cast(m_d->canvas->imageView()->document()); KisShapeController *kritaShapeController = dynamic_cast(doc->shapeController()); m_d->framesModel->setDummiesFacade(kritaShapeController, m_d->canvas->image(), m_d->canvas->viewManager()->nodeManager()->nodeDisplayModeAdapter()); updateFrameCache(); { KisSignalsBlocker blocker(m_d->titlebar->sbStartFrame, m_d->titlebar->sbEndFrame, m_d->titlebar->sbFrameRate); KisImageAnimationInterface *animinterface = m_d->canvas->image()->animationInterface(); m_d->titlebar->sbStartFrame->setValue(animinterface->fullClipRange().start()); m_d->titlebar->sbEndFrame->setValue(animinterface->fullClipRange().end()); m_d->titlebar->sbFrameRate->setValue(animinterface->framerate()); m_d->titlebar->sbSpeed->setValue(100); m_d->titlebar->frameRegister->setValue(animinterface->currentTime()); } m_d->framesModel->setAnimationPlayer(m_d->canvas->animationPlayer()); m_d->framesModel->setNodeManipulationInterface( new NodeManagerInterface(m_d->canvas->viewManager()->nodeManager())); m_d->canvasConnections.addConnection( m_d->canvas->viewManager()->nodeManager(), SIGNAL(sigNodeActivated(KisNodeSP)), m_d->framesModel, SLOT(slotCurrentNodeChanged(KisNodeSP))); m_d->canvasConnections.addConnection( m_d->framesModel, SIGNAL(requestCurrentNodeChanged(KisNodeSP)), m_d->canvas->viewManager()->nodeManager(), SLOT(slotNonUiActivatedNode(KisNodeSP))); m_d->framesModel->slotCurrentNodeChanged(m_d->canvas->viewManager()->activeNode()); m_d->canvasConnections.addConnection( m_d->canvas->viewManager()->mainWindow(), SIGNAL(themeChanged()), this, SLOT(handleThemeChange())); m_d->canvasConnections.addConnection( m_d->canvas, SIGNAL(sigCanvasEngineChanged()), this, SLOT(updateFrameCache())); connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigUiTimeChanged(int)), this, SLOT(updateFrameRegister())); connect(m_d->canvas->animationPlayer(), SIGNAL(sigFrameChanged()), this, SLOT(updateFrameRegister())); connect(m_d->canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(updateFrameRegister())); connect(m_d->canvas->animationPlayer(), SIGNAL(sigPlaybackStateChanged(bool)), m_d->titlebar->frameRegister, SLOT(setDisabled(bool))); connect(m_d->canvas->animationPlayer(), SIGNAL(sigPlaybackStateChanged(bool)), m_d->titlebar->transport, SLOT(setPlaying(bool))); connect(m_d->canvas->animationPlayer(), SIGNAL(sigPlaybackStatisticsUpdated()), this, SLOT(updatePlaybackStatistics())); connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigFullClipRangeChanged()), SLOT(handleClipRangeChange())); connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()), SLOT(handleFrameRateChange())); } } void TimelineDocker::handleThemeChange() { if (m_d->framesView) { m_d->framesView->slotUpdateIcons(); } } void TimelineDocker::updateFrameCache() { m_d->framesModel->setFrameCache(m_d->canvas->frameCache()); } void TimelineDocker::updateFrameRegister() { if (!m_d->canvas && !m_d->canvas->image()) { return; } const int frame = m_d->canvas->animationPlayer()->isPlaying() ? m_d->canvas->animationPlayer()->visibleFrame() : m_d->canvas->image()->animationInterface()->currentUITime(); m_d->titlebar->frameRegister->setValue(frame); } void TimelineDocker::updatePlaybackStatistics() { qreal effectiveFps = 0.0; qreal realFps = 0.0; qreal framesDropped = 0.0; bool isPlaying = false; KisAnimationPlayer *player = m_d->canvas && m_d->canvas->animationPlayer() ? m_d->canvas->animationPlayer() : 0; if (player) { effectiveFps = player->effectiveFps(); realFps = player->realFps(); framesDropped = player->framesDroppedPortion(); isPlaying = player->isPlaying(); } KisConfig cfg(true); const bool shouldDropFrames = cfg.animationDropFrames(); QAction *action = m_d->titlebar->btnDropFrames->defaultAction(); const bool droppingFrames = shouldDropFrames && framesDropped > 0.05; action->setIcon(KisIconUtils::loadIcon(droppingFrames ? "droppedframes" : "dropframe")); QString actionText; if (!isPlaying) { actionText = QString("%1 (%2) \n%3") .arg(KisAnimationUtils::dropFramesActionName) .arg(KritaUtils::toLocalizedOnOff(shouldDropFrames)) .arg(i18n("Enable to preserve playback timing.")); } else { actionText = QString("%1 (%2)\n" "%3\n" "%4\n" "%5") .arg(KisAnimationUtils::dropFramesActionName) .arg(KritaUtils::toLocalizedOnOff(shouldDropFrames)) .arg(i18n("Effective FPS:\t%1", effectiveFps)) .arg(i18n("Real FPS:\t%1", realFps)) .arg(i18n("Frames dropped:\t%1\%", framesDropped * 100)); } action->setText(actionText); } void TimelineDocker::unsetCanvas() { setCanvas(0); } void TimelineDocker::setViewManager(KisViewManager *view) { m_d->mainWindow = view->mainWindow(); - KisActionManager *actionManager = view->actionManager(); m_d->framesView->setActionManager(actionManager); KisAction *action = 0; TimelineDockerTitleBar* titleBar = static_cast(titleBarWidget()); action = actionManager->actionByName("add_blank_frame"); titleBar->btnAddKeyframe->setDefaultAction(action); action = actionManager->actionByName("add_duplicate_frame"); titleBar->btnDuplicateKeyframe->setDefaultAction(action); action = actionManager->actionByName("remove_frames"); titleBar->btnRemoveKeyframe->setDefaultAction(action); action = actionManager->createAction("toggle_playback"); action->setActivationFlags(KisAction::ACTIVE_IMAGE); connect(action, SIGNAL(triggered(bool)), SLOT(playPause())); action = actionManager->createAction("stop_playback"); action->setActivationFlags(KisAction::ACTIVE_IMAGE); connect(action, SIGNAL(triggered(bool)), SLOT(stop())); action = actionManager->createAction("previous_frame"); action->setActivationFlags(KisAction::ACTIVE_IMAGE); connect(action, SIGNAL(triggered(bool)), SLOT(previousFrame())); action = actionManager->createAction("next_frame"); action->setActivationFlags(KisAction::ACTIVE_IMAGE); connect(action, SIGNAL(triggered(bool)), SLOT(nextFrame())); action = actionManager->createAction("auto_key"); m_d->titlebar->btnAutoFrame->setDefaultAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(setAutoKey(bool))); { KisImageConfig config(true); action->setChecked(config.autoKeyEnabled()); action->setIcon(config.autoKeyEnabled() ? KisIconUtils::loadIcon("auto-key-on") : KisIconUtils::loadIcon("auto-key-off")); } action = actionManager->createAction("drop_frames"); m_d->titlebar->btnDropFrames->setDefaultAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(setDropFrames(bool))); { KisConfig config(true); action->setChecked(config.animationDropFrames()); } } void TimelineDocker::playPause() { if (!m_d->canvas) return; if (m_d->canvas->animationPlayer()->isPlaying()) { m_d->canvas->animationPlayer()->pause(); } else { m_d->canvas->animationPlayer()->play(); } } void TimelineDocker::stop() { if (!m_d->canvas) return; if( m_d->canvas->animationPlayer()->isStopped()) { m_d->canvas->animationPlayer()->goToStartFrame(); } else { m_d->canvas->animationPlayer()->stop(); m_d->canvas->animationPlayer()->goToPlaybackOrigin(); } } void TimelineDocker::previousFrame() { if (!m_d->canvas) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); int time = animInterface->currentUITime() - 1; if (time >= 0) { animInterface->requestTimeSwitchWithUndo(time); } } void TimelineDocker::nextFrame() { if (!m_d->canvas) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); int time = animInterface->currentUITime() + 1; animInterface->requestTimeSwitchWithUndo(time); } void TimelineDocker::goToFrame(int frameIndex) { if (!m_d->canvas || !m_d->canvas->image()) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); if (m_d->canvas->animationPlayer()->isPlaying() || frameIndex == animInterface->currentUITime()) { return; } animInterface->requestTimeSwitchWithUndo(frameIndex); } void TimelineDocker::setStartFrame(int frame) { if (!m_d->canvas || !m_d->canvas->image()) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); animInterface->setFullClipRangeStartTime(frame); } void TimelineDocker::setEndFrame(int frame) { if (!m_d->canvas || !m_d->canvas->image()) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); animInterface->setFullClipRangeEndTime(frame); } void TimelineDocker::setFrameRate(int framerate) { if (!m_d->canvas || !m_d->canvas->image()) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); animInterface->setFramerate(framerate); } void TimelineDocker::setPlaybackSpeed(int playbackSpeed) { if (!m_d->canvas || !m_d->canvas->image()) return; const float normalizedSpeed = playbackSpeed / 100.0; m_d->canvas->animationPlayer()->slotUpdatePlaybackSpeed(normalizedSpeed); } void TimelineDocker::setDropFrames(bool dropFrames) { KisConfig cfg(false); if (dropFrames != cfg.animationDropFrames()) { cfg.setAnimationDropFrames(dropFrames); updatePlaybackStatistics(); } } void TimelineDocker::setAutoKey(bool autoKey) { KisImageConfig cfg(false); if (autoKey != cfg.autoKeyEnabled()) { cfg.setAutoKeyEnabled(autoKey); const QIcon icon = cfg.autoKeyEnabled() ? KisIconUtils::loadIcon("auto-key-on") : KisIconUtils::loadIcon("auto-key-off"); QAction* action = m_d->titlebar->btnAutoFrame->defaultAction(); action->setIcon(icon); } } void TimelineDocker::handleClipRangeChange() { if (!m_d->canvas || !m_d->canvas->image()) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); m_d->titlebar->sbStartFrame->setValue(animInterface->fullClipRange().start()); m_d->titlebar->sbEndFrame->setValue(animInterface->fullClipRange().end()); } void TimelineDocker::handleFrameRateChange() { if (!m_d->canvas || !m_d->canvas->image()) return; KisImageAnimationInterface *animInterface = m_d->canvas->image()->animationInterface(); m_d->titlebar->sbFrameRate->setValue( m_d->canvas->image()->animationInterface()->framerate() ); } diff --git a/plugins/dockers/animation/timeline_frames_model.h b/plugins/dockers/animation/timeline_frames_model.h index 68e36134a7..0e7f735c80 100644 --- a/plugins/dockers/animation/timeline_frames_model.h +++ b/plugins/dockers/animation/timeline_frames_model.h @@ -1,157 +1,158 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __TIMELINE_FRAMES_MODEL_H #define __TIMELINE_FRAMES_MODEL_H #include #include #include "kritaanimationdocker_export.h" #include "kis_node_model.h" #include "kis_types.h" #include "kis_node.h" #include "timeline_node_list_keeper.h" class KisNodeDummy; class KisDummiesFacadeBase; class KisAnimationPlayer; class KisNodeDisplayModeAdapter; class KRITAANIMATIONDOCKER_EXPORT TimelineFramesModel : public TimelineNodeListKeeper::ModelWithExternalNotifications { Q_OBJECT public: enum MimeCopyPolicy { UndefinedPolicy = 0, MoveFramesPolicy, CopyFramesPolicy }; public: TimelineFramesModel(QObject *parent); ~TimelineFramesModel() override; bool hasConnectionToCanvas() const; void setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageSP image, KisNodeDisplayModeAdapter *displayModeAdapter); bool canDropFrameData(const QMimeData *data, const QModelIndex &index); bool insertOtherLayer(int index, int dstRow); int activeLayerRow() const; bool createFrame(const QModelIndex &dstIndex); bool copyFrame(const QModelIndex &dstIndex); bool insertFrames(int dstColumn, const QList &dstRows, int count, int timing = 1); bool insertHoldFrames(QModelIndexList selectedIndexes, int count); QString audioChannelFileName() const; void setAudioChannelFileName(const QString &fileName); bool isAudioMuted() const; void setAudioMuted(bool value); qreal audioVolume() const; void setAudioVolume(qreal value); void setFullClipRangeStart(int column); void setFullClipRangeEnd(int column); void setLastClickedIndex(const QModelIndex &index); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override; Qt::DropActions supportedDragActions() const override; Qt::DropActions supportedDropActions() const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; QMimeData *mimeDataExtended(const QModelIndexList &indexes, const QModelIndex &baseIndex, MimeCopyPolicy copyPolicy) const; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; bool dropMimeDataExtended(const QMimeData *data, Qt::DropAction action, const QModelIndex &parent, bool *dataMoved = 0); Qt::ItemFlags flags(const QModelIndex &index) const override; bool insertRows(int row, int count, const QModelIndex &parent) override; bool removeRows(int row, int count, const QModelIndex &parent) override; enum ItemDataRole { ActiveLayerRole = KisTimeBasedItemModel::UserRole, TimelinePropertiesRole, OtherLayersRole, PinnedToTimelineRole, FrameColorLabelIndexRole }; // metatype is added by the original implementation typedef KisBaseNode::Property Property; typedef KisBaseNode::PropertyList PropertyList; typedef TimelineNodeListKeeper::OtherLayer OtherLayer; typedef TimelineNodeListKeeper::OtherLayersList OtherLayersList; struct NodeManipulationInterface { virtual ~NodeManipulationInterface() {} virtual KisLayerSP addPaintLayer() const = 0; virtual void removeNode(KisNodeSP node) const = 0; virtual bool setNodeProperties(KisNodeSP node, KisImageSP image, KisBaseNode::PropertyList properties) const = 0; }; /** * NOTE: the model has an ownership over the interface, that is it'll * be deleted automatically later */ void setNodeManipulationInterface(NodeManipulationInterface *iface); KisNodeSP nodeAt(QModelIndex index) const override; protected: QMap channelsAt(QModelIndex index) const override; private Q_SLOTS: void slotDummyChanged(KisNodeDummy *dummy); void slotImageContentChanged(); void processUpdateQueue(); public Q_SLOTS: void slotCurrentNodeChanged(KisNodeSP node); Q_SIGNALS: void requestCurrentNodeChanged(KisNodeSP node); void sigInfiniteTimelineUpdateNeeded(); void sigAudioChannelChanged(); void sigEnsureRowVisible(int row); + void sigFullClipRangeChanged(); private: struct Private; const QScopedPointer m_d; }; #endif /* __TIMELINE_FRAMES_MODEL_H */ diff --git a/plugins/dockers/animation/timeline_frames_view.cpp b/plugins/dockers/animation/timeline_frames_view.cpp index b692d64c89..a95ec7d203 100644 --- a/plugins/dockers/animation/timeline_frames_view.cpp +++ b/plugins/dockers/animation/timeline_frames_view.cpp @@ -1,1552 +1,1661 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_frames_view.h" #include "timeline_frames_model.h" #include "timeline_ruler_header.h" #include "timeline_layers_header.h" #include "timeline_insert_keyframe_dialog.h" #include "timeline_frames_item_delegate.h" #include #include #include #include #include #include #include #include #include #include +#include +#include #include "config-qtmultimedia.h" #include "KSharedConfig" #include "KisKineticScroller.h" #include "kis_zoom_button.h" #include "kis_icon_utils.h" #include "kis_animation_utils.h" #include "kis_custom_modifiers_catcher.h" #include "kis_action.h" #include "kis_signal_compressor.h" #include "kis_time_range.h" #include "kis_color_label_selector_widget.h" +#include "kis_layer_filter_widget.h" #include "kis_keyframe_channel.h" #include "kis_slider_spin_box.h" -#include -#include -#include - -#include -#include +#include "kis_signals_blocker.h" +#include "kis_image_config.h" +#include "kis_zoom_scrollbar.h" +#include "KisImportExportManager.h" +#include "KoFileDialog.h" +#include "KisIconToolTip.h" typedef QPair QItemViewPaintPair; typedef QList QItemViewPaintPairs; struct TimelineFramesView::Private { Private(TimelineFramesView *_q) : q(_q), fps(1), - zoomStillPointIndex(-1), - zoomStillPointOriginalOffset(0), dragInProgress(false), dragWasSuccessful(false), modifiersCatcher(0), - selectionChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE) - {} + kineticScrollInfiniteFrameUpdater(), + selectionChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE), + geometryChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE) + { + kineticScrollInfiniteFrameUpdater.setTimerType(Qt::CoarseTimer); + } TimelineFramesView *q; TimelineFramesModel *model; TimelineRulerHeader *horizontalRuler; TimelineLayersHeader *layersHeader; int fps; - int zoomStillPointIndex; - int zoomStillPointOriginalOffset; QPoint initialDragPanValue; QPoint initialDragPanPos; QToolButton *addLayersButton; KisAction *pinLayerToTimelineAction; QToolButton *audioOptionsButton; KisColorLabelSelectorWidget *colorSelector; QWidgetAction *colorSelectorAction; KisColorLabelSelectorWidget *multiframeColorSelector; QWidgetAction *multiframeColorSelectorAction; QMenu *audioOptionsMenu; QAction *openAudioAction; QAction *audioMuteAction; KisSliderSpinBox *volumeSlider; QMenu *layerEditingMenu; QMenu *existingLayersMenu; TimelineInsertKeyframeDialog *insertKeyframeDialog; KisZoomButton *zoomDragButton; bool dragInProgress; bool dragWasSuccessful; KisCustomModifiersCatcher *modifiersCatcher; QPoint lastPressedPosition; Qt::KeyboardModifiers lastPressedModifier; + + QTimer kineticScrollInfiniteFrameUpdater; + KisSignalCompressor selectionChangedCompressor; + KisSignalCompressor geometryChangedCompressor; QStyleOptionViewItem viewOptionsV4() const; QItemViewPaintPairs draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const; QPixmap renderToPixmap(const QModelIndexList &indexes, QRect *r) const; KisIconToolTip tip; KisActionManager *actionMan = 0; }; TimelineFramesView::TimelineFramesView(QWidget *parent) : QTableView(parent), m_d(new Private(this)) { m_d->modifiersCatcher = new KisCustomModifiersCatcher(this); m_d->modifiersCatcher->addModifier("pan-zoom", Qt::Key_Space); m_d->modifiersCatcher->addModifier("offset-frame", Qt::Key_Alt); setCornerButtonEnabled(false); setSelectionBehavior(QAbstractItemView::SelectItems); setSelectionMode(QAbstractItemView::ExtendedSelection); setItemDelegate(new TimelineFramesItemDelegate(this)); setDragEnabled(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDropIndicatorShown(true); setDefaultDropAction(Qt::MoveAction); m_d->horizontalRuler = new TimelineRulerHeader(this); this->setHorizontalHeader(m_d->horizontalRuler); + KisZoomableScrollBar* hZoomableBar = new KisZoomableScrollBar(this); + setHorizontalScrollBar(hZoomableBar); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + setVerticalScrollBar(new KisZoomableScrollBar(this)); + hZoomableBar->setEnabled(false); + + connect(hZoomableBar, SIGNAL(zoom(qreal)), this, SLOT(slotScrollbarZoom(qreal))); + connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnLeft()), SLOT(slotInsertKeyframeColumnLeft())); connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnRight()), SLOT(slotInsertKeyframeColumnRight())); connect(m_d->horizontalRuler, SIGNAL(sigInsertMultipleColumns()), SLOT(slotInsertMultipleKeyframeColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumns()), SLOT(slotRemoveSelectedColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumnsAndShift()), SLOT(slotRemoveSelectedColumnsAndShift())); connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumns()), SLOT(slotInsertHoldFrameColumn())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumns()), SLOT(slotRemoveHoldFrameColumn())); connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumnsCustom()), SLOT(slotInsertMultipleHoldFrameColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumnsCustom()), SLOT(slotRemoveMultipleHoldFrameColumns())); connect(m_d->horizontalRuler, SIGNAL(sigMirrorColumns()), SLOT(slotMirrorColumns())); connect(m_d->horizontalRuler, SIGNAL(sigCopyColumns()), SLOT(slotCopyColumns())); connect(m_d->horizontalRuler, SIGNAL(sigCutColumns()), SLOT(slotCutColumns())); connect(m_d->horizontalRuler, SIGNAL(sigPasteColumns()), SLOT(slotPasteColumns())); + connect(m_d->horizontalRuler, SIGNAL(geometriesChanged()), &m_d->geometryChangedCompressor, SLOT(start())); + m_d->layersHeader = new TimelineLayersHeader(this); m_d->layersHeader->setSectionResizeMode(QHeaderView::Fixed); m_d->layersHeader->setDefaultSectionSize(24); m_d->layersHeader->setMinimumWidth(60); m_d->layersHeader->setHighlightSections(true); - this->setVerticalHeader(m_d->layersHeader); - connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(slotUpdateInfiniteFramesCount())); - connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount())); + connect(m_d->layersHeader, SIGNAL(geometriesChanged()), &m_d->geometryChangedCompressor, SLOT(start())); + + connect(&m_d->geometryChangedCompressor, SIGNAL(timeout()), SLOT(slotRealignScrollBars())); + + connect(hZoomableBar, SIGNAL(overscroll(int)), SLOT(slotUpdateInfiniteFramesCount())); + connect(hZoomableBar, SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount())); /********** Layer Menu ***********************************************************/ m_d->layerEditingMenu = new QMenu(this); m_d->layerEditingMenu->addSection(i18n("Edit Layers:")); m_d->layerEditingMenu->addSeparator(); m_d->layerEditingMenu->addAction(KisAnimationUtils::newLayerActionName, this, SLOT(slotAddNewLayer())); m_d->layerEditingMenu->addAction(KisAnimationUtils::removeLayerActionName, this, SLOT(slotRemoveLayer())); m_d->layerEditingMenu->addSeparator(); m_d->existingLayersMenu = m_d->layerEditingMenu->addMenu(KisAnimationUtils::pinExistingLayerActionName); connect(m_d->existingLayersMenu, SIGNAL(aboutToShow()), SLOT(slotUpdateLayersMenu())); connect(m_d->existingLayersMenu, SIGNAL(triggered(QAction*)), SLOT(slotAddExistingLayer(QAction*))); connect(m_d->layersHeader, SIGNAL(sigRequestContextMenu(QPoint)), SLOT(slotLayerContextMenuRequested(QPoint))); m_d->addLayersButton = new QToolButton(this); m_d->addLayersButton->setAutoRaise(true); m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer")); m_d->addLayersButton->setIconSize(QSize(20, 20)); m_d->addLayersButton->setPopupMode(QToolButton::InstantPopup); m_d->addLayersButton->setMenu(m_d->layerEditingMenu); /********** Audio Channel Menu *******************************************************/ m_d->audioOptionsButton = new QToolButton(this); m_d->audioOptionsButton->setAutoRaise(true); m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("audio-none")); m_d->audioOptionsButton->setIconSize(QSize(20, 20)); // very small on windows if not explicitly set m_d->audioOptionsButton->setPopupMode(QToolButton::InstantPopup); m_d->audioOptionsMenu = new QMenu(this); m_d->audioOptionsMenu->addSection(i18n("Edit Audio:")); m_d->audioOptionsMenu->addSeparator(); #ifndef HAVE_QT_MULTIMEDIA m_d->audioOptionsMenu->addSection(i18nc("@item:inmenu", "Audio playback is not supported in this build!")); #endif m_d->openAudioAction= new QAction("XXX", this); connect(m_d->openAudioAction, SIGNAL(triggered()), this, SLOT(slotSelectAudioChannelFile())); m_d->audioOptionsMenu->addAction(m_d->openAudioAction); m_d->audioMuteAction = new QAction(i18nc("@item:inmenu", "Mute"), this); m_d->audioMuteAction->setCheckable(true); connect(m_d->audioMuteAction, SIGNAL(triggered(bool)), SLOT(slotAudioChannelMute(bool))); m_d->audioOptionsMenu->addAction(m_d->audioMuteAction); m_d->audioOptionsMenu->addAction(i18nc("@item:inmenu", "Remove audio"), this, SLOT(slotAudioChannelRemove())); m_d->audioOptionsMenu->addSeparator(); m_d->volumeSlider = new KisSliderSpinBox(this); m_d->volumeSlider->setRange(0, 100); m_d->volumeSlider->setSuffix(i18n("%")); m_d->volumeSlider->setPrefix(i18nc("@item:inmenu, slider", "Volume:")); m_d->volumeSlider->setSingleStep(1); m_d->volumeSlider->setPageStep(10); m_d->volumeSlider->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); connect(m_d->volumeSlider, SIGNAL(valueChanged(int)), SLOT(slotAudioVolumeChanged(int))); QWidgetAction *volumeAction = new QWidgetAction(m_d->audioOptionsMenu); volumeAction->setDefaultWidget(m_d->volumeSlider); m_d->audioOptionsMenu->addAction(volumeAction); m_d->audioOptionsButton->setMenu(m_d->audioOptionsMenu); /********** Frame Editing Context Menu ***********************************************/ m_d->colorSelector = new KisColorLabelSelectorWidget(this); + MouseClickIgnore* clickIgnore = new MouseClickIgnore(this); + m_d->colorSelector->installEventFilter(clickIgnore); m_d->colorSelectorAction = new QWidgetAction(this); m_d->colorSelectorAction->setDefaultWidget(m_d->colorSelector); connect(m_d->colorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged); m_d->multiframeColorSelector = new KisColorLabelSelectorWidget(this); + m_d->multiframeColorSelector->installEventFilter(clickIgnore); m_d->multiframeColorSelectorAction = new QWidgetAction(this); m_d->multiframeColorSelectorAction->setDefaultWidget(m_d->multiframeColorSelector); connect(m_d->multiframeColorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged); /********** Insert Keyframes Dialog **************************************************/ m_d->insertKeyframeDialog = new TimelineInsertKeyframeDialog(this); /********** Zoom Button **************************************************************/ m_d->zoomDragButton = new KisZoomButton(this); m_d->zoomDragButton->setAutoRaise(true); m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal")); m_d->zoomDragButton->setIconSize(QSize(20, 20)); // this icon is very small on windows if no explicitly set m_d->zoomDragButton->setToolTip(i18nc("@info:tooltip", "Zoom Timeline. Hold down and drag left or right.")); m_d->zoomDragButton->setPopupMode(QToolButton::InstantPopup); connect(m_d->zoomDragButton, SIGNAL(zoomLevelChanged(qreal)), SLOT(slotZoomButtonChanged(qreal))); - connect(m_d->zoomDragButton, SIGNAL(zoomStarted(qreal)), SLOT(slotZoomButtonPressed(qreal))); setFramesPerSecond(12); setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); { QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(this); if( scroller ) { + QScrollerProperties props = scroller->scrollerProperties(); + connect(scroller, SIGNAL(stateChanged(QScroller::State)), this, SLOT(slotScrollerStateChanged(QScroller::State))); + + connect(&m_d->kineticScrollInfiniteFrameUpdater, &QTimer::timeout, [this, scroller](){ + slotUpdateInfiniteFramesCount(); + scroller->resendPrepareEvent(); + }); + + props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + + scroller->setScrollerProperties(props); } } connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()), SLOT(slotSelectionChanged())); connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()), SLOT(slotUpdateFrameActions())); { QClipboard *cb = QApplication::clipboard(); connect(cb, SIGNAL(dataChanged()), SLOT(slotUpdateFrameActions())); } } TimelineFramesView::~TimelineFramesView() { } void TimelineFramesView::setActionManager(KisActionManager *actionManager) { m_d->actionMan = actionManager; m_d->horizontalRuler->setActionManager(actionManager); if (actionManager) { KisAction *action = 0; action = m_d->actionMan->createAction("add_blank_frame"); connect(action, SIGNAL(triggered()), SLOT(slotAddBlankFrame())); action = m_d->actionMan->createAction("add_duplicate_frame"); connect(action, SIGNAL(triggered()), SLOT(slotAddDuplicateFrame())); action = m_d->actionMan->createAction("insert_keyframe_left"); connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeLeft())); action = m_d->actionMan->createAction("insert_keyframe_right"); connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeRight())); action = m_d->actionMan->createAction("insert_multiple_keyframes"); connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleKeyframes())); action = m_d->actionMan->createAction("remove_frames_and_pull"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFramesAndShift())); action = m_d->actionMan->createAction("remove_frames"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFrames())); action = m_d->actionMan->createAction("insert_hold_frame"); connect(action, SIGNAL(triggered()), SLOT(slotInsertHoldFrame())); action = m_d->actionMan->createAction("insert_multiple_hold_frames"); connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleHoldFrames())); action = m_d->actionMan->createAction("remove_hold_frame"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveHoldFrame())); action = m_d->actionMan->createAction("remove_multiple_hold_frames"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveMultipleHoldFrames())); action = m_d->actionMan->createAction("mirror_frames"); connect(action, SIGNAL(triggered()), SLOT(slotMirrorFrames())); action = m_d->actionMan->createAction("copy_frames_to_clipboard"); connect(action, SIGNAL(triggered()), SLOT(slotCopyFrames())); action = m_d->actionMan->createAction("cut_frames_to_clipboard"); connect(action, SIGNAL(triggered()), SLOT(slotCutFrames())); action = m_d->actionMan->createAction("paste_frames_from_clipboard"); connect(action, SIGNAL(triggered()), SLOT(slotPasteFrames())); action = m_d->actionMan->createAction("set_start_time"); connect(action, SIGNAL(triggered()), SLOT(slotSetStartTimeToCurrentPosition())); action = m_d->actionMan->createAction("set_end_time"); connect(action, SIGNAL(triggered()), SLOT(slotSetEndTimeToCurrentPosition())); action = m_d->actionMan->createAction("update_playback_range"); connect(action, SIGNAL(triggered()), SLOT(slotUpdatePlackbackRange())); action = m_d->actionMan->actionByName("pin_to_timeline"); m_d->pinLayerToTimelineAction = action; m_d->layerEditingMenu->addAction(action); } } void resizeToMinimalSize(QAbstractButton *w, int minimalSize) { QSize buttonSize = w->sizeHint(); if (buttonSize.height() > minimalSize) { buttonSize = QSize(minimalSize, minimalSize); } w->resize(buttonSize); } void TimelineFramesView::updateGeometries() { QTableView::updateGeometries(); const int availableHeight = m_d->horizontalRuler->height(); const int margin = 2; const int minimalSize = availableHeight - 2 * margin; resizeToMinimalSize(m_d->addLayersButton, minimalSize); resizeToMinimalSize(m_d->audioOptionsButton, minimalSize); resizeToMinimalSize(m_d->zoomDragButton, minimalSize); int x = 2 * margin; int y = (availableHeight - minimalSize) / 2; m_d->addLayersButton->move(x, 2 * y); m_d->audioOptionsButton->move(x + minimalSize + 2 * margin, 2 * y); const int availableWidth = m_d->layersHeader->width(); x = availableWidth - margin - minimalSize; m_d->zoomDragButton->move(x, 2 * y); } void TimelineFramesView::setModel(QAbstractItemModel *model) { TimelineFramesModel *framesModel = qobject_cast(model); m_d->model = framesModel; QTableView::setModel(model); connect(m_d->model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), this, SLOT(slotHeaderDataChanged(Qt::Orientation,int,int))); connect(m_d->model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(slotDataChanged(QModelIndex,QModelIndex))); connect(m_d->model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(slotReselectCurrentIndex())); connect(m_d->model, SIGNAL(sigInfiniteTimelineUpdateNeeded()), this, SLOT(slotUpdateInfiniteFramesCount())); connect(m_d->model, SIGNAL(sigAudioChannelChanged()), this, SLOT(slotUpdateAudioActions())); connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), &m_d->selectionChangedCompressor, SLOT(start())); connect(m_d->model, SIGNAL(sigEnsureRowVisible(int)), SLOT(slotEnsureRowVisible(int))); slotUpdateAudioActions(); } void TimelineFramesView::setFramesPerSecond(int fps) { m_d->fps = fps; m_d->horizontalRuler->setFramePerSecond(fps); - - // For some reason simple update sometimes doesn't work here, so - // reset the whole header - // - // m_d->horizontalRuler->reset(); } -void TimelineFramesView::slotZoomButtonPressed(qreal staticPoint) +void TimelineFramesView::slotZoomButtonChanged(qreal zoomLevel) { - m_d->zoomStillPointIndex = - qIsNaN(staticPoint) ? currentIndex().column() : staticPoint; + const int originalFirstColumn = estimateFirstVisibleColumn(); + if (m_d->horizontalRuler->setZoom(zoomLevel)) { + m_d->zoomDragButton->setZoomLevel(m_d->horizontalRuler->zoom()); - const int w = m_d->horizontalRuler->defaultSectionSize(); + if (estimateLastVisibleColumn() >= m_d->model->columnCount()) { + slotUpdateInfiniteFramesCount(); + } - m_d->zoomStillPointOriginalOffset = - w * m_d->zoomStillPointIndex - - horizontalScrollBar()->value(); + viewport()->update(); + horizontalScrollBar()->setValue(scrollPositionFromColumn(originalFirstColumn)); + } } -void TimelineFramesView::slotZoomButtonChanged(qreal zoomLevel) +void TimelineFramesView::slotScrollbarZoom(qreal zoom) { - if (m_d->horizontalRuler->setZoom(zoomLevel)) { - slotUpdateInfiniteFramesCount(); + const int originalFirstColumn = estimateFirstVisibleColumn(); + if (m_d->horizontalRuler->setZoom(m_d->horizontalRuler->zoom() + zoom)) { + m_d->zoomDragButton->setZoomLevel(m_d->horizontalRuler->zoom()); - const int w = m_d->horizontalRuler->defaultSectionSize(); - horizontalScrollBar()->setValue(w * m_d->zoomStillPointIndex - m_d->zoomStillPointOriginalOffset); + if (estimateLastVisibleColumn() >= m_d->model->columnCount()) { + slotUpdateInfiniteFramesCount(); + } viewport()->update(); + horizontalScrollBar()->setValue(scrollPositionFromColumn(originalFirstColumn)); } } void TimelineFramesView::slotColorLabelChanged(int label) { Q_FOREACH(QModelIndex index, selectedIndexes()) { m_d->model->setData(index, label, TimelineFramesModel::FrameColorLabelIndexRole); } KisImageConfig(false).setDefaultFrameColorLabel(label); } void TimelineFramesView::slotSelectAudioChannelFile() { if (!m_d->model) return; QString defaultDir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); const QString currentFile = m_d->model->audioChannelFileName(); QDir baseDir = QFileInfo(currentFile).absoluteDir(); if (baseDir.exists()) { defaultDir = baseDir.absolutePath(); } const QString result = KisImportExportManager::askForAudioFileName(defaultDir, this); const QFileInfo info(result); if (info.exists()) { m_d->model->setAudioChannelFileName(info.absoluteFilePath()); } } void TimelineFramesView::slotAudioChannelMute(bool value) { if (!m_d->model) return; if (value != m_d->model->isAudioMuted()) { m_d->model->setAudioMuted(value); } } void TimelineFramesView::slotUpdateIcons() { m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer")); m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("audio-none")); m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal")); } +void TimelineFramesView::slotCanvasUpdate(KoCanvasBase *canvas) +{ + horizontalScrollBar()->setEnabled(canvas != nullptr); +} + void TimelineFramesView::slotAudioChannelRemove() { if (!m_d->model) return; m_d->model->setAudioChannelFileName(QString()); } void TimelineFramesView::slotUpdateAudioActions() { if (!m_d->model) return; const QString currentFile = m_d->model->audioChannelFileName(); if (currentFile.isEmpty()) { m_d->openAudioAction->setText(i18nc("@item:inmenu", "Open audio...")); } else { QFileInfo info(currentFile); m_d->openAudioAction->setText(i18nc("@item:inmenu", "Change audio (%1)...", info.fileName())); } m_d->audioMuteAction->setChecked(m_d->model->isAudioMuted()); QIcon audioIcon; if (currentFile.isEmpty()) { audioIcon = KisIconUtils::loadIcon("audio-none"); } else { if (m_d->model->isAudioMuted()) { audioIcon = KisIconUtils::loadIcon("audio-volume-mute"); } else { audioIcon = KisIconUtils::loadIcon("audio-volume-high"); } } m_d->audioOptionsButton->setIcon(audioIcon); m_d->volumeSlider->setEnabled(!m_d->model->isAudioMuted()); KisSignalsBlocker b(m_d->volumeSlider); m_d->volumeSlider->setValue(qRound(m_d->model->audioVolume() * 100.0)); } void TimelineFramesView::slotAudioVolumeChanged(int value) { m_d->model->setAudioVolume(qreal(value) / 100.0); } void TimelineFramesView::slotUpdateInfiniteFramesCount() { - if (horizontalScrollBar()->isSliderDown()) return; + const int lastVisibleFrame = estimateLastVisibleColumn(); - const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); - const int calculatedIndex = - (horizontalScrollBar()->value() + - m_d->horizontalRuler->width() - 1) / sectionWidth; + m_d->model->setLastVisibleFrame(lastVisibleFrame); - m_d->model->setLastVisibleFrame(calculatedIndex); } void TimelineFramesView::slotScrollerStateChanged( QScroller::State state ) { + + if (state == QScroller::Dragging || state == QScroller::Scrolling ) { + m_d->kineticScrollInfiniteFrameUpdater.start(16); + } else { + m_d->kineticScrollInfiniteFrameUpdater.stop(); + } + KisKineticScroller::updateCursor(this, state); } +void TimelineFramesView::slotUpdateDragInfiniteFramesCount() { + if(m_d->dragInProgress || + (m_d->model->isScrubbing() && horizontalScrollBar()->sliderPosition() == horizontalScrollBar()->maximum()) ) { + slotUpdateInfiniteFramesCount(); + } +} + +void TimelineFramesView::slotRealignScrollBars() { + QScrollBar* hBar = horizontalScrollBar(); + QScrollBar* vBar = verticalScrollBar(); + + QSize desiredScrollArea = QSize(width() - verticalHeader()->width(), height() - horizontalHeader()->height()); + + // Compensate for corner gap... + if (hBar->isVisible() && vBar->isVisible()) { + desiredScrollArea -= QSize(vBar->width(), hBar->height()); + } + + hBar->parentWidget()->layout()->setAlignment(Qt::AlignRight); + hBar->setMaximumWidth(desiredScrollArea.width()); + hBar->setMinimumWidth(desiredScrollArea.width()); + + + vBar->parentWidget()->layout()->setAlignment(Qt::AlignBottom); + vBar->setMaximumHeight(desiredScrollArea.height()); + vBar->setMinimumHeight(desiredScrollArea.height()); +} + void TimelineFramesView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { QTableView::currentChanged(current, previous); if (previous.column() != current.column()) { m_d->model->setData(previous, false, TimelineFramesModel::ActiveFrameRole); m_d->model->setData(current, true, TimelineFramesModel::ActiveFrameRole); } } QItemSelectionModel::SelectionFlags TimelineFramesView::selectionCommand(const QModelIndex &index, const QEvent *event) const { // WARNING: Copy-pasted from KisNodeView! Please keep in sync! /** * Qt has a bug: when we Ctrl+click on an item, the item's * selections gets toggled on mouse *press*, whereas usually it is * done on mouse *release*. Therefore the user cannot do a * Ctrl+D&D with the default configuration. This code fixes the * problem by manually returning QItemSelectionModel::NoUpdate * flag when the user clicks on an item and returning * QItemSelectionModel::Toggle on release. */ if (event && (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) && index.isValid()) { const QMouseEvent *mevent = static_cast(event); if (mevent->button() == Qt::RightButton && selectionModel()->selectedIndexes().contains(index)) { // Allow calling context menu for multiple layers return QItemSelectionModel::NoUpdate; } if (event->type() == QEvent::MouseButtonPress && (mevent->modifiers() & Qt::ControlModifier)) { return QItemSelectionModel::NoUpdate; } if (event->type() == QEvent::MouseButtonRelease && (mevent->modifiers() & Qt::ControlModifier)) { return QItemSelectionModel::Toggle; } } return QAbstractItemView::selectionCommand(index, event); } void TimelineFramesView::slotSelectionChanged() { int minColumn = std::numeric_limits::max(); int maxColumn = std::numeric_limits::min(); foreach (const QModelIndex &idx, selectedIndexes()) { if (idx.column() > maxColumn) { maxColumn = idx.column(); } if (idx.column() < minColumn) { minColumn = idx.column(); } } KisTimeRange range; if (maxColumn > minColumn) { range = KisTimeRange(minColumn, maxColumn - minColumn + 1); } if (m_d->model->isPlaybackPaused()) { m_d->model->stopPlayback(); } m_d->model->setPlaybackRange(range); } void TimelineFramesView::slotReselectCurrentIndex() { QModelIndex index = currentIndex(); currentChanged(index, index); } void TimelineFramesView::slotEnsureRowVisible(int row) { QModelIndex index = currentIndex(); if (!index.isValid() || row < 0) return; index = m_d->model->index(row, index.column()); scrollTo(index); } void TimelineFramesView::slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (m_d->model->isPlaybackActive()) return; int selectedColumn = -1; for (int j = topLeft.column(); j <= bottomRight.column(); j++) { QVariant value = m_d->model->data( m_d->model->index(topLeft.row(), j), TimelineFramesModel::ActiveFrameRole); if (value.isValid() && value.toBool()) { selectedColumn = j; break; } } QModelIndex index = currentIndex(); if (!index.isValid() && selectedColumn < 0) { return; } if (selectedColumn == -1) { selectedColumn = index.column(); } if (selectedColumn != index.column() && !m_d->dragInProgress) { int row= index.isValid() ? index.row() : 0; selectionModel()->setCurrentIndex(m_d->model->index(row, selectedColumn), QItemSelectionModel::ClearAndSelect); } } void TimelineFramesView::slotHeaderDataChanged(Qt::Orientation orientation, int first, int last) { Q_UNUSED(first); Q_UNUSED(last); if (orientation == Qt::Horizontal) { const int newFps = m_d->model->headerData(0, Qt::Horizontal, TimelineFramesModel::FramesPerSecondRole).toInt(); if (newFps != m_d->fps) { setFramesPerSecond(newFps); } } } void TimelineFramesView::rowsInserted(const QModelIndex& parent, int start, int end) { QTableView::rowsInserted(parent, start, end); } inline bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index) { return (model->flags(index) & Qt::ItemIsDragEnabled); } QStyleOptionViewItem TimelineFramesView::Private::viewOptionsV4() const { QStyleOptionViewItem option = q->viewOptions(); option.locale = q->locale(); option.locale.setNumberOptions(QLocale::OmitGroupSeparator); option.widget = q; return option; } QItemViewPaintPairs TimelineFramesView::Private::draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const { Q_ASSERT(r); QRect &rect = *r; const QRect viewportRect = q->viewport()->rect(); QItemViewPaintPairs ret; for (int i = 0; i < indexes.count(); ++i) { const QModelIndex &index = indexes.at(i); const QRect current = q->visualRect(index); if (current.intersects(viewportRect)) { ret += qMakePair(current, index); rect |= current; } } rect &= viewportRect; return ret; } QPixmap TimelineFramesView::Private::renderToPixmap(const QModelIndexList &indexes, QRect *r) const { Q_ASSERT(r); QItemViewPaintPairs paintPairs = draggablePaintPairs(indexes, r); if (paintPairs.isEmpty()) return QPixmap(); QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); QStyleOptionViewItem option = viewOptionsV4(); option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { option.rect = paintPairs.at(j).first.translated(-r->topLeft()); const QModelIndex ¤t = paintPairs.at(j).second; //adjustViewOptionsForIndex(&option, current); q->itemDelegate(current)->paint(&painter, option, current); } return pixmap; } void TimelineFramesView::startDrag(Qt::DropActions supportedActions) { QModelIndexList indexes = selectionModel()->selectedIndexes(); if (!indexes.isEmpty() && m_d->modifiersCatcher->modifierPressed("offset-frame")) { QVector rows; int leftmostColumn = std::numeric_limits::max(); Q_FOREACH (const QModelIndex &index, indexes) { leftmostColumn = qMin(leftmostColumn, index.column()); if (!rows.contains(index.row())) { rows.append(index.row()); } } const int lastColumn = m_d->model->columnCount() - 1; selectionModel()->clear(); Q_FOREACH (const int row, rows) { QItemSelection sel(m_d->model->index(row, leftmostColumn), m_d->model->index(row, lastColumn)); selectionModel()->select(sel, QItemSelectionModel::Select); } supportedActions = Qt::MoveAction; { QModelIndexList indexes = selectedIndexes(); for(int i = indexes.count() - 1 ; i >= 0; --i) { if (!isIndexDragEnabled(m_d->model, indexes.at(i))) indexes.removeAt(i); } selectionModel()->clear(); if (indexes.count() > 0) { QMimeData *data = m_d->model->mimeData(indexes); if (!data) return; QRect rect; QPixmap pixmap = m_d->renderToPixmap(indexes, &rect); rect.adjust(horizontalOffset(), verticalOffset(), 0, 0); QDrag *drag = new QDrag(this); drag->setPixmap(pixmap); drag->setMimeData(data); drag->setHotSpot(m_d->lastPressedPosition - rect.topLeft()); drag->exec(supportedActions, Qt::MoveAction); setCurrentIndex(currentIndex()); } } } else { /** * Workaround for Qt5's bug: if we start a dragging action right during * Shift-selection, Qt will get crazy. We cannot workaround it easily, * because we would need to fork mouseMoveEvent() for that (where the * decision about drag state is done). So we just abort dragging in that * case. * * BUG:373067 */ if (m_d->lastPressedModifier & Qt::ShiftModifier) { return; } /** * Workaround for Qt5's bugs: * * 1) Qt doesn't treat selection the selection on D&D * correctly, so we save it in advance and restore * afterwards. * * 2) There is a private variable in QAbstractItemView: * QAbstractItemView::Private::currentSelectionStartIndex. * It is initialized *only* when the setCurrentIndex() is called * explicitly on the view object, not on the selection model. * Therefore we should explicitly call setCurrentIndex() after * D&D, even if it already has *correct* value! * * 2) We should also call selectionModel()->select() * explicitly. There are two reasons for it: 1) Qt doesn't * maintain selection over D&D; 2) when reselecting single * element after D&D, Qt goes crazy, because it tries to * read *global* keyboard modifiers. Therefore if we are * dragging with Shift or Ctrl pressed it'll get crazy. So * just reset it explicitly. */ QModelIndexList selectionBefore = selectionModel()->selectedIndexes(); QModelIndex currentBefore = selectionModel()->currentIndex(); // initialize a global status variable m_d->dragWasSuccessful = false; QAbstractItemView::startDrag(supportedActions); QModelIndex newCurrent; QPoint selectionOffset; if (m_d->dragWasSuccessful) { newCurrent = currentIndex(); selectionOffset = QPoint(newCurrent.column() - currentBefore.column(), newCurrent.row() - currentBefore.row()); } else { newCurrent = currentBefore; selectionOffset = QPoint(); } setCurrentIndex(newCurrent); selectionModel()->clearSelection(); Q_FOREACH (const QModelIndex &idx, selectionBefore) { QModelIndex newIndex = model()->index(idx.row() + selectionOffset.y(), idx.column() + selectionOffset.x()); selectionModel()->select(newIndex, QItemSelectionModel::Select); } } } void TimelineFramesView::dragEnterEvent(QDragEnterEvent *event) { m_d->dragInProgress = true; m_d->model->setScrubState(true); QTableView::dragEnterEvent(event); } void TimelineFramesView::dragMoveEvent(QDragMoveEvent *event) { m_d->dragInProgress = true; m_d->model->setScrubState(true); QTableView::dragMoveEvent(event); if (event->isAccepted()) { QModelIndex index = indexAt(event->pos()); + if (!m_d->model->canDropFrameData(event->mimeData(), index)) { event->ignore(); } else { selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); } } } void TimelineFramesView::dropEvent(QDropEvent *event) { m_d->dragInProgress = false; m_d->model->setScrubState(false); if (event->keyboardModifiers() & Qt::ControlModifier) { event->setDropAction(Qt::CopyAction); } QAbstractItemView::dropEvent(event); m_d->dragWasSuccessful = event->isAccepted(); } void TimelineFramesView::dragLeaveEvent(QDragLeaveEvent *event) { m_d->dragInProgress = false; m_d->model->setScrubState(false); QAbstractItemView::dragLeaveEvent(event); } void TimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions) { slotUpdateFrameActions(); // calculate if selection range is set. This will determine if the update playback range is available QSet rows; int minColumn = 0; int maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); bool selectionExists = minColumn != maxColumn; menu->addSection(i18n("Edit Frames:")); menu->addSeparator(); if (selectionExists) { KisActionManager::safePopulateMenu(menu, "update_playback_range", m_d->actionMan); } else { KisActionManager::safePopulateMenu(menu, "set_start_time", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "set_end_time", m_d->actionMan); } menu->addSeparator(); KisActionManager::safePopulateMenu(menu, "cut_frames_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "copy_frames_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "paste_frames_from_clipboard", m_d->actionMan); menu->addSeparator(); { //Frames submenu. QMenu *frames = menu->addMenu(i18nc("@item:inmenu", "Keyframes")); KisActionManager::safePopulateMenu(frames, "insert_keyframe_left", m_d->actionMan); KisActionManager::safePopulateMenu(frames, "insert_keyframe_right", m_d->actionMan); frames->addSeparator(); KisActionManager::safePopulateMenu(frames, "insert_multiple_keyframes", m_d->actionMan); } { //Holds submenu. QMenu *hold = menu->addMenu(i18nc("@item:inmenu", "Hold Frames")); KisActionManager::safePopulateMenu(hold, "insert_hold_frame", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_hold_frame", m_d->actionMan); hold->addSeparator(); KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_frames", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_frames", m_d->actionMan); } menu->addSeparator(); KisActionManager::safePopulateMenu(menu, "remove_frames", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "remove_frames_and_pull", m_d->actionMan); menu->addSeparator(); if (addFrameCreationActions) { KisActionManager::safePopulateMenu(menu, "add_blank_frame", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan); menu->addSeparator(); } } void TimelineFramesView::mousePressEvent(QMouseEvent *event) { QPersistentModelIndex index = indexAt(event->pos()); if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { if (event->button() == Qt::RightButton) { // TODO: try calculate index under mouse cursor even when // it is outside any visible row qreal staticPoint = index.isValid() ? index.column() : currentIndex().column(); m_d->zoomDragButton->beginZoom(event->pos(), staticPoint); } else if (event->button() == Qt::LeftButton) { m_d->initialDragPanPos = event->pos(); m_d->initialDragPanValue = QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()); } event->accept(); } else if (event->button() == Qt::RightButton) { int numSelectedItems = selectionModel()->selectedIndexes().size(); if (index.isValid() && numSelectedItems <= 1 && m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { model()->setData(index, true, TimelineFramesModel::ActiveLayerRole); model()->setData(index, true, TimelineFramesModel::ActiveFrameRole); setCurrentIndex(index); if (model()->data(index, TimelineFramesModel::FrameExistsRole).toBool() || model()->data(index, TimelineFramesModel::SpecialKeyframeExists).toBool()) { { KisSignalsBlocker b(m_d->colorSelector); QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole); int labelIndex = colorLabel.isValid() ? colorLabel.toInt() : 0; m_d->colorSelector->setCurrentIndex(labelIndex); } QMenu menu; createFrameEditingMenuActions(&menu, false); menu.addSeparator(); menu.addAction(m_d->colorSelectorAction); menu.exec(event->globalPos()); } else { { KisSignalsBlocker b(m_d->colorSelector); const int labelIndex = KisImageConfig(true).defaultFrameColorLabel(); m_d->colorSelector->setCurrentIndex(labelIndex); } QMenu menu; createFrameEditingMenuActions(&menu, true); menu.addSeparator(); menu.addAction(m_d->colorSelectorAction); menu.exec(event->globalPos()); } } else if (numSelectedItems > 1) { int labelIndex = -1; bool haveFrames = false; Q_FOREACH(QModelIndex index, selectedIndexes()) { haveFrames |= index.data(TimelineFramesModel::FrameExistsRole).toBool(); QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole); if (colorLabel.isValid()) { if (labelIndex == -1) { // First label labelIndex = colorLabel.toInt(); } else if (labelIndex != colorLabel.toInt()) { // Mixed colors in selection labelIndex = -1; break; } } } if (haveFrames) { KisSignalsBlocker b(m_d->multiframeColorSelector); m_d->multiframeColorSelector->setCurrentIndex(labelIndex); } QMenu menu; createFrameEditingMenuActions(&menu, false); menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "mirror_frames", m_d->actionMan); menu.addSeparator(); menu.addAction(m_d->multiframeColorSelectorAction); menu.exec(event->globalPos()); } } else if (event->button() == Qt::MidButton) { QModelIndex index = model()->buddy(indexAt(event->pos())); if (index.isValid()) { QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // The offset of the headers is needed to get the correct position inside the view. m_d->tip.showTip(this, event->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index); } event->accept(); } else { if (index.isValid()) { m_d->model->setLastClickedIndex(index); } m_d->lastPressedPosition = QPoint(horizontalOffset(), verticalOffset()) + event->pos(); m_d->lastPressedModifier = event->modifiers(); QAbstractItemView::mousePressEvent(event); } } void TimelineFramesView::mouseMoveEvent(QMouseEvent *e) { if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { if (e->buttons() & Qt::RightButton) { m_d->zoomDragButton->continueZoom(e->pos()); } else if (e->buttons() & Qt::LeftButton) { QPoint diff = e->pos() - m_d->initialDragPanPos; QPoint offset = QPoint(m_d->initialDragPanValue.x() - diff.x(), m_d->initialDragPanValue.y() - diff.y()); const int height = m_d->layersHeader->defaultSectionSize(); + if (m_d->initialDragPanValue.x() - diff.x() > horizontalScrollBar()->maximum() || m_d->initialDragPanValue.x() - diff.x() > horizontalScrollBar()->minimum() ){ + KisZoomableScrollBar* zoombar = static_cast(horizontalScrollBar()); + zoombar->overscroll(-diff.x()); + } + horizontalScrollBar()->setValue(offset.x()); verticalScrollBar()->setValue(offset.y() / height); } e->accept(); } else if (e->buttons() == Qt::MidButton) { QModelIndex index = model()->buddy(indexAt(e->pos())); if (index.isValid()) { QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // The offset of the headers is needed to get the correct position inside the view. m_d->tip.showTip(this, e->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index); } e->accept(); } else { m_d->model->setScrubState(true); QTableView::mouseMoveEvent(e); } } void TimelineFramesView::mouseReleaseEvent(QMouseEvent *e) { if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { e->accept(); } else { m_d->model->setScrubState(false); QTableView::mouseReleaseEvent(e); } } void TimelineFramesView::wheelEvent(QWheelEvent *e) { QModelIndex index = currentIndex(); int column= -1; + if (verticalHeader()->rect().contains(verticalHeader()->mapFromGlobal(e->globalPos()))) { + QTableView::wheelEvent(e); + return; + } + if (index.isValid()) { column= index.column() + ((e->delta() > 0) ? 1 : -1); } if (column >= 0 && !m_d->dragInProgress) { setCurrentIndex(m_d->model->index(index.row(), column)); } } +void TimelineFramesView::resizeEvent(QResizeEvent *e) +{ + updateGeometries(); + slotUpdateInfiniteFramesCount(); +} + void TimelineFramesView::slotUpdateLayersMenu() { QAction *action = 0; m_d->existingLayersMenu->clear(); QVariant value = model()->headerData(0, Qt::Vertical, TimelineFramesModel::OtherLayersRole); if (value.isValid()) { TimelineFramesModel::OtherLayersList list = value.value(); int i = 0; Q_FOREACH (const TimelineFramesModel::OtherLayer &l, list) { action = m_d->existingLayersMenu->addAction(l.name); action->setData(i++); } } } void TimelineFramesView::slotUpdateFrameActions() { if (!m_d->actionMan) return; const QModelIndexList editableIndexes = calculateSelectionSpan(false, true); const bool hasEditableFrames = !editableIndexes.isEmpty(); bool hasExistingFrames = false; Q_FOREACH (const QModelIndex &index, editableIndexes) { if (model()->data(index, TimelineFramesModel::FrameExistsRole).toBool()) { hasExistingFrames = true; break; } } auto enableAction = [this] (const QString &id, bool value) { KisAction *action = m_d->actionMan->actionByName(id); KIS_SAFE_ASSERT_RECOVER_RETURN(action); action->setEnabled(value); }; enableAction("add_blank_frame", hasEditableFrames); enableAction("add_duplicate_frame", hasEditableFrames); enableAction("insert_keyframe_left", hasEditableFrames); enableAction("insert_keyframe_right", hasEditableFrames); enableAction("insert_multiple_keyframes", hasEditableFrames); enableAction("remove_frames", hasEditableFrames && hasExistingFrames); enableAction("remove_frames_and_pull", hasEditableFrames); enableAction("insert_hold_frame", hasEditableFrames); enableAction("insert_multiple_hold_frames", hasEditableFrames); enableAction("remove_hold_frame", hasEditableFrames); enableAction("remove_multiple_hold_frames", hasEditableFrames); enableAction("mirror_frames", hasEditableFrames && editableIndexes.size() > 1); enableAction("copy_frames_to_clipboard", true); enableAction("cut_frames_to_clipboard", hasEditableFrames); } void TimelineFramesView::slotSetStartTimeToCurrentPosition() { m_d->model->setFullClipRangeStart(this->currentIndex().column()); } void TimelineFramesView::slotSetEndTimeToCurrentPosition() { m_d->model->setFullClipRangeEnd(this->currentIndex().column()); } void TimelineFramesView::slotUpdatePlackbackRange() { QSet rows; int minColumn = 0; int maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); m_d->model->setFullClipRangeStart(minColumn); m_d->model->setFullClipRangeEnd(maxColumn); } void TimelineFramesView::slotLayerContextMenuRequested(const QPoint &globalPos) { m_d->layerEditingMenu->exec(globalPos); } void TimelineFramesView::slotAddNewLayer() { QModelIndex index = currentIndex(); const int newRow = index.isValid() ? index.row() : 0; model()->insertRow(newRow); } void TimelineFramesView::slotAddExistingLayer(QAction *action) { QVariant value = action->data(); if (value.isValid()) { QModelIndex index = currentIndex(); const int newRow = index.isValid() ? index.row() + 1 : 0; m_d->model->insertOtherLayer(value.toInt(), newRow); } } void TimelineFramesView::slotRemoveLayer() { QModelIndex index = currentIndex(); if (!index.isValid()) return; model()->removeRow(index.row()); } void TimelineFramesView::slotAddBlankFrame() { QModelIndex index = currentIndex(); if (!index.isValid() || !m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { return; } m_d->model->createFrame(index); } void TimelineFramesView::slotAddDuplicateFrame() { QModelIndex index = currentIndex(); if (!index.isValid() || !m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { return; } m_d->model->copyFrame(index); } void TimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const { minColumn = std::numeric_limits::max(); maxColumn = std::numeric_limits::min(); Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) { if (!m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) continue; rows.insert(index.row()); minColumn = qMin(minColumn, index.column()); maxColumn = qMax(maxColumn, index.column()); } } void TimelineFramesView::insertKeyframes(int count, int timing, TimelineDirection direction, bool entireColumn) { QSet rows; int minColumn = 0, maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); if (count <= 0) { //Negative count? Use number of selected frames. count = qMax(1, maxColumn - minColumn + 1); } const int insertionColumn = direction == TimelineDirection::RIGHT ? maxColumn + 1 : minColumn; if (entireColumn) { rows.clear(); for (int i = 0; i < m_d->model->rowCount(); i++) { if (!m_d->model->data(m_d->model->index(i, insertionColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue; rows.insert(i); } } if (!rows.isEmpty()) { #if QT_VERSION >= QT_VERSION_CHECK(5,14,0) m_d->model->insertFrames(insertionColumn, QList(rows.begin(), rows.end()), count, timing); #else m_d->model->insertFrames(insertionColumn, QList::fromSet(rows), count, timing); #endif } } void TimelineFramesView::insertMultipleKeyframes(bool entireColumn) { int count, timing; TimelineDirection direction; if (m_d->insertKeyframeDialog->promptUserSettings(count, timing, direction)) { insertKeyframes(count, timing, direction, entireColumn); } } QModelIndexList TimelineFramesView::calculateSelectionSpan(bool entireColumn, bool editableOnly) const { QModelIndexList indexes; if (entireColumn) { QSet rows; int minColumn = 0; int maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); rows.clear(); for (int i = 0; i < m_d->model->rowCount(); i++) { if (editableOnly && !m_d->model->data(m_d->model->index(i, minColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue; for (int column = minColumn; column <= maxColumn; column++) { indexes << m_d->model->index(i, column); } } } else { Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) { if (!editableOnly || m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { indexes << index; } } } return indexes; } +int TimelineFramesView::estimateLastVisibleColumn() +{ + const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); + const int calculatedIndex = + (horizontalScrollBar()->value() + + m_d->horizontalRuler->width() - 1) / sectionWidth; + return calculatedIndex; +} + +int TimelineFramesView::estimateFirstVisibleColumn() +{ + const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); + const int calculatedIndex = ceil( qreal(horizontalScrollBar()->value()) / sectionWidth ); + return calculatedIndex; +} + +int TimelineFramesView::scrollPositionFromColumn(int column) { + const int sectionWidth = m_d->horizontalRuler->defaultSectionSize(); + return sectionWidth * column; +} + void TimelineFramesView::slotRemoveSelectedFrames(bool entireColumn, bool pull) { const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn); if (!selectedIndices.isEmpty()) { if (pull) { m_d->model->removeFramesAndOffset(selectedIndices); } else { m_d->model->removeFrames(selectedIndices); } } } void TimelineFramesView::insertOrRemoveHoldFrames(int count, bool entireColumn) { QModelIndexList indexes; if (!entireColumn) { Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) { if (m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { indexes << index; } } } else { const int column = selectionModel()->currentIndex().column(); for (int i = 0; i < m_d->model->rowCount(); i++) { const QModelIndex index = m_d->model->index(i, column); if (m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { indexes << index; } } } if (!indexes.isEmpty()) { // add extra columns to the end of the timeline if we are adding hold frames // they will be truncated if we don't do this if (count > 0) { // Scan all the layers and find out what layer has the most keyframes // only keep a reference of layer that has the most keyframes int keyframesInLayerNode = 0; Q_FOREACH (const QModelIndex &index, indexes) { KisNodeSP layerNode = m_d->model->nodeAt(index); KisKeyframeChannel *channel = layerNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) continue; if (keyframesInLayerNode < channel->allKeyframeIds().count()) { keyframesInLayerNode = channel->allKeyframeIds().count(); } } m_d->model->setLastVisibleFrame(m_d->model->columnCount() + count*keyframesInLayerNode); } m_d->model->insertHoldFrames(indexes, count); // bulk adding frames can add too many // trim timeline to clean up extra frames that might have been added slotUpdateInfiniteFramesCount(); } } void TimelineFramesView::insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn) { bool ok = false; const int count = QInputDialog::getInt(this, i18nc("@title:window", "Insert or Remove Hold Frames"), i18nc("@label:spinbox", "Enter number of frames"), insertion ? m_d->insertKeyframeDialog->defaultTimingOfAddedFrames() : m_d->insertKeyframeDialog->defaultNumberOfHoldFramesToRemove(), 1, 10000, 1, &ok); if (ok) { if (insertion) { m_d->insertKeyframeDialog->setDefaultTimingOfAddedFrames(count); insertOrRemoveHoldFrames(count, entireColumn); } else { m_d->insertKeyframeDialog->setDefaultNumberOfHoldFramesToRemove(count); insertOrRemoveHoldFrames(-count, entireColumn); } } } void TimelineFramesView::slotMirrorFrames(bool entireColumn) { const QModelIndexList indexes = calculateSelectionSpan(entireColumn); if (!indexes.isEmpty()) { m_d->model->mirrorFrames(indexes); } } void TimelineFramesView::cutCopyImpl(bool entireColumn, bool copy) { const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, !copy); if (selectedIndices.isEmpty()) return; int minColumn = std::numeric_limits::max(); int minRow = std::numeric_limits::max(); Q_FOREACH (const QModelIndex &index, selectedIndices) { minRow = qMin(minRow, index.row()); minColumn = qMin(minColumn, index.column()); } const QModelIndex baseIndex = m_d->model->index(minRow, minColumn); QMimeData *data = m_d->model->mimeDataExtended(selectedIndices, baseIndex, copy ? TimelineFramesModel::CopyFramesPolicy : TimelineFramesModel::MoveFramesPolicy); if (data) { QClipboard *cb = QApplication::clipboard(); cb->setMimeData(data); } } void TimelineFramesView::slotPasteFrames(bool entireColumn) { const QModelIndex currentIndex = !entireColumn ? this->currentIndex() : m_d->model->index(0, this->currentIndex().column()); if (!currentIndex.isValid()) return; QClipboard *cb = QApplication::clipboard(); const QMimeData *data = cb->mimeData(); if (data && data->hasFormat("application/x-krita-frame")) { bool dataMoved = false; bool result = m_d->model->dropMimeDataExtended(data, Qt::MoveAction, currentIndex, &dataMoved); if (result && dataMoved) { cb->clear(); } } } bool TimelineFramesView::viewportEvent(QEvent *event) { if (event->type() == QEvent::ToolTip && model()) { QHelpEvent *he = static_cast(event); QModelIndex index = model()->buddy(indexAt(he->pos())); if (index.isValid()) { QStyleOptionViewItem option = viewOptions(); option.rect = visualRect(index); // The offset of the headers is needed to get the correct position inside the view. m_d->tip.showTip(this, he->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index); return true; } } return QTableView::viewportEvent(event); } diff --git a/plugins/dockers/animation/timeline_frames_view.h b/plugins/dockers/animation/timeline_frames_view.h index 323fee46b6..741bc11f0a 100644 --- a/plugins/dockers/animation/timeline_frames_view.h +++ b/plugins/dockers/animation/timeline_frames_view.h @@ -1,190 +1,201 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __TIMELINE_FRAMES_VIEW_H #define __TIMELINE_FRAMES_VIEW_H #include #include #include +#include #include "kis_action_manager.h" #include "kritaanimationdocker_export.h" class KisAction; class TimelineWidget; enum TimelineDirection : short { LEFT = -1, BEFORE = -1, RIGHT = 1, AFTER = 1 }; - class KRITAANIMATIONDOCKER_EXPORT TimelineFramesView : public QTableView { Q_OBJECT public: TimelineFramesView(QWidget *parent); ~TimelineFramesView() override; void setModel(QAbstractItemModel *model) override; void updateGeometries() override; void setPinToTimeline(KisAction *action); void setActionManager(KisActionManager *actionManager); public Q_SLOTS: void slotSelectionChanged(); void slotUpdateIcons(); + void slotCanvasUpdate(class KoCanvasBase* canvas); + private Q_SLOTS: void slotUpdateLayersMenu(); void slotUpdateFrameActions(); void slotSetStartTimeToCurrentPosition(); void slotSetEndTimeToCurrentPosition(); void slotUpdatePlackbackRange(); // Layer void slotAddNewLayer(); void slotAddExistingLayer(QAction *action); void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void slotRemoveLayer(); void slotLayerContextMenuRequested(const QPoint &globalPos); // New, Insert and Remove Frames void slotAddBlankFrame(); void slotAddDuplicateFrame(); void slotInsertKeyframeLeft() {insertKeyframes(-1, 1, TimelineDirection::LEFT, false);} void slotInsertKeyframeRight() {insertKeyframes(-1, 1, TimelineDirection::RIGHT, false);} void slotInsertKeyframeColumnLeft() {insertKeyframes(-1, 1, TimelineDirection::LEFT, true);} void slotInsertKeyframeColumnRight() {insertKeyframes(-1, 1, TimelineDirection::RIGHT, true);} void slotInsertMultipleKeyframes() {insertMultipleKeyframes(false);} void slotInsertMultipleKeyframeColumns() {insertMultipleKeyframes(true);} void slotRemoveSelectedFrames(bool entireColumn = false, bool pull = false); void slotRemoveSelectedFramesAndShift() {slotRemoveSelectedFrames(false, true);} void slotRemoveSelectedColumns() {slotRemoveSelectedFrames(true);} void slotRemoveSelectedColumnsAndShift() {slotRemoveSelectedFrames(true, true);} void slotInsertHoldFrame() {insertOrRemoveHoldFrames(1);} void slotRemoveHoldFrame() {insertOrRemoveHoldFrames(-1);} void slotInsertHoldFrameColumn() {insertOrRemoveHoldFrames(1,true);} void slotRemoveHoldFrameColumn() {insertOrRemoveHoldFrames(-1,true);} void slotInsertMultipleHoldFrames() {insertOrRemoveMultipleHoldFrames(true);} void slotRemoveMultipleHoldFrames() {insertOrRemoveMultipleHoldFrames(false);} void slotInsertMultipleHoldFrameColumns() {insertOrRemoveMultipleHoldFrames(true, true);} void slotRemoveMultipleHoldFrameColumns() {insertOrRemoveMultipleHoldFrames(false, true);} void slotMirrorFrames(bool entireColumn = false); void slotMirrorColumns() {slotMirrorFrames(true);} // Copy-paste void slotCopyFrames() {cutCopyImpl(false, true);} void slotCutFrames() {cutCopyImpl(false, false);} void slotCopyColumns() {cutCopyImpl(true, true);} void slotCutColumns() {cutCopyImpl(true, false);} void slotPasteFrames(bool entireColumn = false); void slotPasteColumns() {slotPasteFrames(true);} void slotReselectCurrentIndex(); void slotUpdateInfiniteFramesCount(); void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last); - void slotZoomButtonPressed(qreal staticPoint); void slotZoomButtonChanged(qreal value); + void slotScrollbarZoom(qreal zoom); + void slotColorLabelChanged(int); void slotEnsureRowVisible(int row); // Audio void slotSelectAudioChannelFile(); void slotAudioChannelMute(bool value); void slotAudioChannelRemove(); void slotUpdateAudioActions(); void slotAudioVolumeChanged(int value); // DragScroll void slotScrollerStateChanged(QScroller::State state); + void slotUpdateDragInfiniteFramesCount(); + + void slotRealignScrollBars(); private: void setFramesPerSecond(int fps); void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const; /* Insert new keyframes/columns. * * count - Number of frames to add. If <0, use number of currently SELECTED frames. * timing - Animation timing of frames to be added (on 1s, 2s, 3s, etc.) * direction - Insert frames before (left) or after (right) selection scrubber. * entireColumn - Create frames on all layers (rows) instead of just the active layer? */ void insertKeyframes(int count = 1, int timing = 1, TimelineDirection direction = TimelineDirection::LEFT, bool entireColumn = false); void insertMultipleKeyframes(bool entireColumn = false); void insertOrRemoveHoldFrames(int count, bool entireColumn = false); void insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn = false); void cutCopyImpl(bool entireColumn, bool copy); void createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions); QModelIndexList calculateSelectionSpan(bool entireColumn, bool editableOnly = true) const; + int estimateLastVisibleColumn(); + int estimateFirstVisibleColumn(); + int scrollPositionFromColumn( int column ); + protected: QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex &index, const QEvent *event) const override; void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; void startDrag(Qt::DropActions supportedActions) override; void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; void dragLeaveEvent(QDragLeaveEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void wheelEvent(QWheelEvent *e) override; + void resizeEvent(QResizeEvent *e) override; void rowsInserted(const QModelIndex &parent, int start, int end) override; bool viewportEvent(QEvent *event) override; private: struct Private; const QScopedPointer m_d; }; #endif /* __TIMELINE_FRAMES_VIEW_H */ diff --git a/plugins/dockers/animation/timeline_ruler_header.cpp b/plugins/dockers/animation/timeline_ruler_header.cpp index ee61db82c0..ee59ee8914 100644 --- a/plugins/dockers/animation/timeline_ruler_header.cpp +++ b/plugins/dockers/animation/timeline_ruler_header.cpp @@ -1,551 +1,561 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_ruler_header.h" #include #include #include #include #include #include #include "kis_time_based_item_model.h" #include "timeline_color_scheme.h" #include "kis_action.h" #include "kis_debug.h" struct TimelineRulerHeader::Private { Private() : fps(12), lastPressSectionIndex(-1) {} int fps; KisTimeBasedItemModel *model; int lastPressSectionIndex; int calcSpanWidth(const int sectionWidth); QModelIndexList prepareFramesSlab(int startCol, int endCol); KisActionManager* actionMan = 0; + const int minSectionSize = 4; + const int maxSectionSize = 72; + const int unitSectionSize = 18; + qreal remainder = 0.0f; }; TimelineRulerHeader::TimelineRulerHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent), m_d(new Private) { setSectionResizeMode(QHeaderView::Fixed); setDefaultSectionSize(18); setMinimumSectionSize(8); } TimelineRulerHeader::~TimelineRulerHeader() { } void TimelineRulerHeader::setActionManager(KisActionManager *actionManager) { m_d->actionMan = actionManager; if (actionManager) { KisAction *action; action = actionManager->createAction("insert_column_left"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertColumnLeft())); action = actionManager->createAction("insert_column_right"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertColumnRight())); action = actionManager->createAction("insert_multiple_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertMultipleColumns())); action = actionManager->createAction("remove_columns_and_pull"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveColumnsAndShift())); action = actionManager->createAction("remove_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveColumns())); action = actionManager->createAction("insert_hold_column"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertHoldColumns())); action = actionManager->createAction("insert_multiple_hold_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigInsertHoldColumnsCustom())); action = actionManager->createAction("remove_hold_column"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveHoldColumns())); action = actionManager->createAction("remove_multiple_hold_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigRemoveHoldColumnsCustom())); action = actionManager->createAction("mirror_columns"); connect(action, SIGNAL(triggered()), SIGNAL(sigMirrorColumns())); action = actionManager->createAction("copy_columns_to_clipboard"); connect(action, SIGNAL(triggered()), SIGNAL(sigCopyColumns())); action = actionManager->createAction("cut_columns_to_clipboard"); connect(action, SIGNAL(triggered()), SIGNAL(sigCutColumns())); action = actionManager->createAction("paste_columns_from_clipboard"); connect(action, SIGNAL(triggered()), SIGNAL(sigPasteColumns())); } } void TimelineRulerHeader::paintEvent(QPaintEvent *e) { QHeaderView::paintEvent(e); // Copied from Qt 4.8... if (count() == 0) return; QPainter painter(viewport()); const QPoint offset = dirtyRegionOffset(); QRect translatedEventRect = e->rect(); translatedEventRect.translate(offset); int start = -1; int end = -1; if (orientation() == Qt::Horizontal) { start = visualIndexAt(translatedEventRect.left()); end = visualIndexAt(translatedEventRect.right()); } else { start = visualIndexAt(translatedEventRect.top()); end = visualIndexAt(translatedEventRect.bottom()); } const bool reverseImpl = orientation() == Qt::Horizontal && isRightToLeft(); if (reverseImpl) { start = (start == -1 ? count() - 1 : start); end = (end == -1 ? 0 : end); } else { start = (start == -1 ? 0 : start); end = (end == -1 ? count() - 1 : end); } int tmp = start; start = qMin(start, end); end = qMax(tmp, end); /////////////////////////////////////////////////// /// Krita specific code. We should update in spans! const int spanStart = start - start % m_d->fps; const int spanEnd = end - end % m_d->fps + m_d->fps - 1; start = spanStart; end = qMin(count() - 1, spanEnd); /// End of Krita specific code /////////////////////////////////////////////////// QRect currentSectionRect; int logical; const int width = viewport()->width(); const int height = viewport()->height(); for (int i = start; i <= end; ++i) { // DK: cannot copy-paste easily... // if (d->isVisualIndexHidden(i)) // continue; painter.save(); logical = logicalIndex(i); if (orientation() == Qt::Horizontal) { currentSectionRect.setRect(sectionViewportPosition(logical), 0, sectionSize(logical), height); } else { currentSectionRect.setRect(0, sectionViewportPosition(logical), width, sectionSize(logical)); } currentSectionRect.translate(offset); QVariant variant = model()->headerData(logical, orientation(), Qt::FontRole); if (variant.isValid() && variant.canConvert()) { QFont sectionFont = qvariant_cast(variant); painter.setFont(sectionFont); } paintSection1(&painter, currentSectionRect, logical); painter.restore(); } } void TimelineRulerHeader::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const { // Base paint event should paint nothing in the sections area Q_UNUSED(painter); Q_UNUSED(rect); Q_UNUSED(logicalIndex); } void TimelineRulerHeader::paintSpan(QPainter *painter, int userFrameId, const QRect &spanRect, bool isIntegralLine, bool isPrevIntegralLine, QStyle *style, const QPalette &palette, const QPen &gridPen) const { painter->fillRect(spanRect, palette.brush(QPalette::Button)); int safeRight = spanRect.right(); QPen oldPen = painter->pen(); painter->setPen(gridPen); int adjustedTop = spanRect.top() + (!isIntegralLine ? spanRect.height() / 2 : 0); painter->drawLine(safeRight, adjustedTop, safeRight, spanRect.bottom()); if (isPrevIntegralLine) { painter->drawLine(spanRect.left() + 1, spanRect.top(), spanRect.left() + 1, spanRect.bottom()); } painter->setPen(oldPen); QString frameIdText = QString::number(userFrameId); QRect textRect(spanRect.topLeft() + QPoint(2, 0), QSize(spanRect.width() - 2, spanRect.height())); QStyleOptionHeader opt; initStyleOption(&opt); QStyle::State state = QStyle::State_None; if (isEnabled()) state |= QStyle::State_Enabled; if (window()->isActiveWindow()) state |= QStyle::State_Active; opt.state |= state; opt.selectedPosition = QStyleOptionHeader::NotAdjacent; opt.textAlignment = Qt::AlignLeft | Qt::AlignTop; opt.rect = textRect; opt.text = frameIdText; style->drawControl(QStyle::CE_HeaderLabel, &opt, painter, this); } int TimelineRulerHeader::Private::calcSpanWidth(const int sectionWidth) { const int minWidth = 36; int spanWidth = this->fps; while (spanWidth * sectionWidth < minWidth) { spanWidth *= 2; } bool splitHappened = false; do { splitHappened = false; if (!(spanWidth & 0x1) && spanWidth * sectionWidth / 2 > minWidth) { spanWidth /= 2; splitHappened = true; } else if (!(spanWidth % 3) && spanWidth * sectionWidth / 3 > minWidth) { spanWidth /= 3; splitHappened = true; } else if (!(spanWidth % 5) && spanWidth * sectionWidth / 5 > minWidth) { spanWidth /= 5; splitHappened = true; } } while (splitHappened); if (sectionWidth > minWidth) { spanWidth = 1; } return spanWidth; } void TimelineRulerHeader::paintSection1(QPainter *painter, const QRect &rect, int logicalIndex) const { if (!rect.isValid()) return; QFontMetrics metrics(this->font()); const int textHeight = metrics.height(); QPoint p1 = rect.topLeft() + QPoint(0, textHeight); QPoint p2 = rect.topRight() + QPoint(0, textHeight); QRect frameRect = QRect(p1, QSize(rect.width(), rect.height() - textHeight)); const int width = rect.width(); int spanWidth = m_d->calcSpanWidth(width); const int internalIndex = logicalIndex % spanWidth; const int userFrameId = logicalIndex; const int spanEnd = qMin(count(), logicalIndex + spanWidth); QRect spanRect(rect.topLeft(), QSize(width * (spanEnd - logicalIndex), textHeight)); QStyleOptionViewItem option = viewOptions(); const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); const QColor gridColor = static_cast(gridHint); const QPen gridPen = QPen(gridColor); if (!internalIndex) { bool isIntegralLine = (logicalIndex + spanWidth) % m_d->fps == 0; bool isPrevIntegralLine = logicalIndex % m_d->fps == 0; paintSpan(painter, userFrameId, spanRect, isIntegralLine, isPrevIntegralLine, style(), palette(), gridPen); } { QBrush fillColor = TimelineColorScheme::instance()->headerEmpty(); QVariant activeValue = model()->headerData(logicalIndex, orientation(), KisTimeBasedItemModel::ActiveFrameRole); QVariant cachedValue = model()->headerData(logicalIndex, orientation(), KisTimeBasedItemModel::FrameCachedRole); if (activeValue.isValid() && activeValue.toBool()) { fillColor = TimelineColorScheme::instance()->headerActive(); } else if (cachedValue.isValid() && cachedValue.toBool()) { fillColor = TimelineColorScheme::instance()->headerCachedFrame(); } painter->fillRect(frameRect, fillColor); QVector lines; lines << QLine(p1, p2); lines << QLine(frameRect.topRight(), frameRect.bottomRight()); lines << QLine(frameRect.bottomLeft(), frameRect.bottomRight()); QPen oldPen = painter->pen(); painter->setPen(gridPen); painter->drawLines(lines); painter->setPen(oldPen); } } void TimelineRulerHeader::changeEvent(QEvent *event) { Q_UNUSED(event); updateMinimumSize(); } void TimelineRulerHeader::setFramePerSecond(int fps) { m_d->fps = fps; update(); } bool TimelineRulerHeader::setZoom(qreal zoom) { - const int minSectionSize = 4; - const int unitSectionSize = 18; - - int newSectionSize = zoom * unitSectionSize; - - if (newSectionSize < minSectionSize) { - newSectionSize = minSectionSize; - zoom = qreal(newSectionSize) / unitSectionSize; + qreal newSectionSize = zoom * m_d->unitSectionSize; + + if (newSectionSize < m_d->minSectionSize) { + newSectionSize = m_d->minSectionSize; + zoom = qreal(newSectionSize) / m_d->unitSectionSize; + } else if (newSectionSize > m_d->maxSectionSize) { + newSectionSize = m_d->maxSectionSize; + zoom = qreal(newSectionSize) / m_d->unitSectionSize; } + m_d->remainder = newSectionSize - floor(newSectionSize); + if (newSectionSize != defaultSectionSize()) { setDefaultSectionSize(newSectionSize); return true; } return false; } +qreal TimelineRulerHeader::zoom() { + return (qreal(defaultSectionSize() + m_d->remainder) / m_d->unitSectionSize); +} + void TimelineRulerHeader::updateMinimumSize() { QFontMetrics metrics(this->font()); const int textHeight = metrics.height(); setMinimumSize(0, 1.5 * textHeight); } void TimelineRulerHeader::setModel(QAbstractItemModel *model) { KisTimeBasedItemModel *framesModel = qobject_cast(model); m_d->model = framesModel; QHeaderView::setModel(model); } int getColumnCount(const QModelIndexList &indexes, int *leftmostCol, int *rightmostCol) { QVector columns; int leftmost = std::numeric_limits::max(); int rightmost = std::numeric_limits::min(); Q_FOREACH (const QModelIndex &index, indexes) { leftmost = qMin(leftmost, index.column()); rightmost = qMax(rightmost, index.column()); if (!columns.contains(index.column())) { columns.append(index.column()); } } if (leftmostCol) *leftmostCol = leftmost; if (rightmostCol) *rightmostCol = rightmost; return columns.size(); } void TimelineRulerHeader::mousePressEvent(QMouseEvent *e) { int logical = logicalIndexAt(e->pos()); if (logical != -1) { QModelIndexList selectedIndexes = selectionModel()->selectedIndexes(); int numSelectedColumns = getColumnCount(selectedIndexes, 0, 0); if (e->button() == Qt::RightButton) { if (numSelectedColumns <= 1) { model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole); } /* Fix for safe-assert involving kis_animation_curve_docker. * There should probably be a more elagant way for dealing * with reused timeline_ruler_header instances in other * timeline views instead of simply animation_frame_view. * * This works for now though... */ if(!m_d->actionMan){ return; } QMenu menu; menu.addSection(i18n("Edit Columns:")); menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "cut_columns_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(&menu, "copy_columns_to_clipboard", m_d->actionMan); KisActionManager::safePopulateMenu(&menu, "paste_columns_from_clipboard", m_d->actionMan); menu.addSeparator(); { //Frame Columns Submenu QMenu *frames = menu.addMenu(i18nc("@item:inmenu", "Keyframe Columns")); KisActionManager::safePopulateMenu(frames, "insert_column_left", m_d->actionMan); KisActionManager::safePopulateMenu(frames, "insert_column_right", m_d->actionMan); frames->addSeparator(); KisActionManager::safePopulateMenu(frames, "insert_multiple_columns", m_d->actionMan); } { //Hold Columns Submenu QMenu *hold = menu.addMenu(i18nc("@item:inmenu", "Hold Frame Columns")); KisActionManager::safePopulateMenu(hold, "insert_hold_column", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_hold_column", m_d->actionMan); hold->addSeparator(); KisActionManager::safePopulateMenu(hold, "insert_multiple_hold_columns", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_multiple_hold_columns", m_d->actionMan); } menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "remove_columns", m_d->actionMan); KisActionManager::safePopulateMenu(&menu, "remove_columns_and_pull", m_d->actionMan); if (numSelectedColumns > 1) { menu.addSeparator(); KisActionManager::safePopulateMenu(&menu, "mirror_columns", m_d->actionMan); } menu.exec(e->globalPos()); return; } else if (e->button() == Qt::LeftButton) { m_d->lastPressSectionIndex = logical; model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole); } } QHeaderView::mousePressEvent(e); } void TimelineRulerHeader::mouseMoveEvent(QMouseEvent *e) { int logical = logicalIndexAt(e->pos()); if (logical != -1) { if (e->buttons() & Qt::LeftButton) { m_d->model->setScrubState(true); model()->setHeaderData(logical, orientation(), true, KisTimeBasedItemModel::ActiveFrameRole); if (m_d->lastPressSectionIndex >= 0 && logical != m_d->lastPressSectionIndex && e->modifiers() & Qt::ShiftModifier) { const int minCol = qMin(m_d->lastPressSectionIndex, logical); const int maxCol = qMax(m_d->lastPressSectionIndex, logical); QItemSelection sel(m_d->model->index(0, minCol), m_d->model->index(0, maxCol)); selectionModel()->select(sel, QItemSelectionModel::Columns | QItemSelectionModel::SelectCurrent); } } } QHeaderView::mouseMoveEvent(e); } void TimelineRulerHeader::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { m_d->model->setScrubState(false); } QHeaderView::mouseReleaseEvent(e); } QModelIndexList TimelineRulerHeader::Private::prepareFramesSlab(int startCol, int endCol) { QModelIndexList frames; const int numRows = model->rowCount(); for (int i = 0; i < numRows; i++) { for (int j = startCol; j <= endCol; j++) { QModelIndex index = model->index(i, j); const bool exists = model->data(index, KisTimeBasedItemModel::FrameExistsRole).toBool(); if (exists) { frames << index; } } } return frames; } diff --git a/plugins/dockers/animation/timeline_ruler_header.h b/plugins/dockers/animation/timeline_ruler_header.h index 290de58e25..9291bbb0c5 100644 --- a/plugins/dockers/animation/timeline_ruler_header.h +++ b/plugins/dockers/animation/timeline_ruler_header.h @@ -1,90 +1,91 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TIMELINE_RULER_HEADER_H #define TIMELINE_RULER_HEADER_H #include #include #include "kis_action_manager.h" class QPaintEvent; class TimelineRulerHeader : public QHeaderView { Q_OBJECT public: TimelineRulerHeader(QWidget *parent = 0); ~TimelineRulerHeader() override; void setFramePerSecond(int fps); bool setZoom(qreal zoomLevel); + qreal zoom(); void setModel(QAbstractItemModel *model) override; void setActionManager(KisActionManager *actionManager); void mouseMoveEvent(QMouseEvent *e) override; protected: void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void paintEvent(QPaintEvent *e) override; void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override; void paintSection1(QPainter *painter, const QRect &rect, int logicalIndex) const; void changeEvent(QEvent *event) override; private: void updateMinimumSize(); void paintSpan(QPainter *painter, int userFrameId, const QRect &spanRect, bool isIntegralLine, bool isPrevIntegralLine, QStyle *style, const QPalette &palette, const QPen &gridPen) const; Q_SIGNALS: void sigInsertColumnLeft(); void sigInsertColumnRight(); void sigInsertMultipleColumns(); void sigRemoveColumns(); void sigRemoveColumnsAndShift(); void sigInsertHoldColumns(); void sigRemoveHoldColumns(); void sigInsertHoldColumnsCustom(); void sigRemoveHoldColumnsCustom(); void sigMirrorColumns(); void sigCutColumns(); void sigCopyColumns(); void sigPasteColumns(); private: struct Private; const QScopedPointer m_d; }; #endif // TIMELINE_RULER_HEADER_H diff --git a/plugins/dockers/layerdocker/LayerBox.cpp b/plugins/dockers/layerdocker/LayerBox.cpp index 095cc46235..f1a93865af 100644 --- a/plugins/dockers/layerdocker/LayerBox.cpp +++ b/plugins/dockers/layerdocker/LayerBox.cpp @@ -1,1131 +1,1176 @@ /* * LayerBox.cc - part of Krita aka Krayon aka KimageShop * * Copyright (c) 2002 Patrick Julien * Copyright (C) 2006 Gábor Lehel * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * Copyright (c) 2011 José Luis Vergara * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "LayerBox.h" +#include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_action_manager.h" #include "widgets/kis_cmb_composite.h" #include "kis_slider_spin_box.h" #include "KisViewManager.h" #include "kis_node_manager.h" #include "kis_node_model.h" #include "canvas/kis_canvas2.h" #include "kis_dummies_facade_base.h" #include "kis_shape_controller.h" #include "kis_selection_mask.h" #include "kis_config.h" #include "KisView.h" #include "krita_utils.h" #include "kis_color_label_selector_widget.h" #include "kis_signals_blocker.h" #include "kis_color_filter_combo.h" #include "kis_node_filter_proxy_model.h" #include "kis_selection.h" #include "kis_processing_applicator.h" #include "commands/kis_set_global_selection_command.h" #include "KisSelectionActionsAdapter.h" #include "kis_layer_utils.h" #include "ui_WdgLayerBox.h" #include "NodeView.h" #include "SyncButtonAndAction.h" class LayerBoxStyle : public QProxyStyle { public: LayerBoxStyle(QStyle *baseStyle = 0) : QProxyStyle(baseStyle) {} void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const { if (element == QStyle::PE_IndicatorItemViewItemDrop) { QColor color(widget->palette().color(QPalette::Highlight).lighter()); if (option->rect.height() == 0) { QBrush brush(color); QRect r(option->rect); r.setTop(r.top() - 2); r.setBottom(r.bottom() + 2); painter->fillRect(r, brush); } else { color.setAlpha(200); QBrush brush(color); painter->fillRect(option->rect, brush); } } else { QProxyStyle::drawPrimitive(element, option, painter, widget); } } }; inline void LayerBox::connectActionToButton(KisViewManager* viewManager, QAbstractButton *button, const QString &id) { if (!viewManager || !button) return; KisAction *action = viewManager->actionManager()->actionByName(id); if (!action) return; connect(button, SIGNAL(clicked()), action, SLOT(trigger())); connect(action, SIGNAL(sigEnableSlaves(bool)), button, SLOT(setEnabled(bool))); connect(viewManager->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateIcons())); } inline void LayerBox::addActionToMenu(QMenu *menu, const QString &id) { if (m_canvas) { menu->addAction(m_canvas->viewManager()->actionManager()->actionByName(id)); } } LayerBox::LayerBox() : QDockWidget(i18n("Layers")) , m_canvas(0) , m_wdgLayerBox(new Ui_WdgLayerBox) , m_thumbnailCompressor(500, KisSignalCompressor::FIRST_INACTIVE) - , m_colorLabelCompressor(900, KisSignalCompressor::FIRST_INACTIVE) + , m_colorLabelCompressor(500, KisSignalCompressor::FIRST_INACTIVE) , m_thumbnailSizeCompressor(100, KisSignalCompressor::FIRST_INACTIVE) { KisConfig cfg(false); QWidget* mainWidget = new QWidget(this); setWidget(mainWidget); m_opacityDelayTimer.setSingleShot(true); m_wdgLayerBox->setupUi(mainWidget); m_wdgLayerBox->listLayers->setStyle(new LayerBoxStyle(m_wdgLayerBox->listLayers->style())); connect(m_wdgLayerBox->listLayers, SIGNAL(contextMenuRequested(QPoint,QModelIndex)), this, SLOT(slotContextMenuRequested(QPoint,QModelIndex))); connect(m_wdgLayerBox->listLayers, SIGNAL(collapsed(QModelIndex)), SLOT(slotCollapsed(QModelIndex))); connect(m_wdgLayerBox->listLayers, SIGNAL(expanded(QModelIndex)), SLOT(slotExpanded(QModelIndex))); connect(m_wdgLayerBox->listLayers, SIGNAL(selectionChanged(QModelIndexList)), SLOT(selectionChanged(QModelIndexList))); slotUpdateIcons(); m_wdgLayerBox->bnDelete->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnRaise->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnLower->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnProperties->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnDuplicate->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnLower->setEnabled(false); m_wdgLayerBox->bnRaise->setEnabled(false); if (cfg.sliderLabels()) { m_wdgLayerBox->opacityLabel->hide(); m_wdgLayerBox->doubleOpacity->setPrefix(QString("%1: ").arg(i18n("Opacity"))); } m_wdgLayerBox->doubleOpacity->setRange(0, 100, 0); m_wdgLayerBox->doubleOpacity->setSuffix(i18n("%")); connect(m_wdgLayerBox->doubleOpacity, SIGNAL(valueChanged(qreal)), SLOT(slotOpacitySliderMoved(qreal))); connect(&m_opacityDelayTimer, SIGNAL(timeout()), SLOT(slotOpacityChanged())); connect(m_wdgLayerBox->cmbComposite, SIGNAL(activated(int)), SLOT(slotCompositeOpChanged(int))); m_newLayerMenu = new QMenu(this); m_wdgLayerBox->bnAdd->setMenu(m_newLayerMenu); m_wdgLayerBox->bnAdd->setPopupMode(QToolButton::MenuButtonPopup); m_nodeModel = new KisNodeModel(this); m_filteringModel = new KisNodeFilterProxyModel(this); m_filteringModel->setNodeModel(m_nodeModel); /** * Connect model updateUI() to enable/disable controls. * Note: nodeActivated() is connected separately in setImage(), because * it needs particular order of calls: first the connection to the * node manager should be called, then updateUI() */ connect(m_nodeModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(modelReset()), SLOT(slotModelReset())); connect(m_nodeModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotForgetAboutSavedNodeBeforeEditSelectionMode())); connect(m_nodeModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(slotForgetAboutSavedNodeBeforeEditSelectionMode())); connect(m_nodeModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(slotForgetAboutSavedNodeBeforeEditSelectionMode())); connect(m_nodeModel, SIGNAL(modelReset()), SLOT(slotForgetAboutSavedNodeBeforeEditSelectionMode())); KisAction *showGlobalSelectionMask = new KisAction(i18n("&Show Global Selection Mask"), this); showGlobalSelectionMask->setObjectName("show-global-selection-mask"); showGlobalSelectionMask->setActivationFlags(KisAction::ACTIVE_IMAGE); showGlobalSelectionMask->setToolTip(i18nc("@info:tooltip", "Shows global selection as a usual selection mask in Layers docker")); showGlobalSelectionMask->setCheckable(true); connect(showGlobalSelectionMask, SIGNAL(triggered(bool)), SLOT(slotEditGlobalSelection(bool))); m_actions.append(showGlobalSelectionMask); showGlobalSelectionMask->setChecked(cfg.showGlobalSelection()); m_colorSelector = new KisColorLabelSelectorWidget(this); + MouseClickIgnore* mouseEater = new MouseClickIgnore(this); + m_colorSelector->installEventFilter(mouseEater); connect(m_colorSelector, SIGNAL(currentIndexChanged(int)), SLOT(slotColorLabelChanged(int))); m_colorSelectorAction = new QWidgetAction(this); m_colorSelectorAction->setDefaultWidget(m_colorSelector); connect(m_nodeModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), &m_colorLabelCompressor, SLOT(start())); m_wdgLayerBox->listLayers->setModel(m_filteringModel); // this connection should be done *after* the setModel() call to // happen later than the internal selection model connect(m_filteringModel.data(), &KisNodeFilterProxyModel::rowsAboutToBeRemoved, this, &LayerBox::slotAboutToRemoveRows); - connect(m_wdgLayerBox->cmbFilter, SIGNAL(selectedColorsChanged()), SLOT(updateLayerFiltering())); + + //LayerFilter Menu + QMenu *layerFilterMenu = new QMenu(this); + m_wdgLayerBox->bnLayerFilters->setMenu(layerFilterMenu); + m_wdgLayerBox->bnLayerFilters->setPopupMode(QToolButton::InstantPopup); + + const QIcon filterIcon = KisIconUtils::loadIcon("view-filter"); + m_wdgLayerBox->bnLayerFilters->setIcon(filterIcon); + QPixmap filterEnabledPixmap = filterIcon.pixmap(64,64); + const QBitmap filterEnabledBitmask = filterEnabledPixmap.mask(); + filterEnabledPixmap.fill(palette().color(QPalette::Highlight)); + filterEnabledPixmap.setMask(filterEnabledBitmask); + const QIcon filterEnabledIcon = QIcon(filterEnabledPixmap); + + layerFilterWidget = new KisLayerFilterWidget(this); + connect(layerFilterWidget, SIGNAL(filteringOptionsChanged()), this, SLOT(updateLayerFiltering())); + connect(layerFilterWidget, &KisLayerFilterWidget::filteringOptionsChanged, [this, filterIcon, filterEnabledIcon](){ + if(layerFilterWidget->isCurrentlyFiltering()) { + m_wdgLayerBox->bnLayerFilters->setIcon(filterEnabledIcon); + } else { + m_wdgLayerBox->bnLayerFilters->setIcon(filterIcon); + } + + m_wdgLayerBox->bnLayerFilters->setSelectedColors(QList::fromSet(layerFilterWidget->getActiveColors())); + m_wdgLayerBox->bnLayerFilters->setTextFilter(layerFilterWidget->hasTextFilter()); + }); + + QWidgetAction *layerFilterMenuAction = new QWidgetAction(this); + layerFilterMenuAction->setDefaultWidget(layerFilterWidget); + layerFilterMenu->addAction(layerFilterMenuAction); setEnabled(false); connect(&m_thumbnailCompressor, SIGNAL(timeout()), SLOT(updateThumbnail())); connect(&m_colorLabelCompressor, SIGNAL(timeout()), SLOT(updateAvailableLabels())); - // set up the configure menu for changing thumbnail size QMenu* configureMenu = new QMenu(this); configureMenu->setStyleSheet("margin: 6px"); configureMenu->addSection(i18n("Thumbnail Size")); m_wdgLayerBox->configureLayerDockerToolbar->setMenu(configureMenu); m_wdgLayerBox->configureLayerDockerToolbar->setIcon(KisIconUtils::loadIcon("configure")); m_wdgLayerBox->configureLayerDockerToolbar->setPopupMode(QToolButton::InstantPopup); // add horizontal slider thumbnailSizeSlider = new QSlider(this); thumbnailSizeSlider->setOrientation(Qt::Horizontal); thumbnailSizeSlider->setRange(20, 80); thumbnailSizeSlider->setValue(cfg.layerThumbnailSize(false)); // grab this from the kritarc thumbnailSizeSlider->setMinimumHeight(20); thumbnailSizeSlider->setMinimumWidth(40); thumbnailSizeSlider->setTickInterval(5); QWidgetAction *sliderAction= new QWidgetAction(this); sliderAction->setDefaultWidget(thumbnailSizeSlider); configureMenu->addAction(sliderAction); connect(thumbnailSizeSlider, SIGNAL(sliderMoved(int)), &m_thumbnailSizeCompressor, SLOT(start())); connect(&m_thumbnailSizeCompressor, SIGNAL(timeout()), SLOT(slotUpdateThumbnailIconSize())); } LayerBox::~LayerBox() { delete m_wdgLayerBox; } void expandNodesRecursively(KisNodeSP root, QPointer filteringModel, NodeView *nodeView) { if (!root) return; if (filteringModel.isNull()) return; if (!nodeView) return; nodeView->blockSignals(true); KisNodeSP node = root->firstChild(); while (node) { QModelIndex idx = filteringModel->indexFromNode(node); if (idx.isValid()) { nodeView->setExpanded(idx, !node->collapsed()); } if (!node->collapsed() && node->childCount() > 0) { expandNodesRecursively(node, filteringModel, nodeView); } node = node->nextSibling(); } nodeView->blockSignals(false); } void LayerBox::slotAddLayerBnClicked() { if (m_canvas) { KisNodeList nodes = m_nodeManager->selectedNodes(); if (nodes.size() == 1) { KisAction *action = m_canvas->viewManager()->actionManager()->actionByName("add_new_paint_layer"); action->trigger(); } else { KisAction *action = m_canvas->viewManager()->actionManager()->actionByName("create_quick_group"); action->trigger(); } } } void LayerBox::setViewManager(KisViewManager* kisview) { m_nodeManager = kisview->nodeManager(); if (m_nodeManager) { connect(m_nodeManager, SIGNAL(sigNodeActivated(KisNodeSP)), SLOT(slotForgetAboutSavedNodeBeforeEditSelectionMode())); } Q_FOREACH (KisAction *action, m_actions) { kisview->actionManager()-> addAction(action->objectName(), action); } connect(m_wdgLayerBox->bnAdd, SIGNAL(clicked()), this, SLOT(slotAddLayerBnClicked())); connectActionToButton(kisview, m_wdgLayerBox->bnDuplicate, "duplicatelayer"); KisActionManager *actionManager = kisview->actionManager(); KisAction *action = actionManager->createAction("RenameCurrentLayer"); Q_ASSERT(action); connect(action, SIGNAL(triggered()), this, SLOT(slotRenameCurrentNode())); m_propertiesAction = actionManager->createAction("layer_properties"); Q_ASSERT(m_propertiesAction); new SyncButtonAndAction(m_propertiesAction, m_wdgLayerBox->bnProperties, this); connect(m_propertiesAction, SIGNAL(triggered()), this, SLOT(slotPropertiesClicked())); m_removeAction = actionManager->createAction("remove_layer"); Q_ASSERT(m_removeAction); new SyncButtonAndAction(m_removeAction, m_wdgLayerBox->bnDelete, this); connect(m_removeAction, SIGNAL(triggered()), this, SLOT(slotRmClicked())); action = actionManager->createAction("move_layer_up"); Q_ASSERT(action); new SyncButtonAndAction(action, m_wdgLayerBox->bnRaise, this); connect(action, SIGNAL(triggered()), this, SLOT(slotRaiseClicked())); action = actionManager->createAction("move_layer_down"); Q_ASSERT(action); new SyncButtonAndAction(action, m_wdgLayerBox->bnLower, this); connect(action, SIGNAL(triggered()), this, SLOT(slotLowerClicked())); m_changeCloneSourceAction = actionManager->createAction("set-copy-from"); Q_ASSERT(m_changeCloneSourceAction); connect(m_changeCloneSourceAction, &KisAction::triggered, this, &LayerBox::slotChangeCloneSourceClicked); } void LayerBox::setCanvas(KoCanvasBase *canvas) { if (m_canvas == canvas) return; setEnabled(canvas != 0); if (m_canvas) { m_canvas->disconnectCanvasObserver(this); m_nodeModel->setDummiesFacade(0, 0, 0, 0, 0); m_selectionActionsAdapter.reset(); if (m_image) { KisImageAnimationInterface *animation = m_image->animationInterface(); animation->disconnect(this); } disconnect(m_image, 0, this, 0); disconnect(m_nodeManager, 0, this, 0); disconnect(m_nodeModel, 0, m_nodeManager, 0); m_nodeManager->slotSetSelectedNodes(KisNodeList()); } m_canvas = dynamic_cast(canvas); if (m_canvas) { m_image = m_canvas->image(); + emit imageChanged(); connect(m_image, SIGNAL(sigImageUpdated(QRect)), &m_thumbnailCompressor, SLOT(start())); KisDocument* doc = static_cast(m_canvas->imageView()->document()); KisShapeController *kritaShapeController = dynamic_cast(doc->shapeController()); KisDummiesFacadeBase *kritaDummiesFacade = static_cast(kritaShapeController); m_selectionActionsAdapter.reset(new KisSelectionActionsAdapter(m_canvas->viewManager()->selectionManager())); m_nodeModel->setDummiesFacade(kritaDummiesFacade, m_image, kritaShapeController, m_selectionActionsAdapter.data(), m_nodeManager); connect(m_image, SIGNAL(sigAboutToBeDeleted()), SLOT(notifyImageDeleted())); connect(m_image, SIGNAL(sigNodeCollapsedChanged()), SLOT(slotNodeCollapsedChanged())); // cold start if (m_nodeManager) { setCurrentNode(m_nodeManager->activeNode()); // Connection KisNodeManager -> LayerBox connect(m_nodeManager, SIGNAL(sigUiNeedChangeActiveNode(KisNodeSP)), this, SLOT(setCurrentNode(KisNodeSP))); connect(m_nodeManager, SIGNAL(sigUiNeedChangeSelectedNodes(QList)), SLOT(slotNodeManagerChangedSelection(QList))); } else { setCurrentNode(m_canvas->imageView()->currentNode()); } // Connection LayerBox -> KisNodeManager (isolate layer) connect(m_nodeModel, SIGNAL(toggleIsolateActiveNode()), m_nodeManager, SLOT(toggleIsolateActiveNode())); KisImageAnimationInterface *animation = m_image->animationInterface(); connect(animation, &KisImageAnimationInterface::sigUiTimeChanged, this, &LayerBox::slotImageTimeChanged); expandNodesRecursively(m_image->rootLayer(), m_filteringModel, m_wdgLayerBox->listLayers); m_wdgLayerBox->listLayers->scrollTo(m_wdgLayerBox->listLayers->currentIndex()); updateAvailableLabels(); addActionToMenu(m_newLayerMenu, "add_new_paint_layer"); addActionToMenu(m_newLayerMenu, "add_new_group_layer"); addActionToMenu(m_newLayerMenu, "add_new_clone_layer"); addActionToMenu(m_newLayerMenu, "add_new_shape_layer"); addActionToMenu(m_newLayerMenu, "add_new_adjustment_layer"); addActionToMenu(m_newLayerMenu, "add_new_fill_layer"); addActionToMenu(m_newLayerMenu, "add_new_file_layer"); m_newLayerMenu->addSeparator(); addActionToMenu(m_newLayerMenu, "add_new_transparency_mask"); addActionToMenu(m_newLayerMenu, "add_new_filter_mask"); addActionToMenu(m_newLayerMenu, "add_new_colorize_mask"); addActionToMenu(m_newLayerMenu, "add_new_transform_mask"); addActionToMenu(m_newLayerMenu, "add_new_selection_mask"); } } void LayerBox::unsetCanvas() { setEnabled(false); if (m_canvas) { m_newLayerMenu->clear(); } m_filteringModel->unsetDummiesFacade(); disconnect(m_image, 0, this, 0); disconnect(m_nodeManager, 0, this, 0); disconnect(m_nodeModel, 0, m_nodeManager, 0); m_nodeManager->slotSetSelectedNodes(KisNodeList()); m_canvas = 0; } void LayerBox::notifyImageDeleted() { setCanvas(0); } void LayerBox::updateUI() { if (!m_canvas) return; if (!m_nodeManager) return; KisNodeSP activeNode = m_nodeManager->activeNode(); if (activeNode != m_activeNode) { if( !m_activeNode.isNull() ) m_activeNode->disconnect(this); m_activeNode = activeNode; if (activeNode) { KisKeyframeChannel *opacityChannel = activeNode->getKeyframeChannel(KisKeyframeChannel::Opacity.id(), false); if (opacityChannel) { watchOpacityChannel(opacityChannel); } else { watchOpacityChannel(0); connect(activeNode.data(), &KisNode::keyframeChannelAdded, this, &LayerBox::slotKeyframeChannelAdded); } } } m_wdgLayerBox->bnRaise->setEnabled(activeNode && activeNode->isEditable(false) && (activeNode->nextSibling() || (activeNode->parent() && activeNode->parent() != m_image->root()))); m_wdgLayerBox->bnLower->setEnabled(activeNode && activeNode->isEditable(false) && (activeNode->prevSibling() || (activeNode->parent() && activeNode->parent() != m_image->root()))); m_wdgLayerBox->doubleOpacity->setEnabled(activeNode && activeNode->isEditable(false)); m_wdgLayerBox->cmbComposite->setEnabled(activeNode && activeNode->isEditable(false)); m_wdgLayerBox->cmbComposite->validate(m_image->colorSpace()); if (activeNode) { if (activeNode->inherits("KisColorizeMask") || activeNode->inherits("KisLayer")) { m_wdgLayerBox->doubleOpacity->setEnabled(true); if (!m_wdgLayerBox->doubleOpacity->isDragging()) { slotSetOpacity(activeNode->opacity() * 100.0 / 255); } const KoCompositeOp* compositeOp = activeNode->compositeOp(); if (compositeOp) { slotSetCompositeOp(compositeOp); } else { m_wdgLayerBox->cmbComposite->setEnabled(false); } const KisGroupLayer *group = qobject_cast(activeNode.data()); bool compositeSelectionActive = !(group && group->passThroughMode()); m_wdgLayerBox->cmbComposite->setEnabled(compositeSelectionActive); } else if (activeNode->inherits("KisMask")) { m_wdgLayerBox->cmbComposite->setEnabled(false); m_wdgLayerBox->doubleOpacity->setEnabled(false); } } } /** * This method is called *only* when non-GUI code requested the * change of the current node */ void LayerBox::setCurrentNode(KisNodeSP node) { m_filteringModel->setActiveNode(node); QModelIndex index = node ? m_filteringModel->indexFromNode(node) : QModelIndex(); m_filteringModel->setData(index, true, KisNodeModel::ActiveRole); updateUI(); } void LayerBox::slotModelReset() { if(m_nodeModel->hasDummiesFacade()) { QItemSelection selection; Q_FOREACH (const KisNodeSP node, m_nodeManager->selectedNodes()) { const QModelIndex &idx = m_filteringModel->indexFromNode(node); if(idx.isValid()){ QItemSelectionRange selectionRange(idx); selection << selectionRange; } } m_wdgLayerBox->listLayers->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); } updateUI(); } void LayerBox::slotSetCompositeOp(const KoCompositeOp* compositeOp) { KoID opId = KoCompositeOpRegistry::instance().getKoID(compositeOp->id()); m_wdgLayerBox->cmbComposite->blockSignals(true); m_wdgLayerBox->cmbComposite->selectCompositeOp(opId); m_wdgLayerBox->cmbComposite->blockSignals(false); } // range: 0-100 void LayerBox::slotSetOpacity(double opacity) { Q_ASSERT(opacity >= 0 && opacity <= 100); m_wdgLayerBox->doubleOpacity->blockSignals(true); m_wdgLayerBox->doubleOpacity->setValue(opacity); m_wdgLayerBox->doubleOpacity->blockSignals(false); } void LayerBox::slotContextMenuRequested(const QPoint &pos, const QModelIndex &index) { KisNodeList nodes = m_nodeManager->selectedNodes(); KisNodeSP activeNode = m_nodeManager->activeNode(); if (nodes.isEmpty() || !activeNode) return; if (m_canvas) { QMenu menu; const bool singleLayer = nodes.size() == 1; if (index.isValid()) { menu.addAction(m_propertiesAction); if (singleLayer) { addActionToMenu(&menu, "layer_style"); } Q_FOREACH(KisNodeSP node, nodes) { if (node && node->inherits("KisCloneLayer")) { menu.addAction(m_changeCloneSourceAction); break; } } { KisSignalsBlocker b(m_colorSelector); m_colorSelector->setCurrentIndex(singleLayer ? activeNode->colorLabelIndex() : -1); } + menu.addAction(m_colorSelectorAction); menu.addSeparator(); addActionToMenu(&menu, "cut_layer_clipboard"); addActionToMenu(&menu, "copy_layer_clipboard"); addActionToMenu(&menu, "paste_layer_from_clipboard"); menu.addAction(m_removeAction); addActionToMenu(&menu, "duplicatelayer"); addActionToMenu(&menu, "merge_layer"); addActionToMenu(&menu, "new_from_visible"); if (singleLayer) { addActionToMenu(&menu, "flatten_image"); addActionToMenu(&menu, "flatten_layer"); } menu.addSeparator(); QMenu *selectMenu = menu.addMenu(i18n("&Select")); addActionToMenu(selectMenu, "select_all_layers"); addActionToMenu(selectMenu, "select_visible_layers"); addActionToMenu(selectMenu, "select_invisible_layers"); addActionToMenu(selectMenu, "select_locked_layers"); addActionToMenu(selectMenu, "select_unlocked_layers"); QMenu *groupMenu = menu.addMenu(i18n("&Group")); addActionToMenu(groupMenu, "create_quick_group"); addActionToMenu(groupMenu, "create_quick_clipping_group"); addActionToMenu(groupMenu, "quick_ungroup"); QMenu *locksMenu = menu.addMenu(i18n("&Toggle Locks && Visibility")); addActionToMenu(locksMenu, "toggle_layer_visibility"); addActionToMenu(locksMenu, "toggle_layer_lock"); addActionToMenu(locksMenu, "toggle_layer_inherit_alpha"); addActionToMenu(locksMenu, "toggle_layer_alpha_lock"); if (singleLayer) { QMenu *addLayerMenu = menu.addMenu(i18n("&Add")); addActionToMenu(addLayerMenu, "add_new_transparency_mask"); addActionToMenu(addLayerMenu, "add_new_filter_mask"); addActionToMenu(addLayerMenu, "add_new_colorize_mask"); addActionToMenu(addLayerMenu, "add_new_transform_mask"); addActionToMenu(addLayerMenu, "add_new_selection_mask"); addLayerMenu->addSeparator(); addActionToMenu(addLayerMenu, "add_new_clone_layer"); QMenu *convertToMenu = menu.addMenu(i18n("&Convert")); addActionToMenu(convertToMenu, "convert_to_paint_layer"); addActionToMenu(convertToMenu, "convert_to_transparency_mask"); addActionToMenu(convertToMenu, "convert_to_filter_mask"); addActionToMenu(convertToMenu, "convert_to_selection_mask"); addActionToMenu(convertToMenu, "convert_to_file_layer"); QMenu *splitAlphaMenu = menu.addMenu(i18n("S&plit Alpha")); addActionToMenu(splitAlphaMenu, "split_alpha_into_mask"); addActionToMenu(splitAlphaMenu, "split_alpha_write"); addActionToMenu(splitAlphaMenu, "split_alpha_save_merged"); } else { QMenu *addLayerMenu = menu.addMenu(i18n("&Add")); addActionToMenu(addLayerMenu, "add_new_clone_layer"); } menu.addSeparator(); addActionToMenu(&menu, "pin_to_timeline"); if (singleLayer) { KisNodeSP node = m_filteringModel->nodeFromIndex(index); if (node && !node->inherits("KisTransformMask")) { addActionToMenu(&menu, "isolate_active_layer"); addActionToMenu(&menu, "isolate_active_group"); } addActionToMenu(&menu, "selectopaque"); } } menu.exec(pos); } } void LayerBox::slotMinimalView() { m_wdgLayerBox->listLayers->setDisplayMode(NodeView::MinimalMode); } void LayerBox::slotDetailedView() { m_wdgLayerBox->listLayers->setDisplayMode(NodeView::DetailedMode); } void LayerBox::slotThumbnailView() { m_wdgLayerBox->listLayers->setDisplayMode(NodeView::ThumbnailMode); } void LayerBox::slotRmClicked() { if (!m_canvas) return; m_nodeManager->removeNode(); } void LayerBox::slotRaiseClicked() { if (!m_canvas) return; m_nodeManager->raiseNode(); } void LayerBox::slotLowerClicked() { if (!m_canvas) return; m_nodeManager->lowerNode(); } void LayerBox::slotPropertiesClicked() { if (!m_canvas) return; if (KisNodeSP active = m_nodeManager->activeNode()) { m_nodeManager->nodeProperties(active); } } void LayerBox::slotChangeCloneSourceClicked() { if (!m_canvas) return; m_nodeManager->changeCloneSource(); } void LayerBox::slotCompositeOpChanged(int index) { Q_UNUSED(index); if (!m_canvas) return; QString compositeOp = m_wdgLayerBox->cmbComposite->selectedCompositeOp().id(); m_nodeManager->nodeCompositeOpChanged(m_nodeManager->activeColorSpace()->compositeOp(compositeOp)); } void LayerBox::slotOpacityChanged() { if (!m_canvas) return; m_blockOpacityUpdate = true; m_nodeManager->nodeOpacityChanged(m_newOpacity); m_blockOpacityUpdate = false; } void LayerBox::slotOpacitySliderMoved(qreal opacity) { m_newOpacity = opacity; m_opacityDelayTimer.start(200); } void LayerBox::slotCollapsed(const QModelIndex &index) { KisNodeSP node = m_filteringModel->nodeFromIndex(index); if (node) { node->setCollapsed(true); } } void LayerBox::slotExpanded(const QModelIndex &index) { KisNodeSP node = m_filteringModel->nodeFromIndex(index); if (node) { node->setCollapsed(false); } } void LayerBox::slotSelectOpaque() { if (!m_canvas) return; QAction *action = m_canvas->viewManager()->actionManager()->actionByName("selectopaque"); if (action) { action->trigger(); } } void LayerBox::slotNodeCollapsedChanged() { expandNodesRecursively(m_image->rootLayer(), m_filteringModel, m_wdgLayerBox->listLayers); } inline bool isSelectionMask(KisNodeSP node) { return dynamic_cast(node.data()); } KisNodeSP LayerBox::findNonHidableNode(KisNodeSP startNode) { if (KisNodeManager::isNodeHidden(startNode, true) && startNode->parent() && !startNode->parent()->parent()) { KisNodeSP node = startNode->prevSibling(); while (node && KisNodeManager::isNodeHidden(node, true)) { node = node->prevSibling(); } if (!node) { node = startNode->nextSibling(); while (node && KisNodeManager::isNodeHidden(node, true)) { node = node->nextSibling(); } } if (!node) { node = m_image->root()->lastChild(); while (node && KisNodeManager::isNodeHidden(node, true)) { node = node->prevSibling(); } } KIS_ASSERT_RECOVER_NOOP(node && "cannot activate any node!"); startNode = node; } return startNode; } void LayerBox::slotEditGlobalSelection(bool showSelections) { KisNodeSP lastActiveNode = m_nodeManager->activeNode(); KisNodeSP activateNode = lastActiveNode; KisSelectionMaskSP globalSelectionMask; if (!showSelections) { activateNode = m_savedNodeBeforeEditSelectionMode ? KisNodeSP(m_savedNodeBeforeEditSelectionMode) : findNonHidableNode(activateNode); } m_nodeModel->setShowGlobalSelection(showSelections); globalSelectionMask = m_image->rootLayer()->selectionMask(); // try to find deactivated, but visible masks if (!globalSelectionMask) { KoProperties properties; properties.setProperty("visible", true); QList masks = m_image->rootLayer()->childNodes(QStringList("KisSelectionMask"), properties); if (!masks.isEmpty()) { globalSelectionMask = dynamic_cast(masks.first().data()); } } // try to find at least any selection mask if (!globalSelectionMask) { KoProperties properties; QList masks = m_image->rootLayer()->childNodes(QStringList("KisSelectionMask"), properties); if (!masks.isEmpty()) { globalSelectionMask = dynamic_cast(masks.first().data()); } } if (globalSelectionMask) { if (showSelections) { activateNode = globalSelectionMask; } } if (activateNode != lastActiveNode) { m_nodeManager->slotNonUiActivatedNode(activateNode); } else if (lastActiveNode) { setCurrentNode(lastActiveNode); } if (showSelections && !globalSelectionMask) { KisProcessingApplicator applicator(m_image, 0, KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Quick Selection Mask")); applicator.applyCommand( new KisLayerUtils::KeepNodesSelectedCommand( m_nodeManager->selectedNodes(), KisNodeList(), lastActiveNode, 0, m_image, false), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KisSetEmptyGlobalSelectionCommand(m_image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KisLayerUtils::SelectGlobalSelectionMask(m_image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.end(); } else if (!showSelections && globalSelectionMask && globalSelectionMask->selection()->selectedRect().isEmpty()) { KisProcessingApplicator applicator(m_image, 0, KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Cancel Quick Selection Mask")); applicator.applyCommand(new KisSetGlobalSelectionCommand(m_image, 0), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.end(); } if (showSelections) { m_savedNodeBeforeEditSelectionMode = lastActiveNode; } } void LayerBox::selectionChanged(const QModelIndexList selection) { if (!m_nodeManager) return; /** * When the user clears the extended selection by clicking on the * empty area of the docker, the selection should be reset on to * the active layer, which might be even unselected(!). */ if (selection.isEmpty() && m_nodeManager->activeNode()) { QModelIndex selectedIndex = m_filteringModel->indexFromNode(m_nodeManager->activeNode()); m_wdgLayerBox->listLayers->selectionModel()-> setCurrentIndex(selectedIndex, QItemSelectionModel::ClearAndSelect); return; } QList selectedNodes; Q_FOREACH (const QModelIndex &idx, selection) { selectedNodes << m_filteringModel->nodeFromIndex(idx); } m_nodeManager->slotSetSelectedNodes(selectedNodes); updateUI(); } void LayerBox::slotAboutToRemoveRows(const QModelIndex &parent, int start, int end) { /** * Qt has changed its behavior when deleting an item. Previously * the selection priority was on the next item in the list, and * now it has shanged to the previous item. Here we just adjust * the selected item after the node removal. Please take care that * this method overrides what was done by the corresponding method * of QItemSelectionModel, which *has already done* its work. That * is why we use (start - 1) and (end + 1) in the activation * condition. * * See bug: https://bugs.kde.org/show_bug.cgi?id=345601 */ QModelIndex currentIndex = m_wdgLayerBox->listLayers->currentIndex(); QAbstractItemModel *model = m_filteringModel; if (currentIndex.isValid() && parent == currentIndex.parent() && currentIndex.row() >= start - 1 && currentIndex.row() <= end + 1) { QModelIndex old = currentIndex; if (model && end < model->rowCount(parent) - 1) // there are rows left below the change currentIndex = model->index(end + 1, old.column(), parent); else if (model && start > 0) // there are rows left above the change currentIndex = model->index(start - 1, old.column(), parent); else // there are no rows left in the table currentIndex = QModelIndex(); if (currentIndex.isValid() && currentIndex != old) { m_wdgLayerBox->listLayers->setCurrentIndex(currentIndex); } } } void LayerBox::slotNodeManagerChangedSelection(const KisNodeList &nodes) { if (!m_nodeManager) return; QModelIndexList newSelection; Q_FOREACH(KisNodeSP node, nodes) { newSelection << m_filteringModel->indexFromNode(node); } QItemSelectionModel *model = m_wdgLayerBox->listLayers->selectionModel(); if (KritaUtils::compareListsUnordered(newSelection, model->selectedIndexes())) { return; } QItemSelection selection; Q_FOREACH(const QModelIndex &idx, newSelection) { selection.select(idx, idx); } model->select(selection, QItemSelectionModel::ClearAndSelect); } void LayerBox::updateThumbnail() { m_wdgLayerBox->listLayers->updateNode(m_wdgLayerBox->listLayers->currentIndex()); } void LayerBox::slotRenameCurrentNode() { m_wdgLayerBox->listLayers->edit(m_wdgLayerBox->listLayers->currentIndex()); } void LayerBox::slotColorLabelChanged(int label) { - KisNodeList nodes = m_nodeManager->selectedNodes(); + KisNodeList selectedNodes = m_nodeManager->selectedNodes(); + + Q_FOREACH(KisNodeSP selectedNode, selectedNodes) { + //Always apply label to selected nodes.. + selectedNode->setColorLabelIndex(label); + + //Apply label only to unlabelled children.. + KisNodeList children = selectedNode->childNodes(QStringList(), KoProperties()); - Q_FOREACH(KisNodeSP node, nodes) { auto applyLabelFunc = - [label](KisNodeSP node) { - node->setColorLabelIndex(label); + [label](KisNodeSP child) { + if (child->colorLabelIndex() == 0) { + child->setColorLabelIndex(label); + } }; - KisLayerUtils::recursiveApplyNodes(node, applyLabelFunc); + Q_FOREACH(KisNodeSP child, children) { + KisLayerUtils::recursiveApplyNodes(child, applyLabelFunc); + } } } void LayerBox::updateAvailableLabels() { if (!m_image) return; - m_wdgLayerBox->cmbFilter->updateAvailableLabels(m_image->root()); + layerFilterWidget->updateColorLabels(m_image->root()); } void LayerBox::updateLayerFiltering() { - m_filteringModel->setAcceptedLabels(m_wdgLayerBox->cmbFilter->selectedColors()); + m_filteringModel->setAcceptedLabels(layerFilterWidget->getActiveColors()); + m_filteringModel->setTextFilter(layerFilterWidget->getTextFilter()); } void LayerBox::slotKeyframeChannelAdded(KisKeyframeChannel *channel) { if (channel->id() == KisKeyframeChannel::Opacity.id()) { watchOpacityChannel(channel); } } void LayerBox::watchOpacityChannel(KisKeyframeChannel *channel) { if (m_opacityChannel) { m_opacityChannel->disconnect(this); } m_opacityChannel = channel; if (m_opacityChannel) { connect(m_opacityChannel, SIGNAL(sigKeyframeAdded(KisKeyframeSP)), this, SLOT(slotOpacityKeyframeChanged(KisKeyframeSP))); connect(m_opacityChannel, SIGNAL(sigKeyframeRemoved(KisKeyframeSP)), this, SLOT(slotOpacityKeyframeChanged(KisKeyframeSP))); connect(m_opacityChannel, SIGNAL(sigKeyframeMoved(KisKeyframeSP)), this, SLOT(slotOpacityKeyframeMoved(KisKeyframeSP))); connect(m_opacityChannel, SIGNAL(sigKeyframeChanged(KisKeyframeSP)), this, SLOT(slotOpacityKeyframeChanged(KisKeyframeSP))); } } void LayerBox::slotOpacityKeyframeChanged(KisKeyframeSP keyframe) { Q_UNUSED(keyframe); if (m_blockOpacityUpdate) return; updateUI(); } void LayerBox::slotOpacityKeyframeMoved(KisKeyframeSP keyframe, int fromTime) { Q_UNUSED(fromTime); slotOpacityKeyframeChanged(keyframe); } void LayerBox::slotImageTimeChanged(int time) { Q_UNUSED(time); updateUI(); } void LayerBox::slotForgetAboutSavedNodeBeforeEditSelectionMode() { m_savedNodeBeforeEditSelectionMode = 0; } void LayerBox::slotUpdateIcons() { m_wdgLayerBox->bnAdd->setIcon(KisIconUtils::loadIcon("addlayer")); m_wdgLayerBox->bnRaise->setIcon(KisIconUtils::loadIcon("arrowupblr")); m_wdgLayerBox->bnDelete->setIcon(KisIconUtils::loadIcon("deletelayer")); m_wdgLayerBox->bnLower->setIcon(KisIconUtils::loadIcon("arrowdown")); m_wdgLayerBox->bnProperties->setIcon(KisIconUtils::loadIcon("properties")); m_wdgLayerBox->bnDuplicate->setIcon(KisIconUtils::loadIcon("duplicatelayer")); // call child function about needing to update icons m_wdgLayerBox->listLayers->slotUpdateIcons(); } void LayerBox::slotUpdateThumbnailIconSize() { KisConfig cfg(false); cfg.setLayerThumbnailSize(thumbnailSizeSlider->value()); // this is a hack to force the layers list to update its display and // re-layout all the layers with the new thumbnail size resize(this->width()+1, this->height()+1); resize(this->width()-1, this->height()-1); } #include "moc_LayerBox.cpp" diff --git a/plugins/dockers/layerdocker/LayerBox.h b/plugins/dockers/layerdocker/LayerBox.h index 799b8ea8a3..766564fbaf 100644 --- a/plugins/dockers/layerdocker/LayerBox.h +++ b/plugins/dockers/layerdocker/LayerBox.h @@ -1,205 +1,209 @@ /* * LayerBox.h - part of Krita aka Krayon aka KimageShop * * Copyright (c) 2002 Patrick Julien * Copyright (C) 2006 Gábor Lehel * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007-2009 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_LAYERBOX_H #define KIS_LAYERBOX_H #include #include #include #include #include #include #include #include #include #include "kis_action.h" #include "KisViewManager.h" #include "kis_mainwindow_observer.h" #include "kis_signal_compressor.h" +#include "kis_layer_filter_widget.h" #include class QModelIndex; typedef QList QModelIndexList; class QMenu; class QAbstractButton; class KoCompositeOp; class KisCanvas2; class KisNodeModel; class KisNodeFilterProxyModel; class Ui_WdgLayerBox; class KisNodeJugglerCompressed; class KisColorLabelSelectorWidget; class QWidgetAction; class KisKeyframeChannel; class KisSelectionActionsAdapter; /** * A widget that shows a visualization of the layer structure. * * The center of the layer box is KisNodeModel, which shows the actual layers. * This widget adds docking functionality and command buttons. * */ class LayerBox : public QDockWidget, public KisMainwindowObserver { Q_OBJECT public: LayerBox(); ~LayerBox() override; QString observerName() override { return "LayerBox"; } /// reimplemented from KisMainwindowObserver void setViewManager(KisViewManager* kisview) override; void setCanvas(KoCanvasBase *canvas) override; void unsetCanvas() override; private Q_SLOTS: void notifyImageDeleted(); void slotContextMenuRequested(const QPoint &pos, const QModelIndex &index); void slotMinimalView(); void slotDetailedView(); void slotThumbnailView(); // From the node manager to the layerbox void slotSetCompositeOp(const KoCompositeOp* compositeOp); void slotSetOpacity(double opacity); void updateUI(); void setCurrentNode(KisNodeSP node); void slotModelReset(); // from the layerbox to the node manager void slotRmClicked(); void slotRaiseClicked(); void slotLowerClicked(); void slotPropertiesClicked(); void slotChangeCloneSourceClicked(); void slotCompositeOpChanged(int index); void slotOpacityChanged(); void slotOpacitySliderMoved(qreal opacity); void slotCollapsed(const QModelIndex &index); void slotExpanded(const QModelIndex &index); void slotSelectOpaque(); void slotNodeCollapsedChanged(); void slotEditGlobalSelection(bool showSelections); void slotRenameCurrentNode(); void slotAboutToRemoveRows(const QModelIndex &parent, int first, int last); void selectionChanged(const QModelIndexList selection); void slotNodeManagerChangedSelection(const QList &nodes); void slotColorLabelChanged(int index); void slotUpdateIcons(); void slotAddLayerBnClicked(); void updateThumbnail(); void updateAvailableLabels(); void updateLayerFiltering(); void slotUpdateThumbnailIconSize(); // Opacity keyframing void slotKeyframeChannelAdded(KisKeyframeChannel *channel); void slotOpacityKeyframeChanged(KisKeyframeSP keyframe); void slotOpacityKeyframeMoved(KisKeyframeSP keyframe, int fromTime); void slotImageTimeChanged(int time); - void slotForgetAboutSavedNodeBeforeEditSelectionMode(); +Q_SIGNALS: + void imageChanged(); + private: inline void connectActionToButton(KisViewManager* view, QAbstractButton *button, const QString &id); inline void addActionToMenu(QMenu *menu, const QString &id); void watchOpacityChannel(KisKeyframeChannel *channel); KisNodeSP findNonHidableNode(KisNodeSP startNode); private: QPointer m_canvas; QScopedPointer m_selectionActionsAdapter; QMenu *m_newLayerMenu; KisImageWSP m_image; QPointer m_nodeModel; QPointer m_filteringModel; QPointer m_nodeManager; QPointer m_colorSelector; QPointer m_colorSelectorAction; Ui_WdgLayerBox* m_wdgLayerBox; QTimer m_opacityDelayTimer; int m_newOpacity; QVector m_actions; KisAction* m_removeAction; KisAction* m_propertiesAction; KisAction* m_changeCloneSourceAction; KisSignalCompressor m_thumbnailCompressor; KisSignalCompressor m_colorLabelCompressor; KisSignalCompressor m_thumbnailSizeCompressor; + KisLayerFilterWidget* layerFilterWidget; QSlider* thumbnailSizeSlider; KisNodeSP m_activeNode; KisNodeWSP m_savedNodeBeforeEditSelectionMode; QPointer m_opacityChannel; bool m_blockOpacityUpdate {false}; }; class LayerBoxFactory : public KoDockFactoryBase { public: LayerBoxFactory() { } QString id() const override { return QString("KisLayerBox"); } QDockWidget* createDockWidget() override { LayerBox * dockWidget = new LayerBox(); dockWidget->setObjectName(id()); return dockWidget; } DockPosition defaultDockPosition() const override { return DockRight; } }; #endif // KIS_LAYERBOX_H diff --git a/plugins/dockers/layerdocker/WdgLayerBox.ui b/plugins/dockers/layerdocker/WdgLayerBox.ui index 6822e8329c..9b3ca1d4e0 100644 --- a/plugins/dockers/layerdocker/WdgLayerBox.ui +++ b/plugins/dockers/layerdocker/WdgLayerBox.ui @@ -1,349 +1,377 @@ WdgLayerBox 0 0 220 384 1 0 0 0 0 0 0 0 0 0 0 + + + 0 + 20 + + Blending Mode Select the blending mode for the layer. - - + + - 22 - 22 + 32 + 20 + + + 32 + 20 + + + + ... + 0 0 Opacity: 0 0 Layer Opacity Adjust the transparency of the layer 0 0 + + + 32 + 32 + + + + + 32 + 32 + + ... 22 22 0 0 2 0 0 28 16777215 28 ... true 28 28 Duplicate layer or mask ... 22 22 true 28 28 Move layer or mask down ... 22 22 true 28 28 Move layer or mask up ... 22 22 true 28 28 View or change the layer properties ... 22 22 true Qt::Horizontal QSizePolicy::Expanding 0 28 28 28 Delete the layer or mask ... 22 22 true - - KisCompositeOpComboBox - QComboBox -
widgets/kis_cmb_composite.h
-
KisDoubleSliderSpinBox QWidget
kis_slider_spin_box.h
+ 1 +
+ + KisCompositeOpComboBox + QComboBox +
widgets/kis_cmb_composite.h
NodeView
NodeView.h
KisToolButton QToolButton
kis_tool_button.h
- KisColorFilterCombo - QComboBox -
kis_color_filter_combo.h
+ KisLayerFilterWidgetToolButton + QToolButton +
libs/ui/widgets/kis_layer_filter_widget.h
cmbComposite bnDuplicate bnLower bnRaise bnProperties bnDelete
diff --git a/plugins/impex/jpeg/krita_jpeg_export.json b/plugins/impex/jpeg/krita_jpeg_export.json index 2a26cf9b65..661dc164d8 100644 --- a/plugins/impex/jpeg/krita_jpeg_export.json +++ b/plugins/impex/jpeg/krita_jpeg_export.json @@ -1,14 +1,14 @@ { "Icon": "", "Id": "Krita JPEG Export Filter", "NoDisplay": "true", "Type": "Service", - "X-KDE-Export": "image/jpeg,jpeg/jfif", + "X-KDE-Export": "image/jpeg", "X-KDE-Library": "kritajpegexport", "X-KDE-ServiceTypes": [ "Krita/FileFilter" ], "X-KDE-Weight": "1", "X-KDE-Extensions" : "jpeg,jpg,jfif" } diff --git a/plugins/impex/jpeg/krita_jpeg_import.json b/plugins/impex/jpeg/krita_jpeg_import.json index 2c6b97ffa4..b243d58657 100644 --- a/plugins/impex/jpeg/krita_jpeg_import.json +++ b/plugins/impex/jpeg/krita_jpeg_import.json @@ -1,14 +1,14 @@ { "Icon": "", "Id": "Krita JPEG Import Filter", "NoDisplay": "true", "Type": "Service", - "X-KDE-Import": "image/jpeg,jpeg/jfif", + "X-KDE-Import": "image/jpeg", "X-KDE-Library": "kritajpegimport", "X-KDE-ServiceTypes": [ "Krita/FileFilter" ], "X-KDE-Weight": "1", "X-KDE-Extensions" : "jpg,jpeg,jfif" } diff --git a/plugins/impex/ora/kis_open_raster_stack_save_visitor.cpp b/plugins/impex/ora/kis_open_raster_stack_save_visitor.cpp index c7b3b6ada0..b9853fe670 100644 --- a/plugins/impex/ora/kis_open_raster_stack_save_visitor.cpp +++ b/plugins/impex/ora/kis_open_raster_stack_save_visitor.cpp @@ -1,182 +1,182 @@ /* * Copyright (c) 2006-2007,2009 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_open_raster_stack_save_visitor.h" #include #include #include #include #include "kis_adjustment_layer.h" #include "filter/kis_filter.h" #include "filter/kis_filter_configuration.h" #include "kis_group_layer.h" #include "kis_paint_layer.h" #include #include "kis_open_raster_save_context.h" #include #include struct KisOpenRasterStackSaveVisitor::Private { Private() {} KisOpenRasterSaveContext* saveContext; QDomDocument layerStack; QDomElement currentElement; vKisNodeSP activeNodes; }; KisOpenRasterStackSaveVisitor::KisOpenRasterStackSaveVisitor(KisOpenRasterSaveContext* saveContext, vKisNodeSP activeNodes) : d(new Private) { d->saveContext = saveContext; d->activeNodes = activeNodes; } KisOpenRasterStackSaveVisitor::~KisOpenRasterStackSaveVisitor() { delete d; } void KisOpenRasterStackSaveVisitor::saveLayerInfo(QDomElement& elt, KisLayer* layer) { elt.setAttribute("name", layer->name()); elt.setAttribute("opacity", QString().setNum(layer->opacity() / 255.0)); elt.setAttribute("visibility", layer->visible() ? "visible" : "hidden"); - elt.setAttribute("x", QString().setNum(layer->x())); - elt.setAttribute("y", QString().setNum(layer->y())); + elt.setAttribute("x", QString().setNum(layer->exactBounds().x())); + elt.setAttribute("y", QString().setNum(layer->exactBounds().y())); if (layer->userLocked()) { elt.setAttribute("edit-locked", "true"); } if (d->activeNodes.contains(layer)) { elt.setAttribute("selected", "true"); } QString compop = layer->compositeOpId(); if (layer->compositeOpId() == COMPOSITE_CLEAR) compop = "svg:clear"; else if (layer->compositeOpId() == COMPOSITE_ERASE) compop = "svg:dst-out"; else if (layer->compositeOpId() == COMPOSITE_DESTINATION_ATOP) compop = "svg:dst-atop"; else if (layer->compositeOpId() == COMPOSITE_DESTINATION_IN) compop = "svg:dst-in"; else if (layer->compositeOpId() == COMPOSITE_ADD) compop = "svg:plus"; else if (layer->compositeOpId() == COMPOSITE_MULT) compop = "svg:multiply"; else if (layer->compositeOpId() == COMPOSITE_SCREEN) compop = "svg:screen"; else if (layer->compositeOpId() == COMPOSITE_OVERLAY) compop = "svg:overlay"; else if (layer->compositeOpId() == COMPOSITE_DARKEN) compop = "svg:darken"; else if (layer->compositeOpId() == COMPOSITE_LIGHTEN) compop = "svg:lighten"; else if (layer->compositeOpId() == COMPOSITE_DODGE) compop = "svg:color-dodge"; else if (layer->compositeOpId() == COMPOSITE_BURN) compop = "svg:color-burn"; else if (layer->compositeOpId() == COMPOSITE_HARD_LIGHT) compop = "svg:hard-light"; else if (layer->compositeOpId() == COMPOSITE_SOFT_LIGHT_SVG) compop = "svg:soft-light"; else if (layer->compositeOpId() == COMPOSITE_DIFF) compop = "svg:difference"; else if (layer->compositeOpId() == COMPOSITE_COLOR) compop = "svg:color"; else if (layer->compositeOpId() == COMPOSITE_LUMINIZE) compop = "svg:luminosity"; else if (layer->compositeOpId() == COMPOSITE_HUE) compop = "svg:hue"; else if (layer->compositeOpId() == COMPOSITE_SATURATION) compop = "svg:saturation"; // it is important that the check for alphaChannelDisabled (and other non compositeOpId checks) // come before the check for COMPOSITE_OVER, otherwise they will be logically ignored. else if (layer->alphaChannelDisabled()) compop = "svg:src-atop"; else if (layer->compositeOpId() == COMPOSITE_OVER) compop = "svg:src-over"; //else if (layer->compositeOpId() == COMPOSITE_EXCLUSION) compop = "svg:exclusion"; else compop = "krita:" + layer->compositeOpId(); elt.setAttribute("composite-op", compop); } bool KisOpenRasterStackSaveVisitor::visit(KisPaintLayer *layer) { return saveLayer(layer); } bool KisOpenRasterStackSaveVisitor::visit(KisGeneratorLayer* layer) { return saveLayer(layer); } bool KisOpenRasterStackSaveVisitor::visit(KisGroupLayer *layer) { QDomElement previousElt = d->currentElement; QDomElement elt = d->layerStack.createElement("stack"); d->currentElement = elt; saveLayerInfo(elt, layer); QString isolate = "isolate"; if (layer->passThroughMode()) { isolate = "auto"; } elt.setAttribute("isolation", isolate); visitAll(layer); if (!previousElt.isNull()) { previousElt.insertBefore(elt, QDomNode()); d->currentElement = previousElt; } else { QDomElement imageElt = d->layerStack.createElement("image"); int width = layer->image()->width(); int height = layer->image()->height(); int xRes = (int)(qRound(layer->image()->xRes() * 72)); int yRes = (int)(qRound(layer->image()->yRes() * 72)); imageElt.setAttribute("version", "0.0.1"); imageElt.setAttribute("w", width); imageElt.setAttribute("h", height); imageElt.setAttribute("xres", xRes); imageElt.setAttribute("yres", yRes); imageElt.appendChild(elt); d->layerStack.insertBefore(imageElt, QDomNode()); d->currentElement = QDomElement(); d->saveContext->saveStack(d->layerStack); } return true; } bool KisOpenRasterStackSaveVisitor::visit(KisAdjustmentLayer *layer) { QDomElement elt = d->layerStack.createElement("filter"); saveLayerInfo(elt, layer); elt.setAttribute("type", "applications:krita:" + layer->filter()->name()); return true; } bool KisOpenRasterStackSaveVisitor::visit(KisCloneLayer *layer) { return saveLayer(layer); } bool KisOpenRasterStackSaveVisitor::visit(KisExternalLayer * layer) { return saveLayer(layer); } bool KisOpenRasterStackSaveVisitor::saveLayer(KisLayer *layer) { - // here we adjust the bounds to encompass the entire area of the layer with color data by adding the current offsets - QRect adjustedBounds = layer->image()->bounds(); - adjustedBounds.adjust(layer->x(), layer->y(), layer->x(), layer->y()); + // here we adjust the bounds to encompass the entire area of the layer, including transforms + QRect adjustedBounds = layer->exactBounds(); + QString filename = d->saveContext->saveDeviceData(layer->projection(), layer->metaData(), adjustedBounds, layer->image()->xRes(), layer->image()->yRes()); QDomElement elt = d->layerStack.createElement("layer"); saveLayerInfo(elt, layer); elt.setAttribute("src", filename); d->currentElement.insertBefore(elt, QDomNode()); return true; } diff --git a/plugins/tools/basictools/kis_tool_fill.cc b/plugins/tools/basictools/kis_tool_fill.cc index 7f62504f12..52d42db6b0 100644 --- a/plugins/tools/basictools/kis_tool_fill.cc +++ b/plugins/tools/basictools/kis_tool_fill.cc @@ -1,490 +1,525 @@ /* * kis_tool_fill.cc - part of Krayon * * Copyright (c) 2000 John Califf * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Bart Coppens * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_fill.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_resources_snapshot.h" #include "commands_new/KisMergeLabeledLayersCommand.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "KoCompositeOpRegistry.h" KisToolFill::KisToolFill(KoCanvasBase * canvas) : KisToolPaint(canvas, KisCursor::load("tool_fill_cursor.png", 6, 6)) - , m_colorLabelCompressor(900, KisSignalCompressor::FIRST_INACTIVE) + , m_colorLabelCompressor(500, KisSignalCompressor::FIRST_INACTIVE) { setObjectName("tool_fill"); m_feather = 0; m_sizemod = 0; m_threshold = 80; m_usePattern = false; m_fillOnlySelection = false; connect(&m_colorLabelCompressor, SIGNAL(timeout()), SLOT(slotUpdateAvailableColorLabels())); } KisToolFill::~KisToolFill() { } void KisToolFill::resetCursorStyle() { KisToolPaint::resetCursorStyle(); overrideCursorIfNotEditable(); } void KisToolFill::slotUpdateAvailableColorLabels() { if (m_widgetsInitialized && m_cmbSelectedLabels) { m_cmbSelectedLabels->updateAvailableLabels(currentImage()->root()); } } void KisToolFill::activate(ToolActivation toolActivation, const QSet &shapes) { KisToolPaint::activate(toolActivation, shapes); m_configGroup = KSharedConfig::openConfig()->group(toolId()); if (m_widgetsInitialized && m_imageConnections.isEmpty()) { activateConnectionsToImage(); } } void KisToolFill::deactivate() { KisToolPaint::deactivate(); m_imageConnections.clear(); } void KisToolFill::beginPrimaryAction(KoPointerEvent *event) { // cannot use fill tool on non-painting layers. // this logic triggers with multiple layer types like vector layer, clone layer, file layer, group layer if ( currentNode().isNull() || currentNode()->inherits("KisShapeLayer") || nodePaintAbility()!=NodePaintAbility::PAINT ) { KisCanvas2 * kiscanvas = static_cast(canvas()); kiscanvas->viewManager()-> showFloatingMessage( i18n("You cannot use this tool with the selected layer type"), QIcon(), 2000, KisFloatingMessage::Medium, Qt::AlignCenter); event->ignore(); return; } if (!nodeEditable()) { event->ignore(); return; } setMode(KisTool::PAINT_MODE); m_startPos = convertToImagePixelCoordFloored(event); keysAtStart = event->modifiers(); } void KisToolFill::endPrimaryAction(KoPointerEvent *event) { Q_UNUSED(event); CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); setMode(KisTool::HOVER_MODE); if (!currentNode() || (!image()->wrapAroundModePermitted() && !image()->bounds().contains(m_startPos))) { return; } - bool useFastMode = m_useFastMode->isChecked(); + Qt::KeyboardModifiers fillOnlySelectionModifier = Qt::AltModifier; // Not sure where to keep it if (keysAtStart == fillOnlySelectionModifier) { m_fillOnlySelection = true; } keysAtStart = Qt::NoModifier; // libs/ui/tool/kis_tool_select_base.h cleans it up in endPrimaryAction so i do it too KisProcessingApplicator applicator(currentImage(), currentNode(), KisProcessingApplicator::SUPPORTS_WRAPAROUND_MODE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Flood Fill")); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image(), currentNode(), this->canvas()->resourceManager()); KisPaintDeviceSP refPaintDevice = 0; KisImageWSP currentImageWSP = currentImage(); KisNodeSP currentRoot = currentImageWSP->root(); KisImageSP refImage = KisMergeLabeledLayersCommand::createRefImage(image(), "Fill Tool Reference Image"); if (m_sampleLayersMode == SAMPLE_LAYERS_MODE_ALL) { refPaintDevice = currentImage()->projection(); } else if (m_sampleLayersMode == SAMPLE_LAYERS_MODE_CURRENT) { refPaintDevice = currentNode()->paintDevice(); } else if (m_sampleLayersMode == SAMPLE_LAYERS_MODE_COLOR_LABELED) { refPaintDevice = KisMergeLabeledLayersCommand::createRefPaintDevice(image(), "Fill Tool Reference Result Paint Device"); applicator.applyCommand(new KisMergeLabeledLayersCommand(refImage, refPaintDevice, currentRoot, m_selectedColors), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } KIS_ASSERT(refPaintDevice); QTransform transform; transform.rotate(m_patternRotation); transform.scale(m_patternScale, m_patternScale); resources->setFillTransform(transform); KisProcessingVisitorSP visitor = new FillProcessingVisitor(refPaintDevice, m_startPos, resources->activeSelection(), resources, - useFastMode, + m_useFastMode, m_usePattern, m_fillOnlySelection, + m_useSelectionAsBoundary, m_feather, m_sizemod, m_threshold, false, /* use the current device (unmerged) */ false); applicator.applyVisitor(visitor, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.end(); m_fillOnlySelection = m_checkFillSelection->isChecked(); } QWidget* KisToolFill::createOptionWidget() { QWidget *widget = KisToolPaint::createOptionWidget(); widget->setObjectName(toolId() + " option widget"); QLabel *lbl_fastMode = new QLabel(i18n("Fast mode: "), widget); - m_useFastMode = new QCheckBox(QString(), widget); - m_useFastMode->setToolTip( + m_checkUseFastMode = new QCheckBox(QString(), widget); + m_checkUseFastMode->setToolTip( i18n("Fills area faster, but does not take composition " "mode into account. Selections and other extended " "features will also be disabled.")); QLabel *lbl_threshold = new QLabel(i18n("Threshold: "), widget); m_slThreshold = new KisSliderSpinBox(widget); m_slThreshold->setObjectName("int_widget"); m_slThreshold->setRange(1, 100); m_slThreshold->setPageStep(3); QLabel *lbl_sizemod = new QLabel(i18n("Grow selection: "), widget); m_sizemodWidget = new KisSliderSpinBox(widget); m_sizemodWidget->setObjectName("sizemod"); m_sizemodWidget->setRange(-40, 40); m_sizemodWidget->setSingleStep(1); m_sizemodWidget->setSuffix(i18n(" px")); QLabel *lbl_feather = new QLabel(i18n("Feathering radius: "), widget); m_featherWidget = new KisSliderSpinBox(widget); m_featherWidget->setObjectName("feather"); m_featherWidget->setRange(0, 40); m_featherWidget->setSingleStep(1); m_featherWidget->setSuffix(i18n(" px")); QLabel *lbl_usePattern = new QLabel(i18n("Use pattern:"), widget); m_checkUsePattern = new QCheckBox(QString(), widget); m_checkUsePattern->setToolTip(i18n("When checked do not use the foreground color, but the pattern selected to fill with")); QLabel *lbl_patternRotation = new QLabel(i18n("Rotate:"), widget); m_sldPatternRotate = new KisDoubleSliderSpinBox(widget); m_sldPatternRotate->setObjectName("patternrotate"); m_sldPatternRotate->setRange(0, 360, 2); m_sldPatternRotate->setSingleStep(1.0); m_sldPatternRotate->setSuffix(QChar(Qt::Key_degree)); QLabel *lbl_patternScale = new QLabel(i18n("Scale:"), widget); m_sldPatternScale = new KisDoubleSliderSpinBox(widget); m_sldPatternScale->setObjectName("patternscale"); m_sldPatternScale->setRange(0, 500, 2); m_sldPatternScale->setSingleStep(1.0); m_sldPatternScale->setSuffix(QChar(Qt::Key_Percent)); QLabel *lbl_sampleLayers = new QLabel(i18nc("This is a label before a combobox with different choices regarding which layers " "to take into considerationg when calculating the area to fill. " "Options together with the label are: /Sample current layer/ /Sample all layers/ " "/Sample color labeled layers/. Sample is a verb here and means something akin to 'take into account'.", "Sample:"), widget); m_cmbSampleLayersMode = new QComboBox(widget); m_cmbSampleLayersMode->addItem(sampleLayerModeToUserString(SAMPLE_LAYERS_MODE_CURRENT), SAMPLE_LAYERS_MODE_CURRENT); m_cmbSampleLayersMode->addItem(sampleLayerModeToUserString(SAMPLE_LAYERS_MODE_ALL), SAMPLE_LAYERS_MODE_ALL); m_cmbSampleLayersMode->addItem(sampleLayerModeToUserString(SAMPLE_LAYERS_MODE_COLOR_LABELED), SAMPLE_LAYERS_MODE_COLOR_LABELED); m_cmbSampleLayersMode->setEditable(false); QLabel *lbl_cmbLabel = new QLabel(i18nc("This is a string in tool options for Fill Tool to describe a combobox about " "a choice of color labels that a layer can be marked with. Those color labels " "will be used for calculating the area to fill.", "Labels used:"), widget); m_cmbSelectedLabels = new KisColorFilterCombo(widget, false, false); m_cmbSelectedLabels->updateAvailableLabels(currentImage().isNull() ? KisNodeSP() : currentImage()->root()); QLabel *lbl_fillSelection = new QLabel(i18n("Fill entire selection:"), widget); m_checkFillSelection = new QCheckBox(QString(), widget); m_checkFillSelection->setToolTip(i18n("When checked do not look at the current layer colors, but just fill all of the selected area")); - connect (m_useFastMode , SIGNAL(toggled(bool)) , this, SLOT(slotSetUseFastMode(bool))); + QLabel *lbl_useSelectionAsBoundary = new QLabel(i18nc("Description for a checkbox in a Fill Tool to use selection borders as boundary when filling", "Use selection as boundary:"), widget); + m_checkUseSelectionAsBoundary = new QCheckBox(QString(), widget); + m_checkUseSelectionAsBoundary->setToolTip(i18nc("Tooltip for 'Use selection as boundary' checkbox", "When checked, use selection borders as boundary when filling")); + + + + connect (m_checkUseFastMode , SIGNAL(toggled(bool)) , this, SLOT(slotSetUseFastMode(bool))); connect (m_slThreshold , SIGNAL(valueChanged(int)), this, SLOT(slotSetThreshold(int))); connect (m_sizemodWidget , SIGNAL(valueChanged(int)), this, SLOT(slotSetSizemod(int))); connect (m_featherWidget , SIGNAL(valueChanged(int)), this, SLOT(slotSetFeather(int))); connect (m_checkUsePattern , SIGNAL(toggled(bool)) , this, SLOT(slotSetUsePattern(bool))); connect (m_checkFillSelection, SIGNAL(toggled(bool)) , this, SLOT(slotSetFillSelection(bool))); + connect (m_checkUseSelectionAsBoundary, SIGNAL(toggled(bool)) , this, SLOT(slotSetUseSelectionAsBoundary(bool))); + connect (m_cmbSampleLayersMode , SIGNAL(currentIndexChanged(int)), this, SLOT(slotSetSampleLayers(int))); connect (m_cmbSelectedLabels , SIGNAL(selectedColorsChanged()), this, SLOT(slotSetSelectedColorLabels())); connect (m_sldPatternRotate , SIGNAL(valueChanged(qreal)), this, SLOT(slotSetPatternRotation(qreal))); connect (m_sldPatternScale , SIGNAL(valueChanged(qreal)), this, SLOT(slotSetPatternScale(qreal))); - addOptionWidgetOption(m_useFastMode, lbl_fastMode); + addOptionWidgetOption(m_checkUseFastMode, lbl_fastMode); addOptionWidgetOption(m_slThreshold, lbl_threshold); addOptionWidgetOption(m_sizemodWidget , lbl_sizemod); addOptionWidgetOption(m_featherWidget , lbl_feather); addOptionWidgetOption(m_checkFillSelection, lbl_fillSelection); + addOptionWidgetOption(m_checkUseSelectionAsBoundary, lbl_useSelectionAsBoundary); addOptionWidgetOption(m_cmbSampleLayersMode, lbl_sampleLayers); addOptionWidgetOption(m_cmbSelectedLabels, lbl_cmbLabel); addOptionWidgetOption(m_checkUsePattern, lbl_usePattern); addOptionWidgetOption(m_sldPatternRotate, lbl_patternRotation); addOptionWidgetOption(m_sldPatternScale, lbl_patternScale); updateGUI(); widget->setFixedHeight(widget->sizeHint().height()); // load configuration options - m_useFastMode->setChecked(m_configGroup.readEntry("useFastMode", false)); + m_checkUseFastMode->setChecked(m_configGroup.readEntry("useFastMode", false)); m_slThreshold->setValue(m_configGroup.readEntry("thresholdAmount", 80)); m_sizemodWidget->setValue(m_configGroup.readEntry("growSelection", 0)); m_featherWidget->setValue(m_configGroup.readEntry("featherAmount", 0)); m_checkUsePattern->setChecked(m_configGroup.readEntry("usePattern", false)); if (m_configGroup.hasKey("sampleLayersMode")) { m_sampleLayersMode = m_configGroup.readEntry("sampleLayersMode", SAMPLE_LAYERS_MODE_CURRENT); setCmbSampleLayersMode(m_sampleLayersMode); } else { // if neither option is present in the configuration, it will fall back to CURRENT bool sampleMerged = m_configGroup.readEntry("sampleMerged", false); m_sampleLayersMode = sampleMerged ? SAMPLE_LAYERS_MODE_ALL : SAMPLE_LAYERS_MODE_CURRENT; setCmbSampleLayersMode(m_sampleLayersMode); } m_checkFillSelection->setChecked(m_configGroup.readEntry("fillSelection", false)); + m_checkUseSelectionAsBoundary->setChecked(m_configGroup.readEntry("useSelectionAsBoundary", false)); m_sldPatternRotate->setValue(m_configGroup.readEntry("patternRotate", 0.0)); m_sldPatternScale->setValue(m_configGroup.readEntry("patternScale", 100.0)); + // manually set up all variables in case there were no signals when setting value + m_feather = m_featherWidget->value(); + m_sizemod = m_sizemodWidget->value(); + m_threshold = m_slThreshold->value(); + m_useFastMode = m_checkUseFastMode->isChecked(); + m_fillOnlySelection = m_checkFillSelection->isChecked(); + m_useSelectionAsBoundary = m_checkUseSelectionAsBoundary->isChecked(); + m_patternRotation = m_sldPatternRotate->value(); + m_patternScale = m_sldPatternScale->value(); + m_usePattern = m_checkUsePattern->isChecked(); + // m_sampleLayersMode is set manually above + // selectedColors are also set manually + + activateConnectionsToImage(); m_widgetsInitialized = true; return widget; } void KisToolFill::updateGUI() { - bool useAdvancedMode = !m_useFastMode->isChecked(); + bool useAdvancedMode = !m_checkUseFastMode->isChecked(); bool selectionOnly = m_checkFillSelection->isChecked(); - m_useFastMode->setEnabled(!selectionOnly); + m_checkUseFastMode->setEnabled(!selectionOnly); m_slThreshold->setEnabled(!selectionOnly); m_sizemodWidget->setEnabled(!selectionOnly && useAdvancedMode); m_featherWidget->setEnabled(!selectionOnly && useAdvancedMode); m_checkUsePattern->setEnabled(useAdvancedMode); m_sldPatternRotate->setEnabled((m_checkUsePattern->isChecked() && useAdvancedMode)); m_sldPatternScale->setEnabled((m_checkUsePattern->isChecked() && useAdvancedMode)); m_cmbSampleLayersMode->setEnabled(!selectionOnly && useAdvancedMode); + m_checkUseSelectionAsBoundary->setEnabled(!selectionOnly && useAdvancedMode); + bool sampleLayersModeIsColorLabeledLayers = m_cmbSampleLayersMode->currentData().toString() == SAMPLE_LAYERS_MODE_COLOR_LABELED; m_cmbSelectedLabels->setEnabled(!selectionOnly && useAdvancedMode && sampleLayersModeIsColorLabeledLayers); } QString KisToolFill::sampleLayerModeToUserString(QString sampleLayersModeId) { QString currentLayer = i18nc("Option in fill tool: take only the current layer into account when calculating the area to fill", "Current Layer"); if (sampleLayersModeId == SAMPLE_LAYERS_MODE_CURRENT) { return currentLayer; } else if (sampleLayersModeId == SAMPLE_LAYERS_MODE_ALL) { return i18nc("Option in fill tool: take all layers (merged) into account when calculating the area to fill", "All Layers"); } else if (sampleLayersModeId == SAMPLE_LAYERS_MODE_COLOR_LABELED) { return i18nc("Option in fill tool: take all layers that were labeled with a color label (more precisely: all those layers merged)" " into account when calculating the area to fill", "Color Labeled Layers"); } KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(false, currentLayer); return currentLayer; } void KisToolFill::setCmbSampleLayersMode(QString sampleLayersModeId) { for (int i = 0; i < m_cmbSampleLayersMode->count(); i++) { if (m_cmbSampleLayersMode->itemData(i).toString() == sampleLayersModeId) { m_cmbSampleLayersMode->setCurrentIndex(i); break; } } m_sampleLayersMode = sampleLayersModeId; updateGUI(); } void KisToolFill::activateConnectionsToImage() { auto *kisCanvas = dynamic_cast(canvas()); KIS_SAFE_ASSERT_RECOVER_RETURN(kisCanvas); KisDocument *doc = kisCanvas->imageView()->document(); KisShapeController *kritaShapeController = dynamic_cast(doc->shapeController()); m_dummiesFacade = static_cast(kritaShapeController); if (m_dummiesFacade) { m_imageConnections.addConnection(m_dummiesFacade, SIGNAL(sigEndInsertDummy(KisNodeDummy*)), &m_colorLabelCompressor, SLOT(start())); m_imageConnections.addConnection(m_dummiesFacade, SIGNAL(sigEndRemoveDummy()), &m_colorLabelCompressor, SLOT(start())); m_imageConnections.addConnection(m_dummiesFacade, SIGNAL(sigDummyChanged(KisNodeDummy*)), &m_colorLabelCompressor, SLOT(start())); } } void KisToolFill::deactivateConnectionsToImage() { m_imageConnections.clear(); } void KisToolFill::slotSetUseFastMode(bool value) { + m_useFastMode = value; updateGUI(); m_configGroup.writeEntry("useFastMode", value); } void KisToolFill::slotSetThreshold(int threshold) { m_threshold = threshold; m_configGroup.writeEntry("thresholdAmount", threshold); } void KisToolFill::slotSetUsePattern(bool state) { m_usePattern = state; m_sldPatternScale->setEnabled(state); m_sldPatternRotate->setEnabled(state); m_configGroup.writeEntry("usePattern", state); } void KisToolFill::slotSetSampleLayers(int index) { Q_UNUSED(index); m_sampleLayersMode = m_cmbSampleLayersMode->currentData(Qt::UserRole).toString(); updateGUI(); m_configGroup.writeEntry("sampleLayersMode", m_sampleLayersMode); } void KisToolFill::slotSetSelectedColorLabels() { m_selectedColors = m_cmbSelectedLabels->selectedColors(); } void KisToolFill::slotSetPatternScale(qreal scale) { m_patternScale = scale*0.01; m_configGroup.writeEntry("patternScale", scale); } void KisToolFill::slotSetPatternRotation(qreal rotate) { m_patternRotation = rotate; m_configGroup.writeEntry("patternRotate", rotate); } void KisToolFill::slotSetFillSelection(bool state) { m_fillOnlySelection = state; m_configGroup.writeEntry("fillSelection", state); updateGUI(); } +void KisToolFill::slotSetUseSelectionAsBoundary(bool state) +{ + m_useSelectionAsBoundary = state; + m_configGroup.writeEntry("useSelectionAsBoundary", state); + updateGUI(); +} + void KisToolFill::slotSetSizemod(int sizemod) { m_sizemod = sizemod; m_configGroup.writeEntry("growSelection", sizemod); } void KisToolFill::slotSetFeather(int feather) { m_feather = feather; m_configGroup.writeEntry("featherAmount", feather); } diff --git a/plugins/tools/basictools/kis_tool_fill.h b/plugins/tools/basictools/kis_tool_fill.h index 3370614637..9a64f7d90f 100644 --- a/plugins/tools/basictools/kis_tool_fill.h +++ b/plugins/tools/basictools/kis_tool_fill.h @@ -1,154 +1,158 @@ /* * kis_tool_fill.h - part of Krayon^Krita * * Copyright (c) 2004 Bart Coppens * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_TOOL_FILL_H_ #define KIS_TOOL_FILL_H_ #include "kis_tool_paint.h" #include #include #include #include #include #include #include #include #include class QWidget; class QCheckBox; class KisSliderSpinBox; class KisDoubleSliderSpinBox; class KoCanvasBase; class KisColorFilterCombo; class KisDummiesFacadeBase; class KisToolFill : public KisToolPaint { Q_OBJECT public: KisToolFill(KoCanvasBase * canvas); ~KisToolFill() override; void beginPrimaryAction(KoPointerEvent *event) override; void endPrimaryAction(KoPointerEvent *event) override; QWidget * createOptionWidget() override; public Q_SLOTS: void activate(ToolActivation toolActivation, const QSet &shapes) override; void deactivate() override; void slotSetUseFastMode(bool); void slotSetThreshold(int); void slotSetUsePattern(bool); void slotSetFillSelection(bool); + void slotSetUseSelectionAsBoundary(bool); void slotSetSizemod(int); void slotSetFeather(int); void slotSetSampleLayers(int index); void slotSetSelectedColorLabels(); void slotSetPatternScale(qreal scale); void slotSetPatternRotation(qreal rotate); protected Q_SLOTS: void resetCursorStyle() override; void slotUpdateAvailableColorLabels(); protected: bool wantsAutoScroll() const override { return false; } private: void updateGUI(); QString sampleLayerModeToUserString(QString sampleLayersModeId); void setCmbSampleLayersMode(QString sampleLayersModeId); void activateConnectionsToImage(); void deactivateConnectionsToImage(); private: QString SAMPLE_LAYERS_MODE_CURRENT = {"currentLayer"}; QString SAMPLE_LAYERS_MODE_ALL = {"allLayers"}; QString SAMPLE_LAYERS_MODE_COLOR_LABELED = {"colorLabeledLayers"}; private: Qt::KeyboardModifiers keysAtStart; int m_feather; int m_sizemod; QPoint m_startPos; int m_threshold; bool m_usePattern; bool m_fillOnlySelection; + bool m_useSelectionAsBoundary; + bool m_useFastMode; QString m_sampleLayersMode; QList m_selectedColors; qreal m_patternRotation; qreal m_patternScale; bool m_widgetsInitialized {false}; - QCheckBox *m_useFastMode; + QCheckBox *m_checkUseFastMode; KisSliderSpinBox *m_slThreshold; KisSliderSpinBox *m_sizemodWidget; KisSliderSpinBox *m_featherWidget; KisDoubleSliderSpinBox *m_sldPatternRotate; KisDoubleSliderSpinBox *m_sldPatternScale; QCheckBox *m_checkUsePattern; QCheckBox *m_checkFillSelection; + QCheckBox *m_checkUseSelectionAsBoundary; QComboBox *m_cmbSampleLayersMode; KisColorFilterCombo *m_cmbSelectedLabels; KisSignalCompressor m_colorLabelCompressor; KisDummiesFacadeBase* m_dummiesFacade; KisSignalAutoConnectionsStore m_imageConnections; KConfigGroup m_configGroup; }; #include "KisToolPaintFactoryBase.h" class KisToolFillFactory : public KisToolPaintFactoryBase { public: KisToolFillFactory() : KisToolPaintFactoryBase("KritaFill/KisToolFill") { setToolTip(i18n("Fill Tool")); setSection(TOOL_TYPE_FILL); setPriority(0); setActivationShapeId(KRITA_TOOL_ACTIVATION_ID); setIconName(koIconNameCStr("krita_tool_color_fill")); setShortcut( QKeySequence( Qt::Key_F ) ); setPriority(14); } ~KisToolFillFactory() override {} KoToolBase * createTool(KoCanvasBase *canvas) override { return new KisToolFill(canvas); } }; #endif //__filltool_h__ diff --git a/plugins/tools/selectiontools/kis_tool_select_contiguous.cc b/plugins/tools/selectiontools/kis_tool_select_contiguous.cc index 3deba3a300..42267b858c 100644 --- a/plugins/tools/selectiontools/kis_tool_select_contiguous.cc +++ b/plugins/tools/selectiontools/kis_tool_select_contiguous.cc @@ -1,278 +1,307 @@ /* * kis_tool_select_contiguous - part of Krayon^WKrita * * Copyright (c) 1999 Michael Koch * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2012 José Luis Vergara * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_select_contiguous.h" #include #include #include #include #include #include #include #include #include #include "KoPointerEvent.h" #include "KoViewConverter.h" #include "kis_cursor.h" #include "kis_selection_manager.h" #include "kis_image.h" #include "canvas/kis_canvas2.h" #include "kis_layer.h" #include "kis_selection_options.h" #include "kis_paint_device.h" #include "kis_fill_painter.h" #include "kis_pixel_selection.h" #include "kis_selection_tool_helper.h" #include "kis_slider_spin_box.h" #include "tiles3/kis_hline_iterator.h" #include "commands_new/KisMergeLabeledLayersCommand.h" #include "kis_image.h" #include "kis_undo_stores.h" #include "kis_resources_snapshot.h" #include "kis_processing_applicator.h" #include #include "kis_command_utils.h" KisToolSelectContiguous::KisToolSelectContiguous(KoCanvasBase *canvas) : KisToolSelect(canvas, KisCursor::load("tool_contiguous_selection_cursor.png", 6, 6), i18n("Contiguous Area Selection")), m_fuzziness(20), m_sizemod(0), m_feather(0) { setObjectName("tool_select_contiguous"); } KisToolSelectContiguous::~KisToolSelectContiguous() { } void KisToolSelectContiguous::activate(ToolActivation toolActivation, const QSet &shapes) { KisToolSelect::activate(toolActivation, shapes); m_configGroup = KSharedConfig::openConfig()->group(toolId()); } void KisToolSelectContiguous::beginPrimaryAction(KoPointerEvent *event) { KisToolSelectBase::beginPrimaryAction(event); KisPaintDeviceSP dev; if (!currentNode() || !(dev = currentNode()->paintDevice()) || !currentNode()->visible() || !selectionEditable()) { event->ignore(); return; } if (KisToolSelect::selectionDidMove()) { return; } QApplication::setOverrideCursor(KisCursor::waitCursor()); // ------------------------------- KisProcessingApplicator applicator(currentImage(), currentNode(), KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Select Contiguous Area")); QPoint pos = convertToImagePixelCoordFloored(event); QRect rc = currentImage()->bounds(); KisImageSP image = currentImage(); KisPaintDeviceSP sourceDevice; if (sampleLayersMode() == SampleAllLayers) { sourceDevice = image->projection(); } else if (sampleLayersMode() == SampleColorLabeledLayers) { KisImageSP refImage = KisMergeLabeledLayersCommand::createRefImage(image, "Contiguous Selection Tool Reference Image"); sourceDevice = KisMergeLabeledLayersCommand::createRefPaintDevice( image, "Contiguous Selection Tool Reference Result Paint Device"); KisMergeLabeledLayersCommand* command = new KisMergeLabeledLayersCommand(refImage, sourceDevice, image->root(), colorLabelsSelected()); applicator.applyCommand(command, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } else { // Sample Current Layer sourceDevice = dev; } KisPixelSelectionSP selection = KisPixelSelectionSP(new KisPixelSelection(new KisSelectionDefaultBounds(dev))); + bool antiAlias = antiAliasSelection(); int fuzziness = m_fuzziness; int feather = m_feather; int sizemod = m_sizemod; + bool useSelectionAsBoundary = m_useSelectionAsBoundary; + + KisCanvas2 * kisCanvas = dynamic_cast(canvas()); + KIS_SAFE_ASSERT_RECOVER(kisCanvas) { + applicator.cancel(); + QApplication::restoreOverrideCursor(); + return; + }; + + KisPixelSelectionSP existingSelection; + if (kisCanvas->imageView() && kisCanvas->imageView()->selection()) + { + existingSelection = kisCanvas->imageView()->selection()->pixelSelection(); + } KUndo2Command* cmd = new KisCommandUtils::LambdaCommand( - [dev, rc, fuzziness, feather, sizemod, selection, pos, sourceDevice, antiAlias] () mutable -> KUndo2Command* { + [dev, rc, fuzziness, feather, sizemod, useSelectionAsBoundary, + selection, pos, sourceDevice, antiAlias, existingSelection] () mutable -> KUndo2Command* { KisFillPainter fillpainter(dev); fillpainter.setHeight(rc.height()); fillpainter.setWidth(rc.width()); fillpainter.setFillThreshold(fuzziness); fillpainter.setFeather(feather); fillpainter.setSizemod(sizemod); + fillpainter.setUseSelectionAsBoundary((existingSelection.isNull() || existingSelection->isEmpty()) ? false : useSelectionAsBoundary); - fillpainter.createFloodSelection(selection, pos.x(), pos.y(), sourceDevice); + fillpainter.createFloodSelection(selection, pos.x(), pos.y(), sourceDevice, existingSelection); // If we're not antialiasing, threshold the entire selection if (!antiAlias) { const QRect r = selection->selectedExactRect(); KisSequentialIterator it (selection, r); while(it.nextPixel()) { if (*it.rawData() > 0) { *it.rawData() = OPACITY_OPAQUE_U8; } } } selection->invalidateOutlineCache(); return 0; }); applicator.applyCommand(cmd, KisStrokeJobData::SEQUENTIAL); - KisCanvas2 * kisCanvas = dynamic_cast(canvas()); - KIS_SAFE_ASSERT_RECOVER(kisCanvas) { - applicator.cancel(); - QApplication::restoreOverrideCursor(); - return; - }; + KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Select Contiguous Area")); helper.selectPixelSelection(applicator, selection, selectionAction()); applicator.end(); QApplication::restoreOverrideCursor(); } void KisToolSelectContiguous::paint(QPainter &painter, const KoViewConverter &converter) { Q_UNUSED(painter); Q_UNUSED(converter); } void KisToolSelectContiguous::slotSetFuzziness(int fuzziness) { m_fuzziness = fuzziness; m_configGroup.writeEntry("fuzziness", fuzziness); } void KisToolSelectContiguous::slotSetSizemod(int sizemod) { m_sizemod = sizemod; m_configGroup.writeEntry("sizemod", sizemod); } void KisToolSelectContiguous::slotSetFeather(int feather) { m_feather = feather; m_configGroup.writeEntry("feather", feather); } +void KisToolSelectContiguous::slotSetUseSelectionAsBoundary(bool useSelectionAsBoundary) +{ + m_useSelectionAsBoundary = useSelectionAsBoundary; + m_configGroup.writeEntry("useSelectionAsBoundary", useSelectionAsBoundary); +} + QWidget* KisToolSelectContiguous::createOptionWidget() { KisToolSelectBase::createOptionWidget(); KisSelectionOptions *selectionWidget = selectionOptionWidget(); QVBoxLayout * l = dynamic_cast(selectionWidget->layout()); Q_ASSERT(l); if (l) { QGridLayout * gridLayout = new QGridLayout(); l->insertLayout(1, gridLayout); QLabel * lbl = new QLabel(i18n("Fuzziness: "), selectionWidget); gridLayout->addWidget(lbl, 0, 0, 1, 1); KisSliderSpinBox *input = new KisSliderSpinBox(selectionWidget); Q_CHECK_PTR(input); input->setObjectName("fuzziness"); input->setRange(1, 100); input->setSingleStep(1); input->setExponentRatio(2); gridLayout->addWidget(input, 0, 1, 1, 1); lbl = new QLabel(i18n("Grow/shrink selection: "), selectionWidget); gridLayout->addWidget(lbl, 1, 0, 1, 1); KisSliderSpinBox *sizemod = new KisSliderSpinBox(selectionWidget); Q_CHECK_PTR(sizemod); sizemod->setObjectName("sizemod"); //grow/shrink selection sizemod->setRange(-40, 40); sizemod->setSingleStep(1); gridLayout->addWidget(sizemod, 1, 1, 1, 1); lbl = new QLabel(i18n("Feathering radius: "), selectionWidget); gridLayout->addWidget(lbl, 2, 0, 1, 1); KisSliderSpinBox *feather = new KisSliderSpinBox(selectionWidget); Q_CHECK_PTR(feather); feather->setObjectName("feathering"); feather->setRange(0, 40); feather->setSingleStep(1); gridLayout->addWidget(feather, 2, 1, 1, 1); + lbl = new QLabel(i18n("Use selection as boundary: "), selectionWidget); + gridLayout->addWidget(lbl, 3, 0, 1, 1); + + QCheckBox *useSelectionAsBoundary = new QCheckBox(selectionWidget); + Q_CHECK_PTR(useSelectionAsBoundary); + gridLayout->addWidget(useSelectionAsBoundary, 3, 1, 1, 1); + connect (input , SIGNAL(valueChanged(int)), this, SLOT(slotSetFuzziness(int))); connect (sizemod, SIGNAL(valueChanged(int)), this, SLOT(slotSetSizemod(int))); connect (feather, SIGNAL(valueChanged(int)), this, SLOT(slotSetFeather(int))); + connect (useSelectionAsBoundary, SIGNAL(toggled(bool)), this, SLOT(slotSetUseSelectionAsBoundary(bool))); + selectionWidget->attachToImage(image(), dynamic_cast(canvas())); m_widgetHelper.setConfigGroupForExactTool(toolId()); // load configuration settings into tool options input->setValue(m_configGroup.readEntry("fuzziness", 20)); // fuzziness sizemod->setValue( m_configGroup.readEntry("sizemod", 0)); //grow/shrink sizemod->setSuffix(i18n(" px")); feather->setValue(m_configGroup.readEntry("feather", 0)); feather->setSuffix(i18n(" px")); + useSelectionAsBoundary->setChecked(m_configGroup.readEntry("useSelectionAsBoundary", false)); + } return selectionWidget; } void KisToolSelectContiguous::resetCursorStyle() { if (selectionAction() == SELECTION_ADD) { useCursor(KisCursor::load("tool_contiguous_selection_cursor_add.png", 6, 6)); } else if (selectionAction() == SELECTION_SUBTRACT) { useCursor(KisCursor::load("tool_contiguous_selection_cursor_sub.png", 6, 6)); } else { KisToolSelect::resetCursorStyle(); } } diff --git a/plugins/tools/selectiontools/kis_tool_select_contiguous.h b/plugins/tools/selectiontools/kis_tool_select_contiguous.h index 5163e48ac0..6aca20b3f2 100644 --- a/plugins/tools/selectiontools/kis_tool_select_contiguous.h +++ b/plugins/tools/selectiontools/kis_tool_select_contiguous.h @@ -1,96 +1,98 @@ /* * kis_tool_select_contiguous.h - part of KImageShop^WKrayon^Krita * * Copyright (c) 1999 Michael Koch * Copyright (c) 2002 Patrick Julien * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_TOOL_SELECT_CONTIGUOUS_H__ #define __KIS_TOOL_SELECT_CONTIGUOUS_H__ #include "KisSelectionToolFactoryBase.h" #include "kis_tool_select_base.h" #include #include #include /** * The 'magic wand' selection tool -- in fact just * a floodfill that only creates a selection. */ class KisToolSelectContiguous : public KisToolSelect { Q_OBJECT public: KisToolSelectContiguous(KoCanvasBase *canvas); ~KisToolSelectContiguous() override; QWidget* createOptionWidget() override; void paint(QPainter &painter, const KoViewConverter &converter) override; void beginPrimaryAction(KoPointerEvent *event) override; void resetCursorStyle() override; protected: bool wantsAutoScroll() const override { return false; } bool isPixelOnly() const override { return true; } bool usesColorLabels() const override { return true; } public Q_SLOTS: void activate(ToolActivation toolActivation, const QSet &shapes) override; virtual void slotSetFuzziness(int); virtual void slotSetSizemod(int); virtual void slotSetFeather(int); + virtual void slotSetUseSelectionAsBoundary(bool); //virtual bool antiAliasSelection(); protected: using KisToolSelectBase::m_widgetHelper; private: int m_fuzziness; int m_sizemod; int m_feather; + bool m_useSelectionAsBoundary; KConfigGroup m_configGroup; }; class KisToolSelectContiguousFactory : public KisSelectionToolFactoryBase { public: KisToolSelectContiguousFactory() : KisSelectionToolFactoryBase("KisToolSelectContiguous") { setToolTip(i18n("Contiguous Selection Tool")); setSection(TOOL_TYPE_SELECTION); setIconName(koIconNameCStr("tool_contiguous_selection")); setPriority(4); setActivationShapeId(KRITA_TOOL_ACTIVATION_ID); } ~KisToolSelectContiguousFactory() override {} KoToolBase * createTool(KoCanvasBase *canvas) override { return new KisToolSelectContiguous(canvas); } }; #endif //__KIS_TOOL_SELECT_CONTIGUOUS_H__