diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..4f33343242 --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ + + diff --git a/3rdparty/ext_pyqt/CMakeLists.txt b/3rdparty/ext_pyqt/CMakeLists.txt index 09fc7654d0..cb056cddbb 100644 --- a/3rdparty/ext_pyqt/CMakeLists.txt +++ b/3rdparty/ext_pyqt/CMakeLists.txt @@ -1,73 +1,70 @@ SET(PREFIX_ext_pyqt "${EXTPREFIX}" ) if (UNIX) SET(PYTHON_EXECUTABLE_PATH ${PREFIX_ext_pyqt}/bin/python3) if(NOT EXISTS ${PYTHON_EXECUTABLE_PATH}) message("WARNING: using system python3!") SET(PYTHON_EXECUTABLE_PATH python3) endif() list(APPEND _PYQT_conf --confirm-license --qmake ${PREFIX_ext_pyqt}/bin/qmake --sip ${PREFIX_ext_pyqt}/bin/sip --sip-incdir ${PREFIX_ext_pyqt}/include --sipdir ${PREFIX_ext_pyqt}/share/sip ) ExternalProject_Add( ext_pyqt DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} - URL https://www.riverbankcomputing.com/static/Downloads/PyQt5/5.13.1/PyQt5_gpl-5.13.1.tar.gz + URL https://files.kde.org/krita/build/dependencies/PyQt5_gpl-5.13.1.tar.gz URL_MD5 c77af2f4e230b0053fd22bc2ece2e6c0 CONFIGURE_COMMAND ${PYTHON_EXECUTABLE_PATH} /configure.py ${_PYQT_conf} BUILD_COMMAND make # force single-threaded installation to avoid # race conditions in post-install .py script INSTALL_COMMAND make -j1 install BUILD_IN_SOURCE 1 UPDATE_COMMAND "" ) elseif(MINGW) list(APPEND _PYQT_conf --confirm-license --target-py-version 3.8 --bindir ${PREFIX_ext_pyqt}/bin - --qt ${PREFIX_ext_pyqt} --qmake ${PREFIX_ext_pyqt}/bin/qmake.exe --sip ${PREFIX_ext_pyqt}/bin/sip.exe --sip-incdir ${PREFIX_ext_pyqt}/include --spec win32-g++ --verbose --sipdir ${PREFIX_ext_pyqt}/share/sip --destdir ${PREFIX_ext_pyqt}/lib/krita-python-libs --stubsdir ${PREFIX_ext_pyqt}/lib/krita-python-libs/PyQt5 --no-qml-plugin --no-python-dbus --no-qsci-api --no-tools --disable QtSql --disable QtTest --disable QtWinExtras --disable QtHelp --qmake ${PREFIX_ext_pyqt}/bin/qmake.exe ) ExternalProject_Add( ext_pyqt DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} - #URL https://files.kde.org/krita/build/dependencies/PyQt5_gpl-5.12.1.zip - #URL_MD5 0b2912828a4d59e13d86decdce1687e6 - URL https://www.riverbankcomputing.com/static/Downloads/PyQt5/5.12.1/PyQt5_gpl-5.12.1.tar.gz - URL_MD5 67508b652098d2e05c4c2b5baeb170cc + URL https://files.kde.org/krita/build/dependencies/PyQt5_gpl-5.13.1.zip + URL_MD5 bf3c8d54d99a6ee355a309925e403c6e PATCH_COMMAND ${PATCH_COMMAND} -p1 -i ${CMAKE_CURRENT_SOURCE_DIR}/pyqt-configure-fix.patch CONFIGURE_COMMAND ${PYTHON_EXECUTABLE} /configure.py ${_PYQT_conf} BUILD_COMMAND mingw32-make -j${SUBMAKE_JOBS} CXXFLAGS=-D_hypot=hypot LDFLAGS=${SECURITY_SHARED_LINKER_FLAGS} # force single-threaded installation to avoid # race conditions in post-install .py script INSTALL_COMMAND mingw32-make -j1 install BUILD_IN_SOURCE 1 UPDATE_COMMAND "" ) endif() diff --git a/3rdparty/ext_python/CMakeLists.txt b/3rdparty/ext_python/CMakeLists.txt index b906da0c9a..6f50dc49ed 100644 --- a/3rdparty/ext_python/CMakeLists.txt +++ b/3rdparty/ext_python/CMakeLists.txt @@ -1,76 +1,76 @@ SET(PREFIX_ext_python "${EXTPREFIX}" ) if (UNIX) if (APPLE) - set(PYTHON_VERSION "3.8") - ExternalProject_Add( ext_python - DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} - URL https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tar.xz - URL_MD5 b3fb85fd479c0bf950c626ef80cacb57 - - PATCH_COMMAND ${PATCH_COMMAND} -p1 -i ${CMAKE_CURRENT_SOURCE_DIR}/osx_fixappinstall.diff + set(PYTHON_VERSION "3.8") + ExternalProject_Add( ext_python + DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} + URL https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tar.xz + URL_MD5 b3fb85fd479c0bf950c626ef80cacb57 + + PATCH_COMMAND ${PATCH_COMMAND} -p1 -i ${CMAKE_CURRENT_SOURCE_DIR}/osx_fixappinstall.diff - CONFIGURE_COMMAND /configure MACOSX_DEPLOYMENT_TARGET=10.13 --prefix=${PREFIX_ext_python} ${GLOBAL_AUTOMAKE_PROFILE} - --without-ensurepip --enable-framework=${PREFIX_ext_python}/lib --enable-optimizations - BUILD_COMMAND make - INSTALL_COMMAND make install - COMMAND ${CMAKE_COMMAND} -E copy ${PREFIX_ext_python}/bin/python3 ${PREFIX_ext_python}/bin/python - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/sitecustomize.py ${PREFIX_ext_python}/lib/Python.framework/Versions/Current/lib/python${PYTHON_VERSION}/ - COMMAND ${CMAKE_COMMAND} -E create_symlink ./lib/python${PYTHON_VERSION}/site-packages ${PREFIX_ext_python}/lib/Python.framework/Versions/Current/site-packages - # CMake FindPythonLib can't find framework libraries, lack of maintainer for Python - COMMAND find ${PREFIX_ext_python}/lib/Python.framework/Versions/Current/lib -type l -d 1 - | grep -o "[^/]*$" - | xargs -I FILE ${CMAKE_COMMAND} -E create_symlink ./Python.framework/Python ${PREFIX_ext_python}/lib/FILE + CONFIGURE_COMMAND /configure MACOSX_DEPLOYMENT_TARGET=10.13 --prefix=${PREFIX_ext_python} ${GLOBAL_AUTOMAKE_PROFILE} + --without-ensurepip --enable-framework=${PREFIX_ext_python}/lib --enable-optimizations + BUILD_COMMAND make + INSTALL_COMMAND make install + COMMAND ${CMAKE_COMMAND} -E copy ${PREFIX_ext_python}/bin/python3 ${PREFIX_ext_python}/bin/python + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/sitecustomize.py ${PREFIX_ext_python}/lib/Python.framework/Versions/Current/lib/python${PYTHON_VERSION}/ + COMMAND ${CMAKE_COMMAND} -E create_symlink ./lib/python${PYTHON_VERSION}/site-packages ${PREFIX_ext_python}/lib/Python.framework/Versions/Current/site-packages + # CMake FindPythonLib can't find framework libraries, lack of maintainer for Python + COMMAND find ${PREFIX_ext_python}/lib/Python.framework/Versions/Current/lib -type l -d 1 + | grep -o "[^/]*$" + | xargs -I FILE ${CMAKE_COMMAND} -E create_symlink ./Python.framework/Python ${PREFIX_ext_python}/lib/FILE - UPDATE_COMMAND "" - ALWAYS 0 - ) + UPDATE_COMMAND "" + ALWAYS 0 + ) else() ExternalProject_Add( ext_python DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tar.xz URL_MD5 b3fb85fd479c0bf950c626ef80cacb57 CONFIGURE_COMMAND /configure --prefix=${PREFIX_ext_python} ${GLOBAL_AUTOMAKE_PROFILE} --enable-shared BUILD_COMMAND make INSTALL_COMMAND make install COMMAND ${CMAKE_COMMAND} -E copy ${PREFIX_ext_python}/bin/python3 ${PREFIX_ext_python}/bin/python UPDATE_COMMAND "" ALWAYS 0 ) endif() elseif(MINGW) if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") ExternalProject_Add( ext_python DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://www.python.org/ftp/python/3.8.1/python-3.8.1-embed-amd64.zip URL_MD5 4d091857a2153d9406bb5c522b211061 INSTALL_DIR ${PREFIX_ext_python} CONFIGURE_COMMAND "" BUILD_COMMAND ${CMAKE_COMMAND} -E echo deploying python3 64-bit binary INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_directory / ${PREFIX_ext_python}/python COMMAND ${CMAKE_COMMAND} -E copy /python3.dll ${PREFIX_ext_python}/bin COMMAND ${CMAKE_COMMAND} -E copy /python38.dll ${PREFIX_ext_python}/bin COMMAND ${CMAKE_COMMAND} -E copy /vcruntime140.dll ${PREFIX_ext_python}/bin UPDATE_COMMAND "" ) else("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") ExternalProject_Add( ext_python DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://www.python.org/ftp/python/3.8.1/python-3.8.1-embed-win32.zip URL_MD5 980d5745a7e525be5abf4b443a00f734 INSTALL_DIR ${PREFIX_ext_python} CONFIGURE_COMMAND "" BUILD_COMMAND ${CMAKE_COMMAND} -E echo deploying python3 32-bit binary INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_directory / ${PREFIX_ext_python}/python COMMAND ${CMAKE_COMMAND} -E copy /python3.dll ${PREFIX_ext_python}/bin COMMAND ${CMAKE_COMMAND} -E copy /python38.dll ${PREFIX_ext_python}/bin COMMAND ${CMAKE_COMMAND} -E copy /vcruntime140.dll ${PREFIX_ext_python}/bin UPDATE_COMMAND "" ) endif("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") endif() diff --git a/3rdparty/ext_qt/CMakeLists.txt b/3rdparty/ext_qt/CMakeLists.txt index 9d1f023a06..08eb205eb3 100644 --- a/3rdparty/ext_qt/CMakeLists.txt +++ b/3rdparty/ext_qt/CMakeLists.txt @@ -1,300 +1,301 @@ SET(EXTPREFIX_qt "${EXTPREFIX}") if (WIN32) list(APPEND _QT_conf -skip qt3d -skip qtactiveqt -skip qtcanvas3d -skip qtconnectivity -skip qtdoc -skip qtgraphicaleffects -skip qtlocation -skip qtsensors -skip qtserialport -skip qtwayland -skip qtwebchannel -skip qtwebengine -skip qtwebsockets -skip qtwebview -skip qtxmlpatterns -nomake examples -nomake tools -no-compile-examples -no-dbus -no-iconv -no-qml-debug -no-libproxy -no-system-proxies -no-icu -no-mtdev -skip qtcharts -skip qtdatavis3d -skip qtgamepad -skip qtnetworkauth -skip qtpurchasing -skip qtremoteobjects -skip qtscxml -skip qtserialbus -skip qtspeech -skip qtvirtualkeyboard # -qt-zlib -qt-pcre -qt-libpng -qt-libjpeg -openssl-linked -I ${EXTPREFIX_qt}/include -L ${EXTPREFIX_qt}/lib # -opensource -confirm-license # -release -platform win32-g++ -prefix ${EXTPREFIX_qt} QMAKE_LFLAGS_APP+=${SECURITY_EXE_LINKER_FLAGS} QMAKE_LFLAGS_SHLIB+=${SECURITY_SHARED_LINKER_FLAGS} QMAKE_LFLAGS_SONAME+=${SECURITY_SHARED_LINKER_FLAGS} ) if (QT_ENABLE_DEBUG_INFO) # Set the option to build Qt with debugging info enabled list(APPEND _QT_conf -force-debug-info) endif(QT_ENABLE_DEBUG_INFO) if (QT_ENABLE_DYNAMIC_OPENGL) list(APPEND _QT_conf -opengl dynamic -angle) else (QT_ENABLE_DYNAMIC_OPENGL) list(APPEND _QT_conf -opengl desktop -no-angle) endif (QT_ENABLE_DYNAMIC_OPENGL) # MIME-type optimization patches set(ext_qt_PATCH_COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0001-Use-fast-path-for-unsupported-mime-types.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0002-Hack-always-return-we-support-DIBV5.patch ) # Tablet support patches if (NOT USE_QT_TABLET_WINDOWS) set(ext_qt_PATCH_COMMAND ${ext_qt_PATCH_COMMAND} COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0001-disable-wintab.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/disable-winink.patch ) else() set(ext_qt_PATCH_COMMAND ${ext_qt_PATCH_COMMAND} COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0020-Synthesize-Enter-LeaveEvent-for-accepted-QTabletEven.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0023-Implement-a-switch-for-tablet-API-on-Windows.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0024-Fetch-stylus-button-remapping-from-WinTab-driver.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0025-Disable-tablet-relative-mode-in-Qt.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0026-Fetch-mapped-screen-size-from-the-Wintab-driver.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0027-Switch-stylus-pointer-type-when-the-tablet-is-in-the.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0028-Fix-updating-tablet-pressure-resolution-on-every-pro.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0051-Add-workaround-for-handling-table-press-correctly-in.patch ) endif() # HDR patches set(ext_qt_PATCH_COMMAND ${ext_qt_PATCH_COMMAND} COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0003-Implement-openGL-surface-color-space-selection-in-An.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0004-Implement-color-space-selection-for-QSurfaceFormat.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0005-Implement-color-conversion-for-the-backing-store-tex.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0006-Return-QScreen-s-HMONITOR-handle-via-QPlatformNative.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0007-Implement-a-manual-test-for-checking-is-HDR-features.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0008-Fix-notification-of-QDockWidget-when-it-gets-undocke.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0009-Fix-Rec2020-display-format.patch ) # Other patches set(ext_qt_PATCH_COMMAND ${ext_qt_PATCH_COMMAND} COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0060-Windows-Add-a-default-setting-for-hasBorderInFullScr.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0061-Hack-to-hide-1px-border-with-OpenGL-fullscreen-hack.patch COMMAND ${PATCH_COMMAND} -p1 -d qttools -i ${CMAKE_CURRENT_SOURCE_DIR}/windeployqt-force-allow-debug-info.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0080-Sync-buffers-of-the-destination-file-after-QFile-cop.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0081-Fix-no-warning-for-overwriting-files-in-non-native-d.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0082-Make-jp-e-g-default-extensions-context-aware.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0100-Fix-qt5_make_output_file-macro-for-paths-containing-.patch # COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0031-Compute-logical-DPI-on-a-per-screen-basis.patch # COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0032-Update-Dpi-and-scale-factor-computation.patch # COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0033-Move-QT_FONT_DPI-to-cross-platform-code.patch # COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0034-Update-QT_SCREEN_SCALE_FACTORS.patch # COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0035-Deprecate-QT_AUTO_SCREEN_SCALE_FACTOR.patch # COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0036-Add-high-DPI-scale-factor-rounding-policy-C-API.patch ) ExternalProject_Add( ext_qt DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://download.qt.io/official_releases/qt/5.12/5.12.7/single/qt-everywhere-src-5.12.7.zip URL_MD5 753e0147d5e32d61c30d6a1b498bed3e PATCH_COMMAND ${ext_qt_PATCH_COMMAND} INSTALL_DIR ${EXTPREFIX_qt} CONFIGURE_COMMAND /configure.bat ${_QT_conf} BUILD_COMMAND mingw32-make -j${SUBMAKE_JOBS} INSTALL_COMMAND mingw32-make -j${SUBMAKE_JOBS} install UPDATE_COMMAND "" # Use a short name to reduce the chance of exceeding path length limit SOURCE_DIR s BINARY_DIR b DEPENDS ext_patch ext_openssl ) elseif (ANDROID) ExternalProject_Add( ext_qt DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://download.qt.io/archive/qt/5.12/5.12.7/single/qt-everywhere-src-5.12.7.tar.xz URL_MD5 ce2c5661c028b9de6183245982d7c120 PATCH_COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0091-Add-support-for-pen-tilt-rotation-for-Android.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0092-Bugfix-fix-the-offset-bug-when-using-Stylus-with-And.patch + COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/fix-locale-android.patch CONFIGURE_COMMAND /configure -prefix ${EXTPREFIX_qt} -opensource -confirm-license -verbose -nomake examples -nomake tests -nomake tools -skip qt3d -skip qtactiveqt -skip qtcanvas3d -skip qtconnectivity -skip qtgraphicaleffects -skip qtlocation -skip qtwayland -skip qtwebchannel -skip qtwebengine -skip qtwebsockets -skip qtwebview -skip qtserialport -skip qtdatavis3d -skip qtvirtualkeyboard -skip qtspeech -skip qtsensors -skip qtgamepad -skip qtscxml -skip qtremoteobjects -skip qtxmlpatterns -skip qtnetworkauth -skip qtcharts -skip qtdatavis3d -skip qtgamepad -skip qtpurchasing -skip qtscxml -skip qtserialbus -skip qtspeech -skip qtvirtualkeyboard -skip qtmultimedia -android-sdk ${ANDROID_SDK_ROOT} -android-ndk ${CMAKE_ANDROID_NDK} -android-arch ${ANDROID_ABI} -xplatform android-clang -android-ndk-platform android-21 -make libs INSTALL_DIR ${EXTPREFIX_qt} BUILD_COMMAND $(MAKE) INSTALL_COMMAND $(MAKE) install UPDATE_COMMAND "" BUILD_IN_SOURCE 1 ) elseif (NOT APPLE) ExternalProject_Add( ext_qt DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://download.qt.io/official_releases/qt/5.12/5.12.7/single/qt-everywhere-src-5.12.7.tar.xz URL_MD5 ce2c5661c028b9de6183245982d7c120 PATCH_COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0012-Synthesize-Enter-LeaveEvent-for-accepted-QTabletEven.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0013-Poison-Qt-s-headers-with-a-mark-about-presence-of-En.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0081-Fix-no-warning-for-overwriting-files-in-non-native-d.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0082-Make-jp-e-g-default-extensions-context-aware.patch CMAKE_ARGS -DOPENSSL_LIBS='-L${EXTPREFIX_qt}/lib -lssl -lcrypto' CONFIGURE_COMMAND /configure -prefix ${EXTPREFIX_qt} -opensource -confirm-license -openssl-linked -verbose -nomake examples -skip qt3d -skip qtactiveqt -skip qtcanvas3d -skip qtconnectivity -skip qtgraphicaleffects -skip qtlocation -skip qtwayland -skip qtwebchannel -skip qtwebengine -skip qtwebsockets -skip qtwebview -skip qtandroidextras -skip qtserialport -skip qtdatavis3d -skip qtvirtualkeyboard -skip qtspeech -skip qtsensors -skip qtgamepad -skip qtscxml -skip qtremoteobjects -skip qtxmlpatterns -skip qtnetworkauth -skip qtcharts -skip qtdatavis3d -skip qtgamepad -skip qtpurchasing -skip qtscxml -skip qtserialbus -skip qtspeech -skip qtvirtualkeyboard -skip qtmultimedia INSTALL_DIR ${EXTPREFIX_qt} BUILD_COMMAND $(MAKE) INSTALL_COMMAND $(MAKE) install UPDATE_COMMAND "" BUILD_IN_SOURCE 1 ) else( APPLE ) # XCODE_VERSION is set by CMake when using the Xcode generator, otherwise we need # to detect it manually here. if (NOT XCODE_VERSION) execute_process( COMMAND xcodebuild -version OUTPUT_VARIABLE xcodebuild_version OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_FILE /dev/null ) string(REGEX MATCH "Xcode ([0-9]+([.][0-9]+)*)" version_match ${xcodebuild_version}) if (version_match) message(STATUS "${EXTPREFIX_qt}:Identified Xcode Version: ${CMAKE_MATCH_1}") set(XCODE_VERSION ${CMAKE_MATCH_1}) else() # If detecting Xcode version failed, set a crazy high version so we default # to the newest. set(XCODE_VERSION 99) message(WARNING "${EXTPREFIX_qt}:Failed to detect the version of an installed copy of Xcode, falling back to highest supported version. Set XCODE_VERSION to override.") endif(version_match) endif(NOT XCODE_VERSION) # ------------------------------------------------------------------------------- # Verify the Xcode installation on Mac OS like Qt5.7 does/will # If not stop now, the system isn't configured correctly for Qt. # No reason to even proceed. # ------------------------------------------------------------------------------- set(XCSELECT_OUTPUT) find_program(XCSELECT_PROGRAM "xcode-select") if(XCSELECT_PROGRAM) message(STATUS "${EXTPREFIX_qt}:Found XCSELECT_PROGRAM as ${XCSELECT_PROGRAM}") set(XCSELECT_COMMAND ${XCSELECT_PROGRAM} "--print-path") execute_process( COMMAND ${XCSELECT_COMMAND} RESULT_VARIABLE XCSELECT_COMMAND_RESULT OUTPUT_VARIABLE XCSELECT_COMMAND_OUTPUT ERROR_FILE /dev/null ) if(NOT XCSELECT_COMMAND_RESULT) # returned 0, we're ok. string(REGEX REPLACE "[ \t]*[\r\n]+[ \t]*" ";" XCSELECT_COMMAND_OUTPUT ${XCSELECT_COMMAND_OUTPUT}) else() string(REPLACE ";" " " XCSELECT_COMMAND_STR "${XCSELECT_COMMAND}") # message(STATUS "${XCSELECT_COMMAND_STR}") message(FATAL_ERROR "${EXTPREFIX_qt}:${XCSELECT_PROGRAM} test failed with status ${XCSELECT_COMMAND_RESULT}") endif() else() message(FATAL_ERROR "${EXTPREFIX_qt}:${XCSELECT_PROGRAM} not found. No Xcode is selected. Use xcode-select -switch to choose an Xcode version") endif() # Belts and suspenders # Beyond all the Xcode and Qt version checking, the proof of the pudding # lies in the success/failure of this command: xcrun --find xcrun. # On failure a patch is necessary, otherwise we're ok # So hard check xcrun now... set(XCRUN_OUTPUT) find_program(XCRUN_PROGRAM "xcrun") if(XCRUN_PROGRAM) message(STATUS "${EXTPREFIX_qt}:Found XCRUN_PROGRAM as ${XCRUN_PROGRAM}") set(XCRUN_COMMAND ${XCRUN_PROGRAM} "--find xcrun") execute_process( COMMAND ${XCRUN_COMMAND} RESULT_VARIABLE XCRUN_COMMAND_RESULT OUTPUT_VARIABLE XCRUN_COMMAND_OUTPUT ERROR_FILE /dev/null ) if(NOT XCRUN_COMMAND_RESULT) # returned 0, we're ok. string(REGEX REPLACE "[ \t]*[\r\n]+[ \t]*" ";" XCRUN_COMMAND_OUTPUT ${XCRUN_COMMAND_OUTPUT}) else() string(REPLACE ";" " " XCRUN_COMMAND_STR "${XCRUN_COMMAND}") # message(STATUS "${XCRUN_COMMAND_STR}") message(STATUS "${EXTPREFIX_qt}:xcrun test failed with status ${XCRUN_COMMAND_RESULT}") endif() else() message(STATUS "${EXTPREFIX_qt}:xcrun not found -- ${XCRUN_PROGRAM}") endif() # # Now configure ext_qt accordingly # if ((XCRUN_COMMAND_RESULT) AND (NOT (XCODE_VERSION VERSION_LESS 8.0.0))) # Fix Xcode xcrun related issue. # NOTE: This should be fixed by Qt 5.7.1 see here: http://code.qt.io/cgit/qt/qtbase.git/commit/?h=dev&id=77a71c32c9d19b87f79b208929e71282e8d8b5d9 # NOTE: but no one's holding their breath. set(ext_qt_PATCH_COMMAND ${PATCH_COMMAND}} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0012-Synthesize-Enter-LeaveEvent-for-accepted-QTabletEven.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0013-Poison-Qt-s-headers-with-a-mark-about-presence-of-En.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0081-Fix-no-warning-for-overwriting-files-in-non-native-d.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0082-Make-jp-e-g-default-extensions-context-aware.patch #COMMAND ${PATCH_COMMAND} -p1 -b -d /qtbase/mkspecs/features/mac -i ${CMAKE_CURRENT_SOURCE_DIR}/mac-default.patch ) message(STATUS "${EXTPREFIX_qt}:Additional patches injected.") else() # No extra patches will be applied # NOTE: defaults for some untested scenarios like xcrun fails and xcode_version < 8. # NOTE: that is uncharted territory and (hopefully) a very unlikely scenario... set(ext_qt_PATCH_COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0012-Synthesize-Enter-LeaveEvent-for-accepted-QTabletEven.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0081-Fix-no-warning-for-overwriting-files-in-non-native-d.patch COMMAND ${PATCH_COMMAND} -p1 -d qtbase -i ${CMAKE_CURRENT_SOURCE_DIR}/0082-Make-jp-e-g-default-extensions-context-aware.patch ) endif() # Qt is big - try and parallelize if at all possible include(ProcessorCount) ProcessorCount(NUM_CORES) if(NOT NUM_CORES EQUAL 0) if (NUM_CORES GREATER 2) # be nice... MATH( EXPR NUM_CORES "${NUM_CORES} - 2" ) endif() set(PARALLEL_MAKE "make;-j${NUM_CORES}") message(STATUS "${EXTPREFIX_qt}:Parallelized make: ${PARALLEL_MAKE}") else() set(PARALLEL_MAKE "make") endif() ExternalProject_Add( ext_qt DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} LOG_DOWNLOAD ON LOG_UPDATE ON LOG_CONFIGURE ON LOG_BUILD ON LOG_TEST ON LOG_INSTALL ON BUILD_IN_SOURCE ON URL https://download.qt.io/official_releases/qt/5.12/5.12.7/single/qt-everywhere-src-5.12.7.tar.xz URL_MD5 ce2c5661c028b9de6183245982d7c120 CMAKE_ARGS -DOPENSSL_LIBS='-L${EXTPREFIX_qt}/lib -lssl -lcrypto' INSTALL_DIR ${EXTPREFIX_qt} CONFIGURE_COMMAND /configure -skip qt3d -skip qtactiveqt -skip qtcanvas3d -skip qtconnectivity -skip qtdoc -skip qtgraphicaleffects -skip qtlocation -skip qtsensors -skip qtserialport -skip qtwayland -skip qtwebchannel -skip qtwebsockets -skip qtwebview -skip qtwebengine -skip qtxmlpatterns -no-sql-sqlite -skip qtcharts -skip qtdatavis3d -skip qtgamepad -skip qtnetworkauth -skip qtpurchasing -skip qtremoteobjects -skip qtscxml -skip qtserialbus -skip qtspeech -skip qtvirtualkeyboard -nomake examples -nomake tools -no-compile-examples -no-dbus -no-iconv -no-qml-debug -no-libproxy -no-system-proxies -no-icu -no-mtdev -system-zlib -qt-pcre -opensource -confirm-license -openssl-linked -prefix ${EXTPREFIX_qt} BUILD_COMMAND ${PARALLEL_MAKE} INSTALL_COMMAND make install UPDATE_COMMAND "" BUILD_IN_SOURCE 1 ) endif() diff --git a/3rdparty/ext_qt/fix-locale-android.patch b/3rdparty/ext_qt/fix-locale-android.patch new file mode 100644 index 0000000000..c8e66c245e --- /dev/null +++ b/3rdparty/ext_qt/fix-locale-android.patch @@ -0,0 +1,31 @@ +For original patch, please see: + +From 0b28f1e3bae10def59e7fe8afac29ed5355a3f1e Mon Sep 17 00:00:00 2001 +From: Andy Shaw +Date: Tue, 21 Jan 2020 15:01:55 +0100 +Subject: [PATCH] Fix QLocale::system() and uiLanguages() for the mobile platforms + +On iOS, it was no longer using the Cocoa specific code needed to get +the locale and uiLanguages information so this functionality is brought +back as it was accidently lost. + +In addition, this has a side-effect of fixing a problem with Android +versions below API 24 where it has no UiLanguages functionality so it +gets the information based on the system locale as a fallback. + +Fixes: QTBUG-81307 +Fixes: QTBUG-81357 +Change-Id: I1709675b5bd5e9cedefb99eaec28279f20a347a4 +Reviewed-by: Edward Welbourne +--- + +diff --git a/src/corelib/tools/qlocale.cpp b/src/corelib/tools/qlocale.cpp +@@ -4463,6 +4463,8 @@ + for (const auto entry : qAsConst(uiLanguages)) + locales.append(QLocale(entry)); + } ++ if (locales.isEmpty()) ++ locales.append(systemLocale()->fallbackUiLocale()); + } else + #endif + { diff --git a/3rdparty/ext_sip/CMakeLists.txt b/3rdparty/ext_sip/CMakeLists.txt index fa75c60e34..a7ae294c60 100644 --- a/3rdparty/ext_sip/CMakeLists.txt +++ b/3rdparty/ext_sip/CMakeLists.txt @@ -1,84 +1,84 @@ SET(PREFIX_ext_sip "${EXTPREFIX}" ) if (UNIX) if(NOT APPLE) SET(PYTHON_EXECUTABLE_PATH ${PREFIX_ext_sip}/bin/python3) if(NOT EXISTS ${PYTHON_EXECUTABLE_PATH}) message("WARNING: using system python3!") SET(PYTHON_EXECUTABLE_PATH python3) endif() ExternalProject_Add( ext_sip DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} - URL https://www.riverbankcomputing.com/static/Downloads/sip/4.19.19/sip-4.19.19.tar.gz + URL https://files.kde.org/krita/build/dependencies/sip-4.19.19.tar.gz URL_MD5 98111479309dc472410f26080d6d4a88 - CONFIGURE_COMMAND ${PYTHON_EXECUTABLE_PATH} /configure.py -b ${PREFIX_ext_sip}/bin -d ${PREFIX_ext_sip}/lib/python3.5/site-packages -e ${PREFIX_ext_sip}/include --sipdir ${PREFIX_ext_sip}/sip --target-py-version 3.8 --sip-module PyQt5.sip + CONFIGURE_COMMAND ${PYTHON_EXECUTABLE_PATH} /configure.py -b ${PREFIX_ext_sip}/bin -d ${PREFIX_ext_sip}/lib/python3.8/site-packages -e ${PREFIX_ext_sip}/include --sipdir ${PREFIX_ext_sip}/sip --target-py-version 3.8 --sip-module PyQt5.sip BUILD_COMMAND make # force single-threaded installation to avoid # race conditions in post-install .py script INSTALL_COMMAND make -j1 install BUILD_IN_SOURCE 1 UPDATE_COMMAND "" ) else(APPLE) if(NOT PYTHONINTERP_FOUND) SET(PYTHON_EXECUTABLE ${PREFIX_ext_sip}/bin/python3) if(NOT EXISTS "${PYTHON_EXECUTABLE}") message("WARNING: using system python3!") SET(PYTHON_EXECUTABLE python3) endif() endif() ExternalProject_Add( ext_sip DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} - URL https://www.riverbankcomputing.com/static/Downloads/sip/4.19.19/sip-4.19.19.tar.gz + URL https://files.kde.org/krita/build/dependencies/sip-4.19.19.tar.gz URL_MD5 98111479309dc472410f26080d6d4a88 CMAKE_ARGS -DPYTHON_INCLUDE_DIR=${PREFIX_ext_sip}/lib/Python.framework/Headers CONFIGURE_COMMAND ${PYTHON_EXECUTABLE} /configure.py --deployment-target=10.11 -b ${PREFIX_ext_sip}/bin -d ${PREFIX_ext_sip}/lib/Python.framework/Versions/Current/site-packages/ -e ${PREFIX_ext_sip}/include --sipdir ${PREFIX_ext_sip}/share/sip --target-py-version 3.8 --sip-module PyQt5.sip BUILD_COMMAND make # force single-threaded installation to avoid # race conditions in post-install .py script INSTALL_COMMAND make -j1 install # COMMAND ${CMAKE_COMMAND} -E create_symlink ./PyQt5/sip.so ${PREFIX_ext_sip}/lib/Python.framework/Versions/Current/site-packages/sip.so # COMMAND ${CMAKE_COMMAND} -E create_symlink ./PyQt5/sip.pyi ${PREFIX_ext_sip}/lib/Python.framework/Versions/Current/site-packages/sip.pyi BUILD_IN_SOURCE 1 UPDATE_COMMAND "" ) endif() elseif (MINGW) list(APPEND _SIP_conf --platform win32-g++ -b ${PREFIX_ext_sip}/bin -d ${PREFIX_ext_sip}/lib/krita-python-libs -e ${PREFIX_ext_sip}/include --sipdir ${PREFIX_ext_sip}/share/sip --target-py-version 3.8 --sip-module PyQt5.sip ) ExternalProject_Add( ext_sip DOWNLOAD_DIR ${EXTERNALS_DOWNLOAD_DIR} URL https://files.kde.org/krita/build/dependencies/sip-4.19.15.zip URL_MD5 4a1a4760cfabef15d68f45a6920974c2 CONFIGURE_COMMAND ${PYTHON_EXECUTABLE} /configure.py ${_SIP_conf} BUILD_COMMAND mingw32-make -j${SUBMAKE_JOBS} LDFLAGS=${SECURITY_SHARED_LINKER_FLAGS} # force single-threaded installation to avoid # race conditions in post-install .py script INSTALL_COMMAND mingw32-make -j1 install BUILD_IN_SOURCE 1 UPDATE_COMMAND "" ) endif() diff --git a/libs/brush/kis_auto_brush.cpp b/libs/brush/kis_auto_brush.cpp index 8820f3472c..53fffbaa9f 100644 --- a/libs/brush/kis_auto_brush.cpp +++ b/libs/brush/kis_auto_brush.cpp @@ -1,413 +1,413 @@ /* * Copyright (c) 2004,2007-2009 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * Copyright (c) 2012 Sven Langkamp * * 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 //MSVC requires that Vc come first #include "kis_auto_brush.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(_WIN64) #include #define srand48 srand inline double drand48() { return double(rand()) / RAND_MAX; } #endif struct KisAutoBrush::Private { Private() : randomness(0) , density(1.0) , idealThreadCountCached(1) {} Private(const Private &rhs) : shape(rhs.shape->clone()) , randomness(rhs.randomness) , density(rhs.density) , idealThreadCountCached(rhs.idealThreadCountCached) { } QScopedPointer shape; qreal randomness; qreal density; int idealThreadCountCached; }; KisAutoBrush::KisAutoBrush(KisMaskGenerator* as, qreal angle, qreal randomness, qreal density) : KoEphemeralResource(), d(new Private) { d->shape.reset(as); d->randomness = randomness; d->density = density; d->idealThreadCountCached = QThread::idealThreadCount(); setBrushType(MASK); setWidth(qMax(qreal(1.0), d->shape->width())); setHeight(qMax(qreal(1.0), d->shape->height())); QImage image = createBrushPreview(); setBrushTipImage(image); // Set angle here so brush tip image is generated unrotated setAngle(angle); image = createBrushPreview(); setImage(image); } KisAutoBrush::~KisAutoBrush() { } qreal KisAutoBrush::userEffectiveSize() const { return d->shape->diameter(); } void KisAutoBrush::setUserEffectiveSize(qreal value) { d->shape->setDiameter(value); } KisAutoBrush::KisAutoBrush(const KisAutoBrush& rhs) : KoEphemeralResource(rhs) , d(new Private(*rhs.d)) { } KisAutoBrush &KisAutoBrush::operator=(const KisAutoBrush &rhs) { if (*this != rhs) { d->shape.reset(rhs.d->shape->clone()); d->randomness = rhs.d->randomness; d->density = rhs.d->density; d->idealThreadCountCached = rhs.d->idealThreadCountCached; } return *this; } KoResourceSP KisAutoBrush::clone() const { return KoResourceSP(new KisAutoBrush(*this)); } -/* It's difficult to predict the mask height when exactly when there are +/* It's difficult to predict the mask height exactly when there are * more than 2 spikes, so we return an upperbound instead. */ -static KisDabShape lieAboutDabShape(KisDabShape const& shape) +static KisDabShape lieAboutDabShape(KisDabShape const& shape, int spikes) { - return KisDabShape(shape.scale(), 1.0, shape.rotation()); + return spikes > 2 ? KisDabShape(shape.scale(), 1.0, shape.rotation()) : shape; } qint32 KisAutoBrush::maskHeight(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskHeight( - lieAboutDabShape(shape), subPixelX, subPixelY, info); + lieAboutDabShape(shape, maskGenerator()->spikes()), subPixelX, subPixelY, info); } qint32 KisAutoBrush::maskWidth(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskWidth( - lieAboutDabShape(shape), subPixelX, subPixelY, info); + lieAboutDabShape(shape, maskGenerator()->spikes()), subPixelX, subPixelY, info); } QSizeF KisAutoBrush::characteristicSize(KisDabShape const& shape) const { - return KisBrush::characteristicSize(lieAboutDabShape(shape)); + return KisBrush::characteristicSize(lieAboutDabShape(shape, maskGenerator()->spikes())); } inline void fillPixelOptimized_4bytes(quint8 *color, quint8 *buf, int size) { /** * This version of filling uses low granularity of data transfers * (32-bit chunks) and internal processor's parallelism. It reaches * 25% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge). */ int block1 = size / 8; int block2 = size % 8; quint32 *src = reinterpret_cast(color); quint32 *dst = reinterpret_cast(buf); // check whether all buffers are 4 bytes aligned // (uncomment if experience some problems) // Q_ASSERT(((qint64)src & 3) == 0); // Q_ASSERT(((qint64)dst & 3) == 0); for (int i = 0; i < block1; i++) { *dst = *src; *(dst + 1) = *src; *(dst + 2) = *src; *(dst + 3) = *src; *(dst + 4) = *src; *(dst + 5) = *src; *(dst + 6) = *src; *(dst + 7) = *src; dst += 8; } for (int i = 0; i < block2; i++) { *dst = *src; dst++; } } inline void fillPixelOptimized_general(quint8 *color, quint8 *buf, int size, int pixelSize) { /** * This version uses internal processor's parallelism and gives * 20% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge (+20%) and * on Merom (+10%)). */ int block1 = size / 8; int block2 = size % 8; for (int i = 0; i < block1; i++) { quint8 *d1 = buf; quint8 *d2 = buf + pixelSize; quint8 *d3 = buf + 2 * pixelSize; quint8 *d4 = buf + 3 * pixelSize; quint8 *d5 = buf + 4 * pixelSize; quint8 *d6 = buf + 5 * pixelSize; quint8 *d7 = buf + 6 * pixelSize; quint8 *d8 = buf + 7 * pixelSize; for (int j = 0; j < pixelSize; j++) { *(d1 + j) = color[j]; *(d2 + j) = color[j]; *(d3 + j) = color[j]; *(d4 + j) = color[j]; *(d5 + j) = color[j]; *(d6 + j) = color[j]; *(d7 + j) = color[j]; *(d8 + j) = color[j]; } buf += 8 * pixelSize; } for (int i = 0; i < block2; i++) { memcpy(buf, color, pixelSize); buf += pixelSize; } } void KisAutoBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX , double subPixelY, qreal softnessFactor) const { Q_UNUSED(info); // Generate the paint device from the mask const KoColorSpace* cs = dst->colorSpace(); quint32 pixelSize = cs->pixelSize(); // mask dimension methods already includes KisBrush::angle() int dstWidth = maskWidth(shape, subPixelX, subPixelY, info); int dstHeight = maskHeight(shape, subPixelX, subPixelY, info); QPointF hotSpot = this->hotSpot(shape, info); // mask size and hotSpot function take the KisBrush rotation into account qreal angle = shape.rotation() + KisBrush::angle(); // if there's coloring information, we merely change the alpha: in that case, // the dab should be big enough! if (coloringInformation) { // new bounds. we don't care if there is some extra memory occcupied. dst->setRect(QRect(0, 0, dstWidth, dstHeight)); dst->lazyGrowBufferWithoutInitialization(); } else { KIS_SAFE_ASSERT_RECOVER_RETURN(dst->bounds().width() >= dstWidth && dst->bounds().height() >= dstHeight); } quint8* dabPointer = dst->data(); quint8* color = 0; if (coloringInformation) { if (dynamic_cast(coloringInformation)) { color = const_cast(coloringInformation->color()); } } double centerX = hotSpot.x() - 0.5 + subPixelX; double centerY = hotSpot.y() - 0.5 + subPixelY; d->shape->setSoftness(softnessFactor); // softness must be set first d->shape->setScale(shape.scaleX(), shape.scaleY()); if (coloringInformation) { if (color && pixelSize == 4) { fillPixelOptimized_4bytes(color, dabPointer, dstWidth * dstHeight); } else if (color) { fillPixelOptimized_general(color, dabPointer, dstWidth * dstHeight, pixelSize); } else { for (int y = 0; y < dstHeight; y++) { for (int x = 0; x < dstWidth; x++) { memcpy(dabPointer, coloringInformation->color(), pixelSize); coloringInformation->nextColumn(); dabPointer += pixelSize; } coloringInformation->nextRow(); } } } MaskProcessingData data(dst, cs, d->randomness, d->density, centerX, centerY, angle); KisBrushMaskApplicatorBase *applicator = d->shape->applicator(); applicator->initializeData(&data); int jobs = d->idealThreadCountCached; if (threadingAllowed() && dstHeight > 100 && jobs >= 4) { int splitter = dstHeight / jobs; QVector rects; for (int i = 0; i < jobs - 1; i++) { rects << QRect(0, i * splitter, dstWidth, splitter); } rects << QRect(0, (jobs - 1)*splitter, dstWidth, dstHeight - (jobs - 1)*splitter); OperatorWrapper wrapper(applicator); QtConcurrent::blockingMap(rects, wrapper); } else { QRect rect(0, 0, dstWidth, dstHeight); applicator->process(rect); } } void KisAutoBrush::toXML(QDomDocument& doc, QDomElement& e) const { QDomElement shapeElt = doc.createElement("MaskGenerator"); d->shape->toXML(doc, shapeElt); e.appendChild(shapeElt); e.setAttribute("type", "auto_brush"); e.setAttribute("spacing", QString::number(spacing())); e.setAttribute("useAutoSpacing", QString::number(autoSpacingActive())); e.setAttribute("autoSpacingCoeff", QString::number(autoSpacingCoeff())); e.setAttribute("angle", QString::number(KisBrush::angle())); e.setAttribute("randomness", QString::number(d->randomness)); e.setAttribute("density", QString::number(d->density)); KisBrush::toXML(doc, e); } QImage KisAutoBrush::createBrushPreview() { int width = maskWidth(KisDabShape(), 0.0, 0.0, KisPaintInformation()); int height = maskHeight(KisDabShape(), 0.0, 0.0, KisPaintInformation()); KisPaintInformation info(QPointF(width * 0.5, height * 0.5), 0.5, 0, 0, angle(), 0, 0, 0, 0); KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); fdev->setRect(QRect(0, 0, width, height)); fdev->initialize(); mask(fdev, KoColor(Qt::black, fdev->colorSpace()), KisDabShape(), info); return fdev->convertToQImage(0); } const KisMaskGenerator* KisAutoBrush::maskGenerator() const { return d->shape.data(); } qreal KisAutoBrush::density() const { return d->density; } qreal KisAutoBrush::randomness() const { return d->randomness; } QPainterPath KisAutoBrush::outline() const { bool simpleOutline = (d->density < 1.0); if (simpleOutline) { QPainterPath path; QRectF brushBoundingbox(0, 0, width(), height()); if (maskGenerator()->type() == KisMaskGenerator::CIRCLE) { path.addEllipse(brushBoundingbox); } else { // if (maskGenerator()->type() == KisMaskGenerator::RECTANGLE) path.addRect(brushBoundingbox); } return path; } return KisBrush::boundary()->path(); } void KisAutoBrush::lodLimitations(KisPaintopLodLimitations *l) const { KisBrush::lodLimitations(l); if (!qFuzzyCompare(density(), 1.0)) { l->limitations << KoID("auto-brush-density", i18nc("PaintOp instant preview limitation", "Brush Density recommended value 100.0")); } if (!qFuzzyCompare(randomness(), 0.0)) { l->limitations << KoID("auto-brush-randomness", i18nc("PaintOp instant preview limitation", "Brush Randomness recommended value 0.0")); } } diff --git a/libs/flake/KoShapeManager.cpp b/libs/flake/KoShapeManager.cpp index 5a26c4b615..52180bd9f0 100644 --- a/libs/flake/KoShapeManager.cpp +++ b/libs/flake/KoShapeManager.cpp @@ -1,775 +1,781 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2009-2010 Jan Hambrecht This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoShapeManager.h" #include "KoShapeManager_p.h" #include "KoSelection.h" #include "KoToolManager.h" #include "KoPointerEvent.h" #include "KoShape.h" #include "KoShape_p.h" #include "KoCanvasBase.h" #include "KoShapeContainer.h" #include "KoShapeStrokeModel.h" #include "KoShapeGroup.h" #include "KoToolProxy.h" #include "KoShapeShadow.h" #include "KoShapeLayer.h" #include "KoFilterEffect.h" #include "KoFilterEffectStack.h" #include "KoFilterEffectRenderContext.h" #include "KoShapeBackground.h" #include #include "KoClipPath.h" #include "KoClipMaskPainter.h" #include "KoShapePaintingContext.h" #include "KoViewConverter.h" #include "KisQPainterStateSaver.h" #include "KoSvgTextChunkShape.h" #include "KoSvgTextShape.h" #include #include #include #include #include "kis_painting_tweaks.h" #include "kis_debug.h" #include "KisForest.h" #include namespace { /** * Returns whether the shape should be added to the RTree for collision and ROI * detection. */ inline bool shapeUsedInRenderingTree(KoShape *shape) { // FIXME: make more general! return !dynamic_cast(shape) && !dynamic_cast(shape) && !(dynamic_cast(shape) && !dynamic_cast(shape)); } /** * Returns whether a shape should be added to the rendering tree because of * its clip mask/path or effects. */ inline bool shapeHasGroupEffects(KoShape *shape) { return shape->clipPath() || (shape->filterEffectStack() && !shape->filterEffectStack()->isEmpty()) || shape->clipMask(); } /** * Returns true if the shape is not fully transparent */ inline bool shapeIsVisible(KoShape *shape) { return shape->isVisible(false) && shape->transparency() < 1.0; } /** * Populate \p tree with the subtree of shapes pointed by a shape \p parentShape. * All new shapes are added as children of \p parentIt. Please take it into account * that \c *parentIt might be not the same as \c parentShape, because \c parentShape * may be hidden from rendering. */ void populateRenderSubtree(KoShape *parentShape, KisForest::child_iterator parentIt, KisForest &tree, std::function shouldIncludeNode, std::function shouldEnterSubtree) { KoShapeContainer *parentContainer = dynamic_cast(parentShape); if (!parentContainer) return; QList children = parentContainer->shapes(); std::sort(children.begin(), children.end(), KoShape::compareShapeZIndex); for (auto it = children.constBegin(); it != children.constEnd(); ++it) { auto newParentIt = parentIt; if (shouldIncludeNode(*it)) { newParentIt = tree.insert(childEnd(parentIt), *it); } if (shouldEnterSubtree(*it)) { populateRenderSubtree(*it, newParentIt, tree, shouldIncludeNode, shouldEnterSubtree); } } } /** * Build a rendering tree for **leaf** nodes defined by \p leafNodes * * Sometimes we should render only a part of the layer (e.g. when we render * in patches). So we shouldn't render the whole graph. The problem is that * some of the shapes may have parents with clip paths/masks and/or effects. * In such a case, these parents should be also included into the rendering * process. * * \c buildRenderTree() builds a graph for such rendering. It includes the * leaf shapes themselves, and all parent shapes that have some effects affecting * these shapes. */ void buildRenderTree(QList leafShapes, KisForest &tree) { QList sortedShapes = leafShapes; std::sort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); std::unordered_set includedShapes; Q_FOREACH (KoShape *shape, sortedShapes) { bool shouldSkipShape = !shapeIsVisible(shape); if (shouldSkipShape) continue; bool shapeIsPartOfIncludedSubtree = false; QVector hierarchy = {shape}; while ((shape = shape->parent())) { if (!shapeIsVisible(shape)) { shouldSkipShape = true; break; } if (includedShapes.find(shape) != end(includedShapes)) { shapeIsPartOfIncludedSubtree = true; break; } if (shapeHasGroupEffects(shape)) { hierarchy << shape; } } if (shouldSkipShape) continue; if (!shapeIsPartOfIncludedSubtree && includedShapes.find(hierarchy.last()) == end(includedShapes)) { tree.insert(childEnd(tree), hierarchy.last()); } std::copy(hierarchy.begin(), hierarchy.end(), std::inserter(includedShapes, end(includedShapes))); } auto shouldIncludeShape = [includedShapes] (KoShape *shape) { // included shapes are guaranteed to be visible return includedShapes.find(shape) != end(includedShapes); }; for (auto it = childBegin(tree); it != childEnd(tree); ++it) { populateRenderSubtree(*it, it, tree, shouldIncludeShape, &shapeIsVisible); } } /** * Render the prebuilt rendering tree on \p painter */ void renderShapes(typename KisForest::child_iterator beginIt, typename KisForest::child_iterator endIt, QPainter &painter, KoShapePaintingContext &paintContext) { for (auto it = beginIt; it != endIt; ++it) { KoShape *shape = *it; KisQPainterStateSaver saver(&painter); if (!isEnd(parent(it))) { painter.setTransform(shape->transformation() * painter.transform()); } else { painter.setTransform(shape->absoluteTransformation() * painter.transform()); } KoClipPath::applyClipping(shape, painter); qreal transparency = shape->transparency(true); if (transparency > 0.0) { painter.setOpacity(1.0-transparency); } if (shape->shadow()) { KisQPainterStateSaver saver(&painter); shape->shadow()->paint(shape, painter); } QScopedPointer clipMaskPainter; QPainter *shapePainter = &painter; KoClipMask *clipMask = shape->clipMask(); if (clipMask) { const QRectF bounds = painter.transform().mapRect(shape->outlineRect()); clipMaskPainter.reset(new KoClipMaskPainter(&painter, bounds/*shape->boundingRect())*/)); shapePainter = clipMaskPainter->shapePainter(); } /** * We expect the shape to save/restore the painter's state itself. Such design was not * not always here, so we need a period of sanity checks to ensure all the shapes are * ported correctly. */ const QTransform sanityCheckTransformSaved = shapePainter->transform(); renderShapes(childBegin(it), childEnd(it), *shapePainter, paintContext); shape->paint(*shapePainter, paintContext); shape->paintStroke(*shapePainter, paintContext); KIS_SAFE_ASSERT_RECOVER(shapePainter->transform() == sanityCheckTransformSaved) { shapePainter->setTransform(sanityCheckTransformSaved); } if (clipMask) { clipMaskPainter->maskPainter()->save(); shape->clipMask()->drawMask(clipMaskPainter->maskPainter(), shape); clipMaskPainter->renderOnGlobalPainter(); clipMaskPainter->maskPainter()->restore(); } } } } void KoShapeManager::Private::updateTree() { - QMutexLocker l(&this->treeMutex); - bool selectionModified = false; bool anyModified = false; - Q_FOREACH (KoShape *shape, aggregate4update) { - selectionModified = selectionModified || selection->isSelected(shape); - anyModified = true; - } - foreach (KoShape *shape, aggregate4update) { - if (!shapeUsedInRenderingTree(shape)) continue; + { + QMutexLocker l(&this->treeMutex); - tree.remove(shape); - QRectF br(shape->boundingRect()); - tree.insert(br, shape); - } + Q_FOREACH (KoShape *shape, aggregate4update) { + selectionModified = selectionModified || selection->isSelected(shape); + anyModified = true; + } - aggregate4update.clear(); - shapeIndexesBeforeUpdate.clear(); + foreach (KoShape *shape, aggregate4update) { + if (!shapeUsedInRenderingTree(shape)) continue; + + tree.remove(shape); + QRectF br(shape->boundingRect()); + tree.insert(br, shape); + } + + aggregate4update.clear(); + shapeIndexesBeforeUpdate.clear(); + } if (selectionModified) { emit q->selectionContentChanged(); } if (anyModified) { emit q->contentChanged(); } } void KoShapeManager::Private::forwardCompressedUdpate() { bool shouldUpdateDecorations = false; QRectF scheduledUpdate; { QMutexLocker l(&shapesMutex); if (!compressedUpdate.isEmpty()) { scheduledUpdate = compressedUpdate; compressedUpdate = QRect(); } Q_FOREACH (const KoShape *shape, compressedUpdatedShapes) { if (selection->isSelected(shape)) { shouldUpdateDecorations = true; break; } } compressedUpdatedShapes.clear(); } if (shouldUpdateDecorations && canvas->toolProxy()) { canvas->toolProxy()->repaintDecorations(); } canvas->updateCanvas(scheduledUpdate); } KoShapeManager::KoShapeManager(KoCanvasBase *canvas, const QList &shapes) : d(new Private(this, canvas)) { Q_ASSERT(d->canvas); // not optional. connect(d->selection, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); setShapes(shapes); /** * Shape manager uses signal compressors with timers, therefore * it might handle queued signals, therefore it should belong * to the GUI thread. */ this->moveToThread(qApp->thread()); connect(&d->updateCompressor, SIGNAL(timeout()), this, SLOT(forwardCompressedUdpate())); } KoShapeManager::KoShapeManager(KoCanvasBase *canvas) : d(new Private(this, canvas)) { Q_ASSERT(d->canvas); // not optional. connect(d->selection, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); // see a comment in another constructor this->moveToThread(qApp->thread()); connect(&d->updateCompressor, SIGNAL(timeout()), this, SLOT(forwardCompressedUdpate())); } void KoShapeManager::Private::unlinkFromShapesRecursively(const QList &shapes) { Q_FOREACH (KoShape *shape, shapes) { shape->removeShapeManager(q); KoShapeContainer *container = dynamic_cast(shape); if (container) { unlinkFromShapesRecursively(container->shapes()); } } } KoShapeManager::~KoShapeManager() { d->unlinkFromShapesRecursively(d->shapes); d->shapes.clear(); delete d; } void KoShapeManager::setShapes(const QList &shapes, Repaint repaint) { { QMutexLocker l1(&d->shapesMutex); QMutexLocker l2(&d->treeMutex); //clear selection d->selection->deselectAll(); d->unlinkFromShapesRecursively(d->shapes); d->compressedUpdate = QRect(); d->compressedUpdatedShapes.clear(); d->aggregate4update.clear(); d->shapeIndexesBeforeUpdate.clear(); d->tree.clear(); d->shapes.clear(); } Q_FOREACH (KoShape *shape, shapes) { addShape(shape, repaint); } } void KoShapeManager::addShape(KoShape *shape, Repaint repaint) { { QMutexLocker l1(&d->shapesMutex); if (d->shapes.contains(shape)) return; shape->addShapeManager(this); d->shapes.append(shape); if (shapeUsedInRenderingTree(shape)) { QMutexLocker l2(&d->treeMutex); QRectF br(shape->boundingRect()); d->tree.insert(br, shape); } } if (repaint == PaintShapeOnAdd) { shape->update(); } // add the children of a KoShapeContainer KoShapeContainer *container = dynamic_cast(shape); if (container) { foreach (KoShape *containerShape, container->shapes()) { addShape(containerShape, repaint); } } } void KoShapeManager::remove(KoShape *shape) { QRectF dirtyRect; { QMutexLocker l1(&d->shapesMutex); QMutexLocker l2(&d->treeMutex); dirtyRect = shape->absoluteOutlineRect(); shape->removeShapeManager(this); d->selection->deselect(shape); d->aggregate4update.remove(shape); d->compressedUpdatedShapes.remove(shape); if (shapeUsedInRenderingTree(shape)) { d->tree.remove(shape); } d->shapes.removeAll(shape); } if (!dirtyRect.isEmpty()) { d->canvas->updateCanvas(dirtyRect); } // remove the children of a KoShapeContainer KoShapeContainer *container = dynamic_cast(shape); if (container) { foreach (KoShape *containerShape, container->shapes()) { remove(containerShape); } } } KoShapeManager::ShapeInterface::ShapeInterface(KoShapeManager *_q) : q(_q) { } void KoShapeManager::ShapeInterface::notifyShapeDestructed(KoShape *shape) { QMutexLocker l1(&q->d->shapesMutex); QMutexLocker l2(&q->d->treeMutex); q->d->selection->deselect(shape); q->d->aggregate4update.remove(shape); q->d->compressedUpdatedShapes.remove(shape); // we cannot access RTTI of the semi-destructed shape, so just // unlink it lazily if (q->d->tree.contains(shape)) { q->d->tree.remove(shape); } q->d->shapes.removeAll(shape); } KoShapeManager::ShapeInterface *KoShapeManager::shapeInterface() { return &d->shapeInterface; } void KoShapeManager::preparePaintJobs(PaintJobsList &jobs, KoShape *excludeRoot) { - QMutexLocker l1(&d->shapesMutex); d->updateTree(); + QMutexLocker l1(&d->shapesMutex); + QSet rootShapesSet; Q_FOREACH (KoShape *shape, d->shapes) { while (shape->parent() && shape->parent() != excludeRoot) { shape = shape->parent(); } if (!rootShapesSet.contains(shape) && shape != excludeRoot) { rootShapesSet.insert(shape); } } const QList rootShapes = rootShapesSet.toList(); QList newRootShapes; Q_FOREACH (KoShape *srcShape, rootShapes) { KIS_SAFE_ASSERT_RECOVER(srcShape->parent() == excludeRoot) { continue; } KoShape *clonedShape = srcShape->cloneShape(); KoShapeContainer *parentShape = srcShape->parent(); if (parentShape && !parentShape->transformation().isIdentity()) { clonedShape->applyAbsoluteTransformation(parentShape->transformation()); } newRootShapes << clonedShape; } PaintJobsList result; PaintJob::SharedSafeStorage shapesStorage = std::make_shared(); Q_FOREACH (KoShape *shape, newRootShapes) { shapesStorage->emplace_back(std::unique_ptr(shape)); } const QList originalShapes = KoShape::linearizeSubtreeSorted(rootShapes); const QList clonedShapes = KoShape::linearizeSubtreeSorted(newRootShapes); KIS_SAFE_ASSERT_RECOVER_RETURN(clonedShapes.size() == originalShapes.size()); QHash clonedFromOriginal; for (int i = 0; i < originalShapes.size(); i++) { clonedFromOriginal[originalShapes[i]] = clonedShapes[i]; } for (auto it = std::begin(jobs); it != std::end(jobs); ++it) { QMutexLocker l(&d->treeMutex); QList unsortedOriginalShapes = d->tree.intersects(it->docUpdateRect); it->allClonedShapes = shapesStorage; Q_FOREACH (KoShape *shape, unsortedOriginalShapes) { KIS_SAFE_ASSERT_RECOVER(shapeUsedInRenderingTree(shape)) { continue; } it->shapes << clonedFromOriginal[shape]; } } } void KoShapeManager::paintJob(QPainter &painter, const KoShapeManager::PaintJob &job, bool forPrint) { painter.setPen(Qt::NoPen); // painters by default have a black stroke, lets turn that off. painter.setBrush(Qt::NoBrush); KisForest renderTree; buildRenderTree(job.shapes, renderTree); KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME renderShapes(childBegin(renderTree), childEnd(renderTree), painter, paintContext); } void KoShapeManager::paint(QPainter &painter, bool forPrint) { + d->updateTree(); + QMutexLocker l1(&d->shapesMutex); - d->updateTree(); painter.setPen(Qt::NoPen); // painters by default have a black stroke, lets turn that off. painter.setBrush(Qt::NoBrush); QList unsortedShapes; if (painter.hasClipping()) { QMutexLocker l(&d->treeMutex); QRectF rect = KisPaintingTweaks::safeClipBoundingRect(painter); unsortedShapes = d->tree.intersects(rect); } else { unsortedShapes = d->shapes; warnFlake << "KoShapeManager::paint Painting with a painter that has no clipping will lead to too much being painted!"; } KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME KisForest renderTree; buildRenderTree(unsortedShapes, renderTree); renderShapes(childBegin(renderTree), childEnd(renderTree), painter, paintContext); } void KoShapeManager::renderSingleShape(KoShape *shape, QPainter &painter, KoShapePaintingContext &paintContext) { KisForest renderTree; KoViewConverter converter; auto root = renderTree.insert(childBegin(renderTree), shape); populateRenderSubtree(shape, root, renderTree, &shapeIsVisible, &shapeIsVisible); renderShapes(childBegin(renderTree), childEnd(renderTree), painter, paintContext); } KoShape *KoShapeManager::shapeAt(const QPointF &position, KoFlake::ShapeSelection selection, bool omitHiddenShapes) { + d->updateTree(); + QMutexLocker l(&d->shapesMutex); - d->updateTree(); QList sortedShapes; { QMutexLocker l(&d->treeMutex); sortedShapes = d->tree.contains(position); } std::sort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); KoShape *firstUnselectedShape = 0; for (int count = sortedShapes.count() - 1; count >= 0; count--) { KoShape *shape = sortedShapes.at(count); if (omitHiddenShapes && ! shape->isVisible()) continue; if (! shape->hitTest(position)) continue; switch (selection) { case KoFlake::ShapeOnTop: if (shape->isSelectable()) return shape; break; case KoFlake::Selected: if (d->selection->isSelected(shape)) return shape; break; case KoFlake::Unselected: if (! d->selection->isSelected(shape)) return shape; break; case KoFlake::NextUnselected: // we want an unselected shape if (d->selection->isSelected(shape)) continue; // memorize the first unselected shape if (! firstUnselectedShape) firstUnselectedShape = shape; // check if the shape above is selected if (count + 1 < sortedShapes.count() && d->selection->isSelected(sortedShapes.at(count + 1))) return shape; break; } } // if we want the next unselected below a selected but there was none selected, // return the first found unselected shape if (selection == KoFlake::NextUnselected && firstUnselectedShape) return firstUnselectedShape; if (d->selection->hitTest(position)) return d->selection; return 0; // missed everything } QList KoShapeManager::shapesAt(const QRectF &rect, bool omitHiddenShapes, bool containedMode) { QMutexLocker l(&d->shapesMutex); d->updateTree(); QList shapes; { QMutexLocker l(&d->treeMutex); shapes = containedMode ? d->tree.contained(rect) : d->tree.intersects(rect); } for (int count = shapes.count() - 1; count >= 0; count--) { KoShape *shape = shapes.at(count); if (omitHiddenShapes && !shape->isVisible()) { shapes.removeAt(count); } else { const QPainterPath outline = shape->absoluteTransformation().map(shape->outline()); if (!containedMode && !outline.intersects(rect) && !outline.contains(rect)) { shapes.removeAt(count); } else if (containedMode) { QPainterPath containingPath; containingPath.addRect(rect); if (!containingPath.contains(outline)) { shapes.removeAt(count); } } } } return shapes; } void KoShapeManager::update(const QRectF &rect, const KoShape *shape, bool selectionHandles) { if (d->updatesBlocked) return; { QMutexLocker l(&d->shapesMutex); d->compressedUpdate |= rect; if (selectionHandles) { d->compressedUpdatedShapes.insert(shape); } } d->updateCompressor.start(); } void KoShapeManager::setUpdatesBlocked(bool value) { d->updatesBlocked = value; } bool KoShapeManager::updatesBlocked() const { return d->updatesBlocked; } void KoShapeManager::notifyShapeChanged(KoShape *shape) { { QMutexLocker l(&d->treeMutex); Q_ASSERT(shape); if (d->aggregate4update.contains(shape)) { return; } d->aggregate4update.insert(shape); d->shapeIndexesBeforeUpdate.insert(shape, shape->zIndex()); } KoShapeContainer *container = dynamic_cast(shape); if (container) { Q_FOREACH (KoShape *child, container->shapes()) notifyShapeChanged(child); } } QList KoShapeManager::shapes() const { QMutexLocker l(&d->shapesMutex); return d->shapes; } QList KoShapeManager::topLevelShapes() const { QMutexLocker l(&d->shapesMutex); QList shapes; // get all toplevel shapes Q_FOREACH (KoShape *shape, d->shapes) { if (!shape->parent() || dynamic_cast(shape->parent())) { shapes.append(shape); } } return shapes; } KoSelection *KoShapeManager::selection() const { return d->selection; } KoCanvasBase *KoShapeManager::canvas() { return d->canvas; } //have to include this because of Q_PRIVATE_SLOT #include "moc_KoShapeManager.cpp" diff --git a/libs/flake/KoToolManager.cpp b/libs/flake/KoToolManager.cpp index 2932f06b8b..429fa978f3 100644 --- a/libs/flake/KoToolManager.cpp +++ b/libs/flake/KoToolManager.cpp @@ -1,953 +1,958 @@ /* This file is part of the KDE project * * Copyright (c) 2005-2010 Boudewijn Rempt * Copyright (C) 2006-2008 Thomas Zander * Copyright (C) 2006 Thorsten Zachmann * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // flake #include "KoToolManager.h" #include "KoToolManager_p.h" #include "KoToolRegistry.h" #include "KoToolProxy.h" #include "KoToolProxy_p.h" #include "KoSelection.h" #include "KoCanvasController.h" #include "KoCanvasControllerWidget.h" #include "KoShape.h" #include "KoShapeLayer.h" #include "KoShapeRegistry.h" #include "KoShapeManager.h" #include "KoSelectedShapesProxy.h" #include "KoCanvasBase.h" #include "KoInputDeviceHandlerRegistry.h" #include "KoInputDeviceHandlerEvent.h" #include "KoPointerEvent.h" #include "tools/KoZoomTool.h" #include "kis_action_registry.h" #include "KoToolFactoryBase.h" #include "kis_assert.h" #include // Qt + kde #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_GLOBAL_STATIC(KoToolManager, s_instance) class CanvasData { public: CanvasData(KoCanvasController *cc, const KoInputDevice &id) : activeTool(0), canvas(cc), inputDevice(id), dummyToolWidget(0), dummyToolLabel(0) { } ~CanvasData() { // the dummy tool widget does not necessarily have a parent and we create it, so we delete it. delete dummyToolWidget; } void activateToolActions() { toolActions.clear(); disabledGlobalActions.clear(); KActionCollection *windowActionCollection = canvas->actionCollection(); if (!windowActionCollection) { qWarning() << "We haven't got an action collection"; return; } QStringList globalActions; QMap shortcutMap; //qDebug() << "................... activating tool" << activeToolId; Q_FOREACH(QAction *action, windowActionCollection->actions()) { //qDebug() << "Action" << action->objectName() << "shortcuts" << action->shortcuts(); if (action->property("tool_action").isValid()) { QStringList tools = action->property("tool_action").toStringList(); //qDebug() << "\tassociated with" << tools; if (tools.contains(activeToolId)) { //qDebug() << "\t\tenabling"; action->setEnabled(true); toolActions << action->objectName(); } else { action->setDisabled(true); } } else { globalActions << action->objectName(); } Q_FOREACH(QKeySequence keySequence, action->shortcuts()) { // After loading a custom shortcut profile, shortcuts can be defined as an empty string, which is not an empty shortcut if (keySequence.toString() != "") { if (shortcutMap.contains(keySequence)) { shortcutMap[keySequence].append(action->objectName()); } else { shortcutMap[keySequence] = QStringList() << action->objectName(); } } } } // Make sure the tool's actions override the global actions that aren't associated with the tool. Q_FOREACH(const QKeySequence &k, shortcutMap.keys()) { if (shortcutMap[k].size() > 1) { QStringList actions = shortcutMap[k]; //qDebug() << k << actions; bool toolActionFound = false; Q_FOREACH(const QString &action, actions) { if (toolActions.contains(action)) { toolActionFound = true; } } Q_FOREACH(const QString &action, actions) { if (toolActionFound && globalActions.contains(action)) { //qDebug() << "\tdisabling global action" << action; windowActionCollection->action(action)->setEnabled(false); disabledGlobalActions << action; } } //qDebug() << k << shortcutMap[k]; } } windowActionCollection->readSettings(); // The shortcuts might have been configured in the meantime. } void deactivateToolActions() { if (!activeTool) return; //qDebug() << "............... deactivating previous tool because activating" << activeToolId; KActionCollection *windowActionCollection = canvas->actionCollection(); Q_FOREACH(const QString &action, toolActions) { //qDebug() << "disabling" << action; windowActionCollection->action(action)->setDisabled(true); } Q_FOREACH(const QString &action, disabledGlobalActions) { //qDebug() << "enabling" << action; windowActionCollection->action(action)->setEnabled(true); } } KoToolBase *activeTool; // active Tool QString activeToolId; // the id of the active Tool QString activationShapeId; // the shape-type (KoShape::shapeId()) the activeTool 'belongs' to. QHash allTools; // all the tools that are created for this canvas. QStack stack; // stack of temporary tools KoCanvasController *const canvas; const KoInputDevice inputDevice; QWidget *dummyToolWidget; // the widget shown in the toolDocker. QLabel *dummyToolLabel; QStringList toolActions; QStringList disabledGlobalActions; }; // ******** KoToolManager ********** KoToolManager::KoToolManager() : QObject(), d(new Private(this)) { connect(QApplication::instance(), SIGNAL(focusChanged(QWidget*,QWidget*)), this, SLOT(movedFocus(QWidget*,QWidget*))); } KoToolManager::~KoToolManager() { delete d; } QList KoToolManager::toolActionList() const { QList answer; answer.reserve(d->tools.count()); Q_FOREACH (ToolHelper *tool, d->tools) { answer.append(tool->toolAction()); } return answer; } void KoToolManager::requestToolActivation(KoCanvasController * controller) { if (d->canvasses.contains(controller)) { QString activeToolId = d->canvasses.value(controller).first()->activeToolId; Q_FOREACH (ToolHelper * th, d->tools) { if (th->id() == activeToolId) { d->toolActivated(th); break; } } } } KoInputDevice KoToolManager::currentInputDevice() const { return d->inputDevice; } void KoToolManager::registerToolActions(KActionCollection *ac, KoCanvasController *controller) { Q_ASSERT(controller); Q_ASSERT(ac); d->setup(); if (!d->canvasses.contains(controller)) { return; } // Actions used to switch tools via shortcuts Q_FOREACH (ToolHelper * th, d->tools) { if (ac->action(th->id())) { continue; } ShortcutToolAction* action = th->createShortcutToolAction(ac); ac->addCategorizedAction(th->id(), action, "tool-shortcuts"); } } void KoToolManager::addController(KoCanvasController *controller) { Q_ASSERT(controller); if (d->canvasses.contains(controller)) return; d->setup(); d->attachCanvas(controller); connect(controller->proxyObject, SIGNAL(destroyed(QObject*)), this, SLOT(attemptCanvasControllerRemoval(QObject*))); connect(controller->proxyObject, SIGNAL(canvasRemoved(KoCanvasController*)), this, SLOT(detachCanvas(KoCanvasController*))); connect(controller->proxyObject, SIGNAL(canvasSet(KoCanvasController*)), this, SLOT(attachCanvas(KoCanvasController*))); } void KoToolManager::removeCanvasController(KoCanvasController *controller) { Q_ASSERT(controller); disconnect(controller->proxyObject, SIGNAL(canvasRemoved(KoCanvasController*)), this, SLOT(detachCanvas(KoCanvasController*))); disconnect(controller->proxyObject, SIGNAL(canvasSet(KoCanvasController*)), this, SLOT(attachCanvas(KoCanvasController*))); d->detachCanvas(controller); } void KoToolManager::attemptCanvasControllerRemoval(QObject* controller) { KoCanvasControllerProxyObject* controllerActual = qobject_cast(controller); if (controllerActual) { removeCanvasController(controllerActual->canvasController()); } } void KoToolManager::switchToolRequested(const QString & id) { Q_ASSERT(d->canvasData); if (!d->canvasData) return; while (!d->canvasData->stack.isEmpty()) // switching means to flush the stack d->canvasData->stack.pop(); d->switchTool(id, false); } void KoToolManager::switchInputDeviceRequested(const KoInputDevice &id) { if (!d->canvasData) return; d->switchInputDevice(id); } void KoToolManager::switchToolTemporaryRequested(const QString &id) { d->switchTool(id, true); } void KoToolManager::switchBackRequested() { if (!d->canvasData) return; if (d->canvasData->stack.isEmpty()) { // default to changing to the interactionTool d->switchTool(KoInteractionTool_ID, false); return; } d->switchTool(d->canvasData->stack.pop(), false); } KoToolBase *KoToolManager::toolById(KoCanvasBase *canvas, const QString &id) const { Q_ASSERT(canvas); Q_FOREACH (KoCanvasController *controller, d->canvasses.keys()) { if (controller->canvas() == canvas) return d->canvasData->allTools.value(id); } return 0; } KoCanvasController *KoToolManager::activeCanvasController() const { if (! d->canvasData) return 0; return d->canvasData->canvas; } QString KoToolManager::preferredToolForSelection(const QList &shapes) { QSet shapeTypes; Q_FOREACH (KoShape *shape, shapes) { shapeTypes << shape->shapeId(); } //KritaUtils::makeContainerUnique(types); QString toolType = KoInteractionTool_ID; int prio = INT_MAX; Q_FOREACH (ToolHelper *helper, d->tools) { if (helper->priority() >= prio) continue; bool toolWillWork = false; foreach (const QString &type, shapeTypes) { if (helper->activationShapeId().split(',').contains(type)) { toolWillWork = true; break; } } if (toolWillWork) { toolType = helper->id(); prio = helper->priority(); } } return toolType; } QPair KoToolManager::createTools(KoCanvasController *controller, ToolHelper *tool) { // XXX: maybe this method should go into the private class? QHash origHash; if (d->canvasses.contains(controller)) { origHash = d->canvasses.value(controller).first()->allTools; } if (origHash.contains(tool->id())) { return QPair(tool->id(), origHash.value(tool->id())); } debugFlake << "Creating tool" << tool->id() << ". Activated on:" << tool->activationShapeId() << ", prio:" << tool->priority(); KoToolBase *tl = tool->createTool(controller->canvas()); if (tl) { d->uniqueToolIds.insert(tl, tool->uniqueId()); tl->setObjectName(tool->id()); } KoZoomTool *zoomTool = dynamic_cast(tl); if (zoomTool) { zoomTool->setCanvasController(controller); } return QPair(tool->id(), tl); } void KoToolManager::initializeCurrentToolForCanvas() { + KIS_ASSERT_RECOVER_RETURN(d->canvasData); + + // make a full reconnect cycle for the currently active tool + d->disconnectActiveTool(); + d->connectActiveTool(); d->postSwitchTool(false); } KoToolManager* KoToolManager::instance() { return s_instance; } QString KoToolManager::activeToolId() const { if (!d->canvasData) return QString(); return d->canvasData->activeToolId; } KoToolManager::Private *KoToolManager::priv() { return d; } /**** KoToolManager::Private ****/ KoToolManager::Private::Private(KoToolManager *qq) : q(qq), canvasData(0), layerExplicitlyDisabled(false) { } KoToolManager::Private::~Private() { qDeleteAll(tools); } // helper method. CanvasData *KoToolManager::Private::createCanvasData(KoCanvasController *controller, const KoInputDevice &device) { QHash toolsHash; Q_FOREACH (ToolHelper *tool, tools) { QPair toolPair = q->createTools(controller, tool); if (toolPair.second) { // only if a real tool was created toolsHash.insert(toolPair.first, toolPair.second); } } CanvasData *cd = new CanvasData(controller, device); cd->allTools = toolsHash; return cd; } void KoToolManager::Private::setup() { if (tools.size() > 0) return; KoShapeRegistry::instance(); KoToolRegistry *registry = KoToolRegistry::instance(); Q_FOREACH (const QString & id, registry->keys()) { ToolHelper *t = new ToolHelper(registry->value(id)); tools.append(t); } // connect to all tools so we can hear their button-clicks Q_FOREACH (ToolHelper *tool, tools) connect(tool, SIGNAL(toolActivated(ToolHelper*)), q, SLOT(toolActivated(ToolHelper*))); // load pluggable input devices KoInputDeviceHandlerRegistry::instance(); } void KoToolManager::Private::connectActiveTool() { if (canvasData->activeTool) { connect(canvasData->activeTool, SIGNAL(cursorChanged(QCursor)), q, SLOT(updateCursor(QCursor))); connect(canvasData->activeTool, SIGNAL(activateTool(QString)), q, SLOT(switchToolRequested(QString))); connect(canvasData->activeTool, SIGNAL(activateTemporary(QString)), q, SLOT(switchToolTemporaryRequested(QString))); connect(canvasData->activeTool, SIGNAL(done()), q, SLOT(switchBackRequested())); connect(canvasData->activeTool, SIGNAL(statusTextChanged(QString)), q, SIGNAL(changedStatusText(QString))); } // we expect the tool to emit a cursor on activation. updateCursor(Qt::ForbiddenCursor); } void KoToolManager::Private::disconnectActiveTool() { if (canvasData->activeTool) { canvasData->deactivateToolActions(); // repaint the decorations before we deactivate the tool as it might deleted // data needed for the repaint emit q->aboutToChangeTool(canvasData->canvas); canvasData->activeTool->deactivate(); disconnect(canvasData->activeTool, SIGNAL(cursorChanged(QCursor)), q, SLOT(updateCursor(QCursor))); disconnect(canvasData->activeTool, SIGNAL(activateTool(QString)), q, SLOT(switchToolRequested(QString))); disconnect(canvasData->activeTool, SIGNAL(activateTemporary(QString)), q, SLOT(switchToolTemporaryRequested(QString))); disconnect(canvasData->activeTool, SIGNAL(done()), q, SLOT(switchBackRequested())); disconnect(canvasData->activeTool, SIGNAL(statusTextChanged(QString)), q, SIGNAL(changedStatusText(QString))); } // emit a empty status text to clear status text from last active tool emit q->changedStatusText(QString()); } void KoToolManager::Private::switchTool(KoToolBase *tool, bool temporary) { Q_ASSERT(tool); if (canvasData == 0) return; if (canvasData->activeTool == tool && tool->toolId() != KoInteractionTool_ID) return; disconnectActiveTool(); canvasData->activeTool = tool; connectActiveTool(); postSwitchTool(temporary); } void KoToolManager::Private::switchTool(const QString &id, bool temporary) { Q_ASSERT(canvasData); if (!canvasData) return; if (canvasData->activeTool && temporary) canvasData->stack.push(canvasData->activeToolId); canvasData->activeToolId = id; KoToolBase *tool = canvasData->allTools.value(id); if (! tool) { return; } Q_FOREACH (ToolHelper *th, tools) { if (th->id() == id) { canvasData->activationShapeId = th->activationShapeId(); break; } } switchTool(tool, temporary); } void KoToolManager::Private::postSwitchTool(bool temporary) { #ifndef NDEBUG int canvasCount = 1; Q_FOREACH (QList list, canvasses) { bool first = true; Q_FOREACH (CanvasData *data, list) { if (first) { debugFlake << "Canvas" << canvasCount++; } debugFlake << " +- Tool:" << data->activeToolId << (data == canvasData ? " *" : ""); first = false; } } #endif Q_ASSERT(canvasData); if (!canvasData) return; KoToolBase::ToolActivation toolActivation; if (temporary) toolActivation = KoToolBase::TemporaryActivation; else toolActivation = KoToolBase::DefaultActivation; QSet shapesToOperateOn; if (canvasData->activeTool && canvasData->activeTool->canvas() && canvasData->activeTool->canvas()->shapeManager()) { KoSelection *selection = canvasData->activeTool->canvas()->shapeManager()->selection(); Q_ASSERT(selection); shapesToOperateOn = QSet::fromList(selection->selectedEditableShapesAndDelegates()); } if (canvasData->canvas->canvas()) { // Caller of postSwitchTool expect this to be called to update the selected tool updateToolForProxy(); canvasData->activeTool->activate(toolActivation, shapesToOperateOn); KoCanvasBase *canvas = canvasData->canvas->canvas(); canvas->updateInputMethodInfo(); } else { canvasData->activeTool->activate(toolActivation, shapesToOperateOn); } QList > optionWidgetList = canvasData->activeTool->optionWidgets(); if (optionWidgetList.empty()) { // no option widget. QWidget *toolWidget; QString title; Q_FOREACH (ToolHelper *tool, tools) { if (tool->id() == canvasData->activeTool->toolId()) { title = tool->toolTip(); break; } } toolWidget = canvasData->dummyToolWidget; if (toolWidget == 0) { toolWidget = new QWidget(); toolWidget->setObjectName("DummyToolWidget"); QVBoxLayout *layout = new QVBoxLayout(toolWidget); layout->setMargin(3); canvasData->dummyToolLabel = new QLabel(toolWidget); layout->addWidget(canvasData->dummyToolLabel); layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding)); toolWidget->setLayout(layout); canvasData->dummyToolWidget = toolWidget; } canvasData->dummyToolLabel->setText(i18n("Active tool: %1", title)); optionWidgetList.append(toolWidget); } // Activate the actions for the currently active tool canvasData->activateToolActions(); emit q->changedTool(canvasData->canvas, uniqueToolIds.value(canvasData->activeTool)); emit q->toolOptionWidgetsChanged(canvasData->canvas, optionWidgetList); } void KoToolManager::Private::switchCanvasData(CanvasData *cd) { Q_ASSERT(cd); KoCanvasBase *oldCanvas = 0; KoInputDevice oldInputDevice; if (canvasData) { oldCanvas = canvasData->canvas->canvas(); oldInputDevice = canvasData->inputDevice; if (canvasData->activeTool) { disconnectActiveTool(); } KoToolProxy *proxy = proxies.value(oldCanvas); Q_ASSERT(proxy); proxy->setActiveTool(0); } canvasData = cd; inputDevice = canvasData->inputDevice; if (canvasData->activeTool) { connectActiveTool(); postSwitchTool(false); } if (oldInputDevice != canvasData->inputDevice) { emit q->inputDeviceChanged(canvasData->inputDevice); } if (oldCanvas != canvasData->canvas->canvas()) { emit q->changedCanvas(canvasData->canvas->canvas()); } } void KoToolManager::Private::toolActivated(ToolHelper *tool) { Q_ASSERT(tool); Q_ASSERT(canvasData); if (!canvasData) return; KoToolBase *t = canvasData->allTools.value(tool->id()); Q_ASSERT(t); canvasData->activeToolId = tool->id(); canvasData->activationShapeId = tool->activationShapeId(); switchTool(t, false); } void KoToolManager::Private::detachCanvas(KoCanvasController *controller) { Q_ASSERT(controller); // check if we are removing the active canvas controller if (canvasData && canvasData->canvas == controller) { KoCanvasController *newCanvas = 0; // try to find another canvas controller beside the one we are removing Q_FOREACH (KoCanvasController* canvas, canvasses.keys()) { if (canvas != controller) { // yay found one newCanvas = canvas; break; } } if (newCanvas) { switchCanvasData(canvasses.value(newCanvas).first()); } else { disconnectActiveTool(); emit q->toolOptionWidgetsChanged(controller, QList >()); // as a last resort just set a blank one canvasData = 0; } } KoToolProxy *proxy = proxies.value(controller->canvas()); if (proxy) proxy->setActiveTool(0); QList tools; Q_FOREACH (CanvasData *canvasData, canvasses.value(controller)) { Q_FOREACH (KoToolBase *tool, canvasData->allTools) { if (! tools.contains(tool)) { tools.append(tool); } } delete canvasData; } Q_FOREACH (KoToolBase *tool, tools) { uniqueToolIds.remove(tool); delete tool; } canvasses.remove(controller); emit q->changedCanvas(canvasData ? canvasData->canvas->canvas() : 0); } void KoToolManager::Private::attachCanvas(KoCanvasController *controller) { Q_ASSERT(controller); CanvasData *cd = createCanvasData(controller, KoInputDevice::mouse()); // switch to new canvas as the active one. switchCanvasData(cd); inputDevice = cd->inputDevice; QList canvasses_; canvasses_.append(cd); canvasses[controller] = canvasses_; KoToolProxy *tp = proxies[controller->canvas()]; if (tp) tp->priv()->setCanvasController(controller); if (cd->activeTool == 0) { // no active tool, so we activate the highest priority main tool int highestPriority = INT_MAX; ToolHelper * helper = 0; Q_FOREACH (ToolHelper * th, tools) { if (th->section() == KoToolFactoryBase::mainToolType()) { if (th->priority() < highestPriority) { highestPriority = qMin(highestPriority, th->priority()); helper = th; } } } if (helper) toolActivated(helper); } Connector *connector = new Connector(controller->canvas()->shapeManager()); connect(connector, SIGNAL(selectionChanged(QList)), q, SLOT(selectionChanged(QList))); connect(controller->canvas()->selectedShapesProxy(), SIGNAL(currentLayerChanged(const KoShapeLayer*)), q, SLOT(currentLayerChanged(const KoShapeLayer*))); emit q->changedCanvas(canvasData ? canvasData->canvas->canvas() : 0); } void KoToolManager::Private::movedFocus(QWidget *from, QWidget *to) { Q_UNUSED(from); // no canvas anyway or no focus set anyway? if (!canvasData || to == 0) { return; } // Check if this app is about QWidget-based KoCanvasControllerWidget canvasses // XXX: Focus handling for non-qwidget based canvases! KoCanvasControllerWidget *canvasControllerWidget = dynamic_cast(canvasData->canvas); if (!canvasControllerWidget) { return; } // canvasWidget is set as focusproxy for KoCanvasControllerWidget, // so all focus checks are to be done against canvasWidget objects // focus returned to current canvas? if (to == canvasData->canvas->canvas()->canvasWidget()) { // nothing to do return; } // if the 'to' is one of our canvasWidgets, then switch. // for code simplicity the current canvas will be checked again, // but would have been caught already in the lines above, so no issue KoCanvasController *newCanvas = 0; Q_FOREACH (KoCanvasController* canvas, canvasses.keys()) { if (canvas->canvas()->canvasWidget() == to) { newCanvas = canvas; break; } } // none of our canvasWidgets got focus? if (newCanvas == 0) { return; } // switch to canvasdata matching inputdevice used last with this app instance Q_FOREACH (CanvasData *data, canvasses.value(newCanvas)) { if (data->inputDevice == inputDevice) { switchCanvasData(data); return; } } // if no such inputDevice for this canvas, then simply fallback to first one switchCanvasData(canvasses.value(newCanvas).first()); } void KoToolManager::Private::updateCursor(const QCursor &cursor) { Q_ASSERT(canvasData); Q_ASSERT(canvasData->canvas); Q_ASSERT(canvasData->canvas->canvas()); canvasData->canvas->canvas()->setCursor(cursor); } void KoToolManager::Private::selectionChanged(const QList &shapes) { QList types; Q_FOREACH (KoShape *shape, shapes) { QSet delegates = shape->toolDelegates(); if (delegates.isEmpty()) { // no delegates, just the orig shape delegates << shape; } foreach (KoShape *shape2, delegates) { Q_ASSERT(shape2); if (! types.contains(shape2->shapeId())) { types.append(shape2->shapeId()); } } } // check if there is still a shape selected the active tool can work on // there needs to be at least one shape for a tool without an activationShapeId // to work // if not change the current tool to the default tool const QStringList activationShapeIds = canvasData->activationShapeId.split(','); if (!(canvasData->activationShapeId.isNull() && shapes.size() > 0) && !activationShapeIds.contains("flake/always") && !activationShapeIds.contains("flake/edit")) { bool currentToolWorks = false; foreach (const QString &type, types) { if (activationShapeIds.contains(type)) { currentToolWorks = true; break; } } if (!currentToolWorks) { switchTool(KoInteractionTool_ID, false); } } emit q->toolCodesSelected(types); } void KoToolManager::Private::currentLayerChanged(const KoShapeLayer *layer) { emit q->currentLayerChanged(canvasData->canvas, layer); layerExplicitlyDisabled = layer && !layer->isShapeEditable(); updateToolForProxy(); debugFlake << "Layer changed to" << layer << "explicitly disabled:" << layerExplicitlyDisabled; } void KoToolManager::Private::updateToolForProxy() { KoToolProxy *proxy = proxies.value(canvasData->canvas->canvas()); if(!proxy) return; bool canUseTool = !layerExplicitlyDisabled || canvasData->activationShapeId.endsWith(QLatin1String("/always")); proxy->setActiveTool(canUseTool ? canvasData->activeTool : 0); } void KoToolManager::Private::switchInputDevice(const KoInputDevice &device) { Q_ASSERT(canvasData); if (!canvasData) return; if (inputDevice == device) return; if (inputDevice.isMouse() && device.isMouse()) return; if (device.isMouse() && !inputDevice.isMouse()) { // we never switch back to mouse from a tablet input device, so the user can use the // mouse to edit the settings for a tool activated by a tablet. See bugs // https://bugs.kde.org/show_bug.cgi?id=283130 and https://bugs.kde.org/show_bug.cgi?id=285501. // We do continue to switch between tablet devices, thought. return; } QList items = canvasses[canvasData->canvas]; // search for a canvasdata object for the current input device Q_FOREACH (CanvasData *cd, items) { if (cd->inputDevice == device) { switchCanvasData(cd); if (!canvasData->activeTool) { switchTool(KoInteractionTool_ID, false); } return; } } // still here? That means we need to create a new CanvasData instance with the current InputDevice. CanvasData *cd = createCanvasData(canvasData->canvas, device); // switch to new canvas as the active one. QString oldTool = canvasData->activeToolId; items.append(cd); canvasses[cd->canvas] = items; switchCanvasData(cd); q->switchToolRequested(oldTool); } void KoToolManager::Private::registerToolProxy(KoToolProxy *proxy, KoCanvasBase *canvas) { proxies.insert(canvas, proxy); Q_FOREACH (KoCanvasController *controller, canvasses.keys()) { if (controller->canvas() == canvas) { proxy->priv()->setCanvasController(controller); break; } } } //have to include this because of Q_PRIVATE_SLOT #include "moc_KoToolManager.cpp" diff --git a/libs/image/kis_adjustment_layer.cc b/libs/image/kis_adjustment_layer.cc index c63feb8193..a97aa89832 100644 --- a/libs/image/kis_adjustment_layer.cc +++ b/libs/image/kis_adjustment_layer.cc @@ -1,150 +1,150 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2005 C. Boemann * * 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_adjustment_layer.h" #include #include "kis_debug.h" #include #include #include #include "kis_image.h" #include "kis_selection.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "filter/kis_filter.h" #include "kis_node_visitor.h" #include "kis_processing_visitor.h" KisAdjustmentLayer::KisAdjustmentLayer(KisImageWSP image, const QString &name, KisFilterConfigurationSP kfc, KisSelectionSP selection) : KisSelectionBasedLayer(image.data(), name, selection, kfc) { // by default Adjustment Layers have a copy composition, // which is more natural for users // https://bugs.kde.org/show_bug.cgi?id=324505 // https://bugs.kde.org/show_bug.cgi?id=294122 // demand the opposite from each other... // // also see a comment in KisLayerUtils::mergeMultipleLayersImpl() setCompositeOpId(COMPOSITE_COPY); setUseSelectionInProjection(false); } KisAdjustmentLayer::KisAdjustmentLayer(const KisAdjustmentLayer& rhs) : KisSelectionBasedLayer(rhs) { } KisAdjustmentLayer::~KisAdjustmentLayer() { } void KisAdjustmentLayer::setFilter(KisFilterConfigurationSP filterConfig) { filterConfig->setChannelFlags(channelFlags()); KisSelectionBasedLayer::setFilter(filterConfig); } QRect KisAdjustmentLayer::incomingChangeRect(const QRect &rect) const { KisFilterConfigurationSP filterConfig = filter(); QRect filteredRect = rect; if (filterConfig) { KisFilterSP filter = KisFilterRegistry::instance()->value(filterConfig->name()); filteredRect = filter->changedRect(rect, filterConfig.data(), projection()->defaultBounds()->currentLevelOfDetail()); } /** * After the change in the blending using * setUseSelectionInProjection(false) we should *not* crop the * change rect of the layer, because we pass contents through. * * //filteredRect = cropChangeRectBySelection(filteredRect); */ return filteredRect; } QRect KisAdjustmentLayer::needRect(const QRect& rect, PositionToFilthy pos) const { Q_UNUSED(pos); KisFilterConfigurationSP filterConfig = filter(); if (!filterConfig) return rect; KisFilterSP filter = KisFilterRegistry::instance()->value(filterConfig->name()); /** * If we need some additional pixels even outside of a selection * for accurate layer filtering, we'll get them! * And no KisSelectionBasedLayer::needRect will prevent us * from doing this! ;) * That's why simply we do not call * KisSelectionBasedLayer::needRect here :) */ - QRect needRect; + QRect needRect = rect; needRect |= needRectForOriginal(needRect); - needRect = filter->neededRect(rect, filterConfig.data(), projection()->defaultBounds()->currentLevelOfDetail()); + needRect = filter->neededRect(needRect, filterConfig.data(), projection()->defaultBounds()->currentLevelOfDetail()); return needRect; } bool KisAdjustmentLayer::accept(KisNodeVisitor & v) { return v.visit(this); } void KisAdjustmentLayer::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { return visitor.visit(this, undoAdapter); } QIcon KisAdjustmentLayer::icon() const { return KisIconUtils::loadIcon("filterLayer"); } KisBaseNode::PropertyList KisAdjustmentLayer::sectionModelProperties() const { KisFilterConfigurationSP filterConfig = filter(); KisBaseNode::PropertyList l = KisLayer::sectionModelProperties(); if (filterConfig) l << KisBaseNode::Property(KoID("filter", i18nc("property of a filter layer, noun", "Filter")), KisFilterRegistry::instance()->value(filterConfig->name())->name()); return l; } void KisAdjustmentLayer::setChannelFlags(const QBitArray & channelFlags) { KisFilterConfigurationSP filterConfig = filter(); if (filterConfig) { filterConfig->setChannelFlags(channelFlags); } KisLayer::setChannelFlags(channelFlags); } diff --git a/libs/image/kis_image_config.cpp b/libs/image/kis_image_config.cpp index ab4550d59e..455b423806 100644 --- a/libs/image/kis_image_config.cpp +++ b/libs/image/kis_image_config.cpp @@ -1,635 +1,641 @@ /* * Copyright (c) 2010 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_image_config.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include "kis_global.h" #include #include #ifdef Q_OS_MACOS #include #endif KisImageConfig::KisImageConfig(bool readOnly) : m_config(KSharedConfig::openConfig()->group(QString())) , m_readOnly(readOnly) { if (!readOnly) { KIS_SAFE_ASSERT_RECOVER_RETURN(qApp->thread() == QThread::currentThread()); } #ifdef Q_OS_MACOS // clear /var/folders/ swap path set by old broken Krita swap implementation in order to use new default swap dir. QString swap = m_config.readEntry("swaplocation", ""); if (swap.startsWith("/var/folders/")) { m_config.deleteEntry("swaplocation"); } #endif } KisImageConfig::~KisImageConfig() { if (m_readOnly) return; if (qApp->thread() != QThread::currentThread()) { dbgKrita << "KisImageConfig: requested config synchronization from nonGUI thread! Called from" << kisBacktrace(); return; } m_config.sync(); } bool KisImageConfig::enableProgressReporting(bool requestDefault) const { return !requestDefault ? m_config.readEntry("enableProgressReporting", true) : true; } void KisImageConfig::setEnableProgressReporting(bool value) { m_config.writeEntry("enableProgressReporting", value); } bool KisImageConfig::enablePerfLog(bool requestDefault) const { return !requestDefault ? m_config.readEntry("enablePerfLog", false) :false; } void KisImageConfig::setEnablePerfLog(bool value) { m_config.writeEntry("enablePerfLog", value); } qreal KisImageConfig::transformMaskOffBoundsReadArea() const { return m_config.readEntry("transformMaskOffBoundsReadArea", 0.5); } int KisImageConfig::updatePatchHeight() const { return m_config.readEntry("updatePatchHeight", 512); } void KisImageConfig::setUpdatePatchHeight(int value) { m_config.writeEntry("updatePatchHeight", value); } int KisImageConfig::updatePatchWidth() const { return m_config.readEntry("updatePatchWidth", 512); } void KisImageConfig::setUpdatePatchWidth(int value) { m_config.writeEntry("updatePatchWidth", value); } qreal KisImageConfig::maxCollectAlpha() const { return m_config.readEntry("maxCollectAlpha", 2.5); } qreal KisImageConfig::maxMergeAlpha() const { return m_config.readEntry("maxMergeAlpha", 1.); } qreal KisImageConfig::maxMergeCollectAlpha() const { return m_config.readEntry("maxMergeCollectAlpha", 1.5); } qreal KisImageConfig::schedulerBalancingRatio() const { /** * updates-queue-size / strokes-queue-size */ return m_config.readEntry("schedulerBalancingRatio", 100.); } void KisImageConfig::setSchedulerBalancingRatio(qreal value) { m_config.writeEntry("schedulerBalancingRatio", value); } int KisImageConfig::maxSwapSize(bool requestDefault) const { return !requestDefault ? m_config.readEntry("maxSwapSize", 4096) : 4096; // in MiB } void KisImageConfig::setMaxSwapSize(int value) { m_config.writeEntry("maxSwapSize", value); } int KisImageConfig::swapSlabSize() const { return m_config.readEntry("swapSlabSize", 64); // in MiB } void KisImageConfig::setSwapSlabSize(int value) { m_config.writeEntry("swapSlabSize", value); } int KisImageConfig::swapWindowSize() const { return m_config.readEntry("swapWindowSize", 16); // in MiB } void KisImageConfig::setSwapWindowSize(int value) { m_config.writeEntry("swapWindowSize", value); } int KisImageConfig::tilesHardLimit() const { qreal hp = qreal(memoryHardLimitPercent()) / 100.0; qreal pp = qreal(memoryPoolLimitPercent()) / 100.0; return totalRAM() * hp * (1 - pp); } int KisImageConfig::tilesSoftLimit() const { qreal sp = qreal(memorySoftLimitPercent()) / 100.0; return tilesHardLimit() * sp; } int KisImageConfig::poolLimit() const { qreal hp = qreal(memoryHardLimitPercent()) / 100.0; qreal pp = qreal(memoryPoolLimitPercent()) / 100.0; return totalRAM() * hp * pp; } qreal KisImageConfig::memoryHardLimitPercent(bool requestDefault) const { return !requestDefault ? m_config.readEntry("memoryHardLimitPercent", 50.) : 50.; } void KisImageConfig::setMemoryHardLimitPercent(qreal value) { m_config.writeEntry("memoryHardLimitPercent", value); } qreal KisImageConfig::memorySoftLimitPercent(bool requestDefault) const { return !requestDefault ? m_config.readEntry("memorySoftLimitPercent", 2.) : 2.; } void KisImageConfig::setMemorySoftLimitPercent(qreal value) { m_config.writeEntry("memorySoftLimitPercent", value); } qreal KisImageConfig::memoryPoolLimitPercent(bool requestDefault) const { return !requestDefault ? m_config.readEntry("memoryPoolLimitPercent", 0.0) : 0.0; } void KisImageConfig::setMemoryPoolLimitPercent(qreal value) { m_config.writeEntry("memoryPoolLimitPercent", value); } QString KisImageConfig::safelyGetWritableTempLocation(const QString &suffix, const QString &configKey, bool requestDefault) const { #ifdef Q_OS_MACOS // On OSX, QDir::tempPath() gives us a folder we cannot reply upon (usually // something like /var/folders/.../...) and that will have vanished when we // try to create the tmp file in KisMemoryWindow::KisMemoryWindow using // swapFileTemplate. thus, we just pick the home folder if swapDir does not // tell us otherwise. // the other option here would be to use a "garbled name" temp file (i.e. no name // KRITA_SWAP_FILE_XXXXXX) in an obscure /var/folders place, which is not // nice to the user. having a clearly named swap file in the home folder is // much nicer to Krita's users. // furthermore, this is just a default and swapDir can always be configured // to another location. QString swap = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QDir::separator() + suffix; #else Q_UNUSED(suffix); QString swap = QDir::tempPath(); #endif if (requestDefault) { return swap; } const QString configuredSwap = m_config.readEntry(configKey, swap); if (!configuredSwap.isEmpty()) { swap = configuredSwap; } QString chosenLocation; QStringList proposedSwapLocations; proposedSwapLocations << swap; proposedSwapLocations << QDir::tempPath(); proposedSwapLocations << QDir::homePath(); Q_FOREACH (const QString location, proposedSwapLocations) { if (!QFileInfo(location).isWritable()) continue; /** * On NTFS, isWritable() doesn't check for attributes due to performance * reasons, so we should try it in a brute-force way... * (yes, there is a hacky-global-variable workaround, but let's be safe) */ QTemporaryFile tempFile; tempFile.setFileTemplate(location + QDir::separator() + "krita_test_swap_location"); if (tempFile.open() && !tempFile.fileName().isEmpty()) { chosenLocation = location; break; } } if (chosenLocation.isEmpty()) { qCritical() << "CRITICAL: no writable location for a swap file found! Tried the following paths:" << proposedSwapLocations; qCritical() << "CRITICAL: hope I don't crash..."; chosenLocation = swap; } if (chosenLocation != swap) { qWarning() << "WARNING: configured swap location is not writable, using a fall-back location" << swap << "->" << chosenLocation; } return chosenLocation; } QString KisImageConfig::swapDir(bool requestDefault) { return safelyGetWritableTempLocation("swap", "swaplocation", requestDefault); } void KisImageConfig::setSwapDir(const QString &swapDir) { m_config.writeEntry("swaplocation", swapDir); } int KisImageConfig::numberOfOnionSkins() const { return m_config.readEntry("numberOfOnionSkins", 10); } void KisImageConfig::setNumberOfOnionSkins(int value) { m_config.writeEntry("numberOfOnionSkins", value); } int KisImageConfig::onionSkinTintFactor() const { return m_config.readEntry("onionSkinTintFactor", 192); } void KisImageConfig::setOnionSkinTintFactor(int value) { m_config.writeEntry("onionSkinTintFactor", value); } int KisImageConfig::onionSkinOpacity(int offset) const { int value = m_config.readEntry("onionSkinOpacity_" + QString::number(offset), -1); if (value < 0) { const int num = numberOfOnionSkins(); const qreal dx = qreal(qAbs(offset)) / num; value = 0.7 * exp(-pow2(dx) / 0.5) * 255; } return value; } void KisImageConfig::setOnionSkinOpacity(int offset, int value) { m_config.writeEntry("onionSkinOpacity_" + QString::number(offset), value); } bool KisImageConfig::onionSkinState(int offset) const { bool enableByDefault = (qAbs(offset) <= 2); return m_config.readEntry("onionSkinState_" + QString::number(offset), enableByDefault); } void KisImageConfig::setOnionSkinState(int offset, bool value) { m_config.writeEntry("onionSkinState_" + QString::number(offset), value); } QColor KisImageConfig::onionSkinTintColorBackward() const { return m_config.readEntry("onionSkinTintColorBackward", QColor(Qt::red)); } void KisImageConfig::setOnionSkinTintColorBackward(const QColor &value) { m_config.writeEntry("onionSkinTintColorBackward", value); } QColor KisImageConfig::onionSkinTintColorForward() const { return m_config.readEntry("oninSkinTintColorForward", QColor(Qt::green)); } void KisImageConfig::setOnionSkinTintColorForward(const QColor &value) { m_config.writeEntry("oninSkinTintColorForward", value); } bool KisImageConfig::lazyFrameCreationEnabled(bool requestDefault) const { return !requestDefault ? m_config.readEntry("lazyFrameCreationEnabled", true) : true; } void KisImageConfig::setLazyFrameCreationEnabled(bool value) { m_config.writeEntry("lazyFrameCreationEnabled", value); } #if defined Q_OS_LINUX #include #elif defined Q_OS_FREEBSD || defined Q_OS_NETBSD || defined Q_OS_OPENBSD #include #elif defined Q_OS_WIN #include #elif defined Q_OS_MACOS #include #include #endif int KisImageConfig::totalRAM() { // let's think that default memory size is 1000MiB int totalMemory = 1000; // MiB int error = 1; #if defined Q_OS_LINUX struct sysinfo info; error = sysinfo(&info); if(!error) { totalMemory = info.totalram * info.mem_unit / (1UL << 20); } #elif defined Q_OS_FREEBSD || defined Q_OS_NETBSD || defined Q_OS_OPENBSD u_long physmem; # if defined HW_PHYSMEM64 // NetBSD only int mib[] = {CTL_HW, HW_PHYSMEM64}; # else int mib[] = {CTL_HW, HW_PHYSMEM}; # endif size_t len = sizeof(physmem); error = sysctl(mib, 2, &physmem, &len, 0, 0); if(!error) { totalMemory = physmem >> 20; } #elif defined Q_OS_WIN MEMORYSTATUSEX status; status.dwLength = sizeof(status); error = !GlobalMemoryStatusEx(&status); if (!error) { totalMemory = status.ullTotalPhys >> 20; } // For 32 bit windows, the total memory available is at max the 2GB per process memory limit. # if defined ENV32BIT totalMemory = qMin(totalMemory, 2000); # endif #elif defined Q_OS_MACOS int mib[2] = { CTL_HW, HW_MEMSIZE }; u_int namelen = sizeof(mib) / sizeof(mib[0]); uint64_t size; size_t len = sizeof(size); errno = 0; if (sysctl(mib, namelen, &size, &len, 0, 0) >= 0) { totalMemory = size >> 20; error = 0; } else { dbgKrita << "sysctl(\"hw.memsize\") raised error" << strerror(errno); } #endif if (error) { warnKrita << "Cannot get the size of your RAM. Using 1 GiB by default."; } return totalMemory; } bool KisImageConfig::showAdditionalOnionSkinsSettings(bool requestDefault) const { return !requestDefault ? m_config.readEntry("showAdditionalOnionSkinsSettings", true) : true; } void KisImageConfig::setShowAdditionalOnionSkinsSettings(bool value) { m_config.writeEntry("showAdditionalOnionSkinsSettings", value); } int KisImageConfig::defaultFrameColorLabel() const { return m_config.readEntry("defaultFrameColorLabel", 0); } void KisImageConfig::setDefaultFrameColorLabel(int label) { m_config.writeEntry("defaultFrameColorLabel", label); } KisProofingConfigurationSP KisImageConfig::defaultProofingconfiguration() { KisProofingConfiguration *proofingConfig= new KisProofingConfiguration(); proofingConfig->proofingProfile = m_config.readEntry("defaultProofingProfileName", "Chemical proof"); proofingConfig->proofingModel = m_config.readEntry("defaultProofingProfileModel", "CMYKA"); proofingConfig->proofingDepth = m_config.readEntry("defaultProofingProfileDepth", "U8"); proofingConfig->intent = (KoColorConversionTransformation::Intent)m_config.readEntry("defaultProofingProfileIntent", 3); if (m_config.readEntry("defaultProofingBlackpointCompensation", true)) { proofingConfig->conversionFlags |= KoColorConversionTransformation::ConversionFlag::BlackpointCompensation; } else { proofingConfig->conversionFlags = proofingConfig->conversionFlags & ~KoColorConversionTransformation::ConversionFlag::BlackpointCompensation; } QColor def(Qt::green); m_config.readEntry("defaultProofingGamutwarning", def); KoColor col(KoColorSpaceRegistry::instance()->rgb8()); col.fromQColor(def); col.setOpacity(1.0); proofingConfig->warningColor = col; proofingConfig->adaptationState = (double)m_config.readEntry("defaultProofingAdaptationState", 1.0); return toQShared(proofingConfig); } void KisImageConfig::setDefaultProofingConfig(const KoColorSpace *proofingSpace, int proofingIntent, bool blackPointCompensation, KoColor warningColor, double adaptationState) { m_config.writeEntry("defaultProofingProfileName", proofingSpace->profile()->name()); m_config.writeEntry("defaultProofingProfileModel", proofingSpace->colorModelId().id()); m_config.writeEntry("defaultProofingProfileDepth", proofingSpace->colorDepthId().id()); m_config.writeEntry("defaultProofingProfileIntent", proofingIntent); m_config.writeEntry("defaultProofingBlackpointCompensation", blackPointCompensation); QColor c; c = warningColor.toQColor(); m_config.writeEntry("defaultProofingGamutwarning", c); m_config.writeEntry("defaultProofingAdaptationState",adaptationState); } bool KisImageConfig::useLodForColorizeMask(bool requestDefault) const { return !requestDefault ? m_config.readEntry("useLodForColorizeMask", false) : false; } void KisImageConfig::setUseLodForColorizeMask(bool value) { m_config.writeEntry("useLodForColorizeMask", value); } int KisImageConfig::maxNumberOfThreads(bool defaultValue) const { return (defaultValue ? QThread::idealThreadCount() : m_config.readEntry("maxNumberOfThreads", QThread::idealThreadCount())); } void KisImageConfig::setMaxNumberOfThreads(int value) { if (value == QThread::idealThreadCount()) { m_config.deleteEntry("maxNumberOfThreads"); } else { m_config.writeEntry("maxNumberOfThreads", value); } } int KisImageConfig::frameRenderingClones(bool defaultValue) const { const int defaultClonesCount = qMax(1, maxNumberOfThreads(defaultValue) / 2); return defaultValue ? defaultClonesCount : m_config.readEntry("frameRenderingClones", defaultClonesCount); } void KisImageConfig::setFrameRenderingClones(int value) { m_config.writeEntry("frameRenderingClones", value); } int KisImageConfig::fpsLimit(bool defaultValue) const { int limit = defaultValue ? 100 : m_config.readEntry("fpsLimit", 100); return limit > 0 ? limit : 1; } void KisImageConfig::setFpsLimit(int value) { m_config.writeEntry("fpsLimit", value); } bool KisImageConfig::useOnDiskAnimationCacheSwapping(bool defaultValue) const { return defaultValue ? true : m_config.readEntry("useOnDiskAnimationCacheSwapping", true); } void KisImageConfig::setUseOnDiskAnimationCacheSwapping(bool value) { m_config.writeEntry("useOnDiskAnimationCacheSwapping", value); } QString KisImageConfig::animationCacheDir(bool defaultValue) const { return safelyGetWritableTempLocation("animation_cache", "animationCacheDir", defaultValue); } void KisImageConfig::setAnimationCacheDir(const QString &value) { m_config.writeEntry("animationCacheDir", value); } bool KisImageConfig::useAnimationCacheFrameSizeLimit(bool defaultValue) const { return defaultValue ? true : m_config.readEntry("useAnimationCacheFrameSizeLimit", true); } void KisImageConfig::setUseAnimationCacheFrameSizeLimit(bool value) { m_config.writeEntry("useAnimationCacheFrameSizeLimit", value); } int KisImageConfig::animationCacheFrameSizeLimit(bool defaultValue) const { return defaultValue ? 2500 : m_config.readEntry("animationCacheFrameSizeLimit", 2500); } void KisImageConfig::setAnimationCacheFrameSizeLimit(int value) { m_config.writeEntry("animationCacheFrameSizeLimit", value); } bool KisImageConfig::useAnimationCacheRegionOfInterest(bool defaultValue) const { return defaultValue ? true : m_config.readEntry("useAnimationCacheRegionOfInterest", true); } void KisImageConfig::setUseAnimationCacheRegionOfInterest(bool value) { m_config.writeEntry("useAnimationCacheRegionOfInterest", value); } qreal KisImageConfig::animationCacheRegionOfInterestMargin(bool defaultValue) const { return defaultValue ? 0.25 : m_config.readEntry("animationCacheRegionOfInterestMargin", 0.25); } void KisImageConfig::setAnimationCacheRegionOfInterestMargin(qreal value) { m_config.writeEntry("animationCacheRegionOfInterestMargin", value); } QColor KisImageConfig::selectionOverlayMaskColor(bool defaultValue) const { QColor def(255, 0, 0, 128); return (defaultValue ? def : m_config.readEntry("selectionOverlayMaskColor", def)); } void KisImageConfig::setSelectionOverlayMaskColor(const QColor &color) { m_config.writeEntry("selectionOverlayMaskColor", color); } + +void KisImageConfig::resetConfig() +{ + KConfigGroup config = KSharedConfig::openConfig()->group(QString()); + config.deleteGroup(); +} diff --git a/libs/image/kis_image_config.h b/libs/image/kis_image_config.h index b3ec8b797e..2f25990edc 100644 --- a/libs/image/kis_image_config.h +++ b/libs/image/kis_image_config.h @@ -1,156 +1,158 @@ /* * Copyright (c) 2010 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_IMAGE_CONFIG_H_ #define KIS_IMAGE_CONFIG_H_ #include #include "kritaimage_export.h" #include "KisProofingConfiguration.h" #include "kis_types.h" class KRITAIMAGE_EXPORT KisImageConfig { public: KisImageConfig(bool readOnly); ~KisImageConfig(); bool enableProgressReporting(bool requestDefault = false) const; void setEnableProgressReporting(bool value); bool enablePerfLog(bool requestDefault = false) const; void setEnablePerfLog(bool value); qreal transformMaskOffBoundsReadArea() const; int updatePatchHeight() const; void setUpdatePatchHeight(int value); int updatePatchWidth() const; void setUpdatePatchWidth(int value); qreal maxCollectAlpha() const; qreal maxMergeAlpha() const; qreal maxMergeCollectAlpha() const; qreal schedulerBalancingRatio() const; void setSchedulerBalancingRatio(qreal value); int maxSwapSize(bool requestDefault = false) const; void setMaxSwapSize(int value); int swapSlabSize() const; void setSwapSlabSize(int value); int swapWindowSize() const; void setSwapWindowSize(int value); int tilesHardLimit() const; // MiB int tilesSoftLimit() const; // MiB int poolLimit() const; // MiB qreal memoryHardLimitPercent(bool requestDefault = false) const; // % of total RAM qreal memorySoftLimitPercent(bool requestDefault = false) const; // % of memoryHardLimitPercent() * (1 - 0.01 * memoryPoolLimitPercent()) qreal memoryPoolLimitPercent(bool requestDefault = false) const; // % of memoryHardLimitPercent() void setMemoryHardLimitPercent(qreal value); void setMemorySoftLimitPercent(qreal value); void setMemoryPoolLimitPercent(qreal value); static int totalRAM(); // MiB /** * @return a specific directory for the swapfile, if set. If not set, return an * empty QString and use the default KDE directory. */ QString swapDir(bool requestDefault = false); void setSwapDir(const QString &swapDir); int numberOfOnionSkins() const; void setNumberOfOnionSkins(int value); int onionSkinTintFactor() const; void setOnionSkinTintFactor(int value); int onionSkinOpacity(int offset) const; void setOnionSkinOpacity(int offset, int value); bool onionSkinState(int offset) const; void setOnionSkinState(int offset, bool value); QColor onionSkinTintColorBackward() const; void setOnionSkinTintColorBackward(const QColor &value); QColor onionSkinTintColorForward() const; void setOnionSkinTintColorForward(const QColor &value); bool lazyFrameCreationEnabled(bool requestDefault = false) const; void setLazyFrameCreationEnabled(bool value); bool showAdditionalOnionSkinsSettings(bool requestDefault = false) const; void setShowAdditionalOnionSkinsSettings(bool value); int defaultFrameColorLabel() const; void setDefaultFrameColorLabel(int label); KisProofingConfigurationSP defaultProofingconfiguration(); void setDefaultProofingConfig(const KoColorSpace *proofingSpace, int proofingIntent, bool blackPointCompensation, KoColor warningColor, double adaptationState); bool useLodForColorizeMask(bool requestDefault = false) const; void setUseLodForColorizeMask(bool value); int maxNumberOfThreads(bool defaultValue = false) const; void setMaxNumberOfThreads(int value); int frameRenderingClones(bool defaultValue = false) const; void setFrameRenderingClones(int value); int fpsLimit(bool defaultValue = false) const; void setFpsLimit(int value); bool useOnDiskAnimationCacheSwapping(bool defaultValue = false) const; void setUseOnDiskAnimationCacheSwapping(bool value); QString animationCacheDir(bool defaultValue = false) const; void setAnimationCacheDir(const QString &value); bool useAnimationCacheFrameSizeLimit(bool defaultValue = false) const; void setUseAnimationCacheFrameSizeLimit(bool value); int animationCacheFrameSizeLimit(bool defaultValue = false) const; void setAnimationCacheFrameSizeLimit(int value); bool useAnimationCacheRegionOfInterest(bool defaultValue = false) const; void setUseAnimationCacheRegionOfInterest(bool value); qreal animationCacheRegionOfInterestMargin(bool defaultValue = false) const; void setAnimationCacheRegionOfInterestMargin(qreal value); QColor selectionOverlayMaskColor(bool defaultValue = false) const; void setSelectionOverlayMaskColor(const QColor &color); + + static void resetConfig(); private: Q_DISABLE_COPY(KisImageConfig) QString safelyGetWritableTempLocation(const QString &suffix, const QString &configKey, bool requestDefault) const; private: KConfigGroup m_config; bool m_readOnly; }; #endif /* KIS_IMAGE_CONFIG_H_ */ diff --git a/libs/image/tests/CMakeLists.txt b/libs/image/tests/CMakeLists.txt index bc07f74243..fddafbc361 100644 --- a/libs/image/tests/CMakeLists.txt +++ b/libs/image/tests/CMakeLists.txt @@ -1,197 +1,192 @@ # cmake in some versions for some not yet known reasons fails to run automoc # on random targets (changing target names already has an effect) # As temporary workaround skipping build of tests on these versions for now # See https://mail.kde.org/pipermail/kde-buildsystem/2015-June/010819.html # extend range of affected cmake versions as needed if(NOT ${CMAKE_VERSION} VERSION_LESS 3.1.3 AND NOT ${CMAKE_VERSION} VERSION_GREATER 3.2.3) message(WARNING "Skipping krita/image/tests, CMake in at least versions 3.1.3 - 3.2.3 seems to have a problem with automoc. \n(FRIENDLY REMINDER: PLEASE DON'T BREAK THE TESTS!)") set (HAVE_FAILING_CMAKE TRUE) else() set (HAVE_FAILING_CMAKE FALSE) endif() include_directories( ${CMAKE_BINARY_DIR}/libs/image/ ${CMAKE_SOURCE_DIR}/libs/image/ ${CMAKE_SOURCE_DIR}/libs/image/brushengine ${CMAKE_SOURCE_DIR}/libs/image/tiles3 ${CMAKE_SOURCE_DIR}/libs/image/tiles3/swap ${CMAKE_SOURCE_DIR}/sdk/tests ) include_Directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ) if(HAVE_VC) include_directories(${Vc_INCLUDE_DIR}) endif() include(ECMAddTests) macro_add_unittest_definitions() set(KisRandomGeneratorDemoSources kis_random_generator_demo.cpp kimageframe.cpp) ki18n_wrap_ui(KisRandomGeneratorDemoSources kis_random_generator_demo.ui) add_executable(KisRandomGeneratorDemo ${KisRandomGeneratorDemoSources}) target_link_libraries(KisRandomGeneratorDemo kritaimage) ecm_mark_as_test(KisRandomGeneratorDemo) ecm_add_tests( kis_base_node_test.cpp kis_fast_math_test.cpp kis_node_test.cpp kis_node_facade_test.cpp kis_fixed_paint_device_test.cpp kis_layer_test.cpp kis_effect_mask_test.cpp kis_iterator_test.cpp kis_painter_test.cpp kis_count_visitor_test.cpp kis_projection_test.cpp kis_properties_configuration_test.cpp kis_transaction_test.cpp kis_pixel_selection_test.cpp kis_group_layer_test.cpp kis_paint_layer_test.cpp kis_adjustment_layer_test.cpp kis_annotation_test.cpp kis_clone_layer_test.cpp kis_convolution_painter_test.cpp kis_crop_processing_visitor_test.cpp kis_processing_applicator_test.cpp kis_datamanager_test.cpp kis_fill_painter_test.cpp kis_filter_configuration_test.cpp kis_filter_test.cpp kis_filter_processing_information_test.cpp kis_filter_registry_test.cpp kis_filter_strategy_test.cpp kis_gradient_painter_test.cpp kis_image_commands_test.cpp kis_image_test.cpp kis_image_signal_router_test.cpp kis_iterators_ng_test.cpp kis_iterator_benchmark.cpp kis_updater_context_test.cpp kis_simple_update_queue_test.cpp kis_stroke_test.cpp kis_simple_stroke_strategy_test.cpp kis_stroke_strategy_undo_command_based_test.cpp kis_strokes_queue_test.cpp kis_mask_test.cpp kis_math_toolbox_test.cpp kis_name_server_test.cpp kis_node_commands_test.cpp kis_node_graph_listener_test.cpp kis_node_visitor_test.cpp kis_paint_information_test.cpp kis_distance_information_test.cpp kis_paintop_test.cpp kis_pattern_test.cpp kis_selection_mask_test.cpp kis_shared_ptr_test.cpp kis_bsplines_test.cpp kis_warp_transform_worker_test.cpp kis_liquify_transform_worker_test.cpp kis_transparency_mask_test.cpp kis_types_test.cpp kis_vec_test.cpp kis_filter_config_widget_test.cpp kis_mask_generator_test.cpp kis_cubic_curve_test.cpp kis_fixed_point_maths_test.cpp kis_node_query_path_test.cpp kis_filter_weights_buffer_test.cpp kis_filter_weights_applicator_test.cpp kis_fill_interval_test.cpp kis_fill_interval_map_test.cpp kis_scanline_fill_test.cpp kis_psd_layer_style_test.cpp kis_layer_style_projection_plane_test.cpp kis_lod_capable_layer_offset_test.cpp kis_algebra_2d_test.cpp kis_marker_painter_test.cpp kis_lazy_brush_test.cpp kis_mask_similarity_test.cpp KisMaskGeneratorTest.cpp kis_layer_style_filter_environment_test.cpp kis_asl_parser_test.cpp KisPerStrokeRandomSourceTest.cpp KisWatershedWorkerTest.cpp kis_dom_utils_test.cpp kis_transform_worker_test.cpp kis_cs_conversion_test.cpp kis_projection_leaf_test.cpp kis_histogram_test.cpp kis_onion_skin_compositor_test.cpp kis_queues_progress_updater_test.cpp kis_image_animation_interface_test.cpp kis_walkers_test.cpp kis_cage_transform_worker_test.cpp kis_random_generator_test.cpp kis_keyframing_test.cpp kis_filter_mask_test.cpp kis_asl_layer_style_serializer_test.cpp TestAslStorage.cpp + kis_async_merger_test.cpp LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) include(KritaAddBrokenUnitTest) krita_add_broken_unit_test( kis_transform_mask_test.cpp - TEST_NAME kis_transform_mask_test.cpp + TEST_NAME kis_transform_mask_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_layer_styles_test.cpp - TEST_NAME kis_layer_styles_test.cpp + TEST_NAME kis_layer_styles_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_update_scheduler_test.cpp - TEST_NAME kis_update_scheduler_test.cpp + TEST_NAME kis_update_scheduler_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_paint_device_test.cpp - TEST_NAME kis_paint_device_test.cpp + TEST_NAME kis_paint_device_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_colorize_mask_test.cpp - TEST_NAME kis_colorize_mask_test.cpp + TEST_NAME kis_colorize_mask_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_selection_test.cpp - TEST_NAME kis_selection_test.cpp + TEST_NAME kis_selection_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_processings_test.cpp - TEST_NAME kis_processings_test.cpp + TEST_NAME kis_processings_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_test( kis_perspective_transform_worker_test.cpp - TEST_NAME kis_perspective_transform_worker_test.cpp - LINK_LIBRARIES kritaimage Qt5::Test - NAME_PREFIX "libs-image-" -) - -krita_add_broken_unit_test( kis_async_merger_test.cpp - TEST_NAME kis_async_merger_test.cpp + TEST_NAME kis_perspective_transform_worker_test LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) diff --git a/libs/image/tests/data/async_merger_test/dependent_adj_mask.png b/libs/image/tests/data/async_merger_test/dependent_adj_mask.png index 92415ec6ee..a778ba2c2f 100644 Binary files a/libs/image/tests/data/async_merger_test/dependent_adj_mask.png and b/libs/image/tests/data/async_merger_test/dependent_adj_mask.png differ diff --git a/libs/image/tests/data/async_merger_test/dependent_adj_style.png b/libs/image/tests/data/async_merger_test/dependent_adj_style.png index 53e9020654..a4ce69a4ab 100644 Binary files a/libs/image/tests/data/async_merger_test/dependent_adj_style.png and b/libs/image/tests/data/async_merger_test/dependent_adj_style.png differ diff --git a/libs/image/tests/data/async_merger_test/dependent_group_mask.png b/libs/image/tests/data/async_merger_test/dependent_group_mask.png index 77640d6d66..b117135d5f 100644 Binary files a/libs/image/tests/data/async_merger_test/dependent_group_mask.png and b/libs/image/tests/data/async_merger_test/dependent_group_mask.png differ diff --git a/libs/image/tests/data/async_merger_test/dependent_group_mask_style.png b/libs/image/tests/data/async_merger_test/dependent_group_mask_style.png index c33d5abed1..6ea0d370ab 100644 Binary files a/libs/image/tests/data/async_merger_test/dependent_group_mask_style.png and b/libs/image/tests/data/async_merger_test/dependent_group_mask_style.png differ diff --git a/libs/image/tests/data/async_merger_test/dependent_group_style.png b/libs/image/tests/data/async_merger_test/dependent_group_style.png index 2273997d15..011aa08d4f 100644 Binary files a/libs/image/tests/data/async_merger_test/dependent_group_style.png and b/libs/image/tests/data/async_merger_test/dependent_group_style.png differ diff --git a/libs/image/tests/data/async_merger_test/mask_on_adj_initial.png b/libs/image/tests/data/async_merger_test/mask_on_adj_initial.png new file mode 100644 index 0000000000..f8b32f7fc0 Binary files /dev/null and b/libs/image/tests/data/async_merger_test/mask_on_adj_initial.png differ diff --git a/libs/image/tests/data/merged_hakonepa.png b/libs/image/tests/data/merged_hakonepa.png index 0d8555609f..528fb70738 100644 Binary files a/libs/image/tests/data/merged_hakonepa.png and b/libs/image/tests/data/merged_hakonepa.png differ diff --git a/libs/image/tests/kis_async_merger_test.cpp b/libs/image/tests/kis_async_merger_test.cpp index 430d2378d6..1338d63a46 100644 --- a/libs/image/tests/kis_async_merger_test.cpp +++ b/libs/image/tests/kis_async_merger_test.cpp @@ -1,452 +1,513 @@ /* * Copyright (c) 2009 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_async_merger_test.h" #include "kis_merge_walker.h" #include "kis_full_refresh_walker.h" #include "kis_async_merger.h" #include #include #include #include "kis_image.h" #include "kis_paint_layer.h" #include "kis_group_layer.h" #include "kis_clone_layer.h" #include "kis_adjustment_layer.h" #include "kis_filter_mask.h" #include "kis_selection.h" #include "kis_paint_device_debug_utils.h" #include #include "filter/kis_filter.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "../../sdk/tests/testutil.h" +#include "kis_image_config.h" +#include "KisImageConfigNotifier.h" + +void KisAsyncMergerTest::init() +{ + KisImageConfig::resetConfig(); +} + + /* +-----------+ |root | | group | | blur 1 | | paint 2 | | paint 1 | +-----------+ */ void KisAsyncMergerTest::testMerger() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 640, 441, colorSpace, "merger test"); QImage sourceImage1(QString(FILES_DATA_DIR) + QDir::separator() + "hakonepa.png"); QImage sourceImage2(QString(FILES_DATA_DIR) + QDir::separator() + "inverted_hakonepa.png"); QImage referenceProjection(QString(FILES_DATA_DIR) + QDir::separator() + "merged_hakonepa.png"); KisPaintDeviceSP device1 = new KisPaintDevice(colorSpace); KisPaintDeviceSP device2 = new KisPaintDevice(colorSpace); device1->convertFromQImage(sourceImage1, 0, 0, 0); device2->convertFromQImage(sourceImage2, 0, 0, 0); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(KisGlobalResourcesInterface::instance()); Q_ASSERT(configuration); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8, device2); KisLayerSP groupLayer = new KisGroupLayer(image, "group", 200/*OPACITY_OPAQUE*/); KisLayerSP blur1 = new KisAdjustmentLayer(image, "blur1", configuration->cloneWithResourcesSnapshot(), 0); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(blur1, groupLayer); QRect testRect1(0,0,100,441); QRect testRect2(100,0,400,441); QRect testRect3(500,0,140,441); QRect testRect4(580,381,40,40); QRect cropRect(image->bounds()); KisMergeWalker walker(cropRect); KisAsyncMerger merger; walker.collectRects(paintLayer2, testRect1); merger.startMerge(walker); walker.collectRects(paintLayer2, testRect2); merger.startMerge(walker); walker.collectRects(paintLayer2, testRect3); merger.startMerge(walker); walker.collectRects(paintLayer2, testRect4); merger.startMerge(walker); // Old style merging: has artifacts at x=100 and x=500 // And should be turned on inside KisLayer /* paintLayer2->setDirty(testRect1); QTest::qSleep(3000); paintLayer2->setDirty(testRect2); QTest::qSleep(3000); paintLayer2->setDirty(testRect3); QTest::qSleep(3000); paintLayer2->setDirty(testRect4); QTest::qSleep(3000); */ KisLayerSP rootLayer = image->rootLayer(); QVERIFY(rootLayer->exactBounds() == image->bounds()); QImage resultProjection = rootLayer->projection()->convertToQImage(0); resultProjection.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + "actual_merge_result.png"); QPoint pt; QVERIFY(TestUtil::compareQImages(pt, resultProjection, referenceProjection, 5, 0, 0)); } /** * This in not fully automated test for child obliging in KisAsyncMerger. * It just checks whether devices are shared. To check if the merger * touches originals you can add a debug message to the merger * and take a look. */ /* +-----------+ |root | | group | | paint 1 | +-----------+ */ void KisAsyncMergerTest::debugObligeChild() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 640, 441, colorSpace, "merger test"); QImage sourceImage1(QString(FILES_DATA_DIR) + QDir::separator() + "hakonepa.png"); KisPaintDeviceSP device1 = new KisPaintDevice(colorSpace); device1->convertFromQImage(sourceImage1, 0, 0, 0); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); image->addNode(groupLayer, image->rootLayer()); image->addNode(paintLayer1, groupLayer); QRect testRect1(0,0,640,441); QRect cropRect(image->bounds()); KisMergeWalker walker(cropRect); KisAsyncMerger merger; walker.collectRects(paintLayer1, testRect1); merger.startMerge(walker); KisLayerSP rootLayer = image->rootLayer(); QVERIFY(rootLayer->original() == groupLayer->projection()); QVERIFY(groupLayer->original() == paintLayer1->projection()); } /* +--------------+ |root | | paint 1 | | invert_mask | | clone_of_1 | +--------------+ */ void KisAsyncMergerTest::testFullRefreshWithClones() { const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 128, 128, colorSpace, "clones test"); KisPaintDeviceSP device1 = new KisPaintDevice(colorSpace); device1->fill(image->bounds(), KoColor( Qt::white, colorSpace)); KisFilterSP filter = KisFilterRegistry::instance()->value("invert"); Q_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(KisGlobalResourcesInterface::instance()); Q_ASSERT(configuration); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisFilterMaskSP invertMask1 = new KisFilterMask(); invertMask1->initSelection(paintLayer1); invertMask1->setFilter(configuration->cloneWithResourcesSnapshot()); KisLayerSP cloneLayer1 = new KisCloneLayer(paintLayer1, image, "clone_of_1", OPACITY_OPAQUE_U8); /** * The clone layer must have a projection to allow us * to read what it got from its source. Just shift it. */ cloneLayer1->setX(10); cloneLayer1->setY(10); image->addNode(cloneLayer1, image->rootLayer()); image->addNode(paintLayer1, image->rootLayer()); image->addNode(invertMask1, paintLayer1); QRect cropRect(image->bounds()); KisFullRefreshWalker walker(cropRect); KisAsyncMerger merger; walker.collectRects(image->rootLayer(), image->bounds()); merger.startMerge(walker); // Wait for additional jobs generated by the clone are finished image->waitForDone(); QRect filledRect(10, 10, image->width() - cloneLayer1->x(), image->height() - cloneLayer1->y()); const int pixelSize = device1->pixelSize(); const int numPixels = filledRect.width() * filledRect.height(); QByteArray bytes(numPixels * pixelSize, 13); cloneLayer1->projection()->readBytes((quint8*)bytes.data(), filledRect); KoColor desiredPixel(Qt::black, colorSpace); quint8 *srcPtr = (quint8*)bytes.data(); quint8 *dstPtr = desiredPixel.data(); for(int i = 0; i < numPixels; i++) { if(memcmp(srcPtr, dstPtr, pixelSize)) { dbgKrita << "expected:" << dstPtr[0] << dstPtr[1] << dstPtr[2] << dstPtr[3]; dbgKrita << "result: " << srcPtr[0] << srcPtr[1] << srcPtr[2] << srcPtr[3]; QFAIL("Failed to compare pixels"); } srcPtr += pixelSize; } } /* +--------------+ |root | | paint 2 | | paint 1 | +--------------+ */ void KisAsyncMergerTest::testSubgraphingWithoutUpdatingParent() { const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 128, 128, colorSpace, "clones test"); KisPaintDeviceSP device1 = new KisPaintDevice(colorSpace); device1->fill(image->bounds(), KoColor(Qt::white, colorSpace)); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisPaintDeviceSP device2 = new KisPaintDevice(colorSpace); device2->fill(image->bounds(), KoColor(Qt::black, colorSpace)); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", 128, device2); image->addNode(paintLayer1, image->rootLayer()); image->addNode(paintLayer2, image->rootLayer()); image->initialRefreshGraph(); QImage refImage(QString(FILES_DATA_DIR) + QDir::separator() + "subgraphing_without_updating.png"); { QImage resultImage = image->projection()->convertToQImage(0); QCOMPARE(resultImage, refImage); } QRect cropRect(image->bounds()); KisRefreshSubtreeWalker walker(cropRect); KisAsyncMerger merger; walker.collectRects(paintLayer2, image->bounds()); merger.startMerge(walker); { QImage resultImage = image->projection()->convertToQImage(0); QCOMPARE(resultImage, refImage); } } #include enum DependentNodeType { GROUP_LAYER, ADJUSTMENT_LAYER }; /* +--------------+ |root | | (blur1) | | group | | blur_mask | | paint 2 | | paint 3 | | paint 1 | +--------------+ */ void testFullRefreshForDependentNodes(const DependentNodeType dependentNode, const bool useFilterMask, const bool useLayerStyle) { const QRect imageRect = QRect(0, 0, 128, 128); const QRect fillRect = QRect(32, 32, 64, 64); const QRect smallRect = QRect(60, 60, 8, 8); const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, imageRect.width(), imageRect.height(), colorSpace, "blur test"); KisPaintDeviceSP device1 = new KisPaintDevice(colorSpace); if (dependentNode == GROUP_LAYER && !useLayerStyle) { device1->fill(imageRect, KoColor(Qt::black, colorSpace)); } KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisPaintDeviceSP device2 = new KisPaintDevice(colorSpace); device2->fill(fillRect, KoColor(Qt::white, colorSpace)); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8, device2); device2->fill(QRect(63, 0, 1, 64), KoColor(Qt::green, colorSpace)); KisPaintDeviceSP device3 = new KisPaintDevice(colorSpace); device3->fill(smallRect, KoColor(Qt::red, colorSpace)); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8, device3); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(paintLayer3, groupLayer); KisLayerSP testingLayer = groupLayer; if (dependentNode == ADJUSTMENT_LAYER) { KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); KIS_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(KisGlobalResourcesInterface::instance()); KIS_ASSERT(configuration); KisLayerSP blur1 = new KisAdjustmentLayer(image, "blur1", configuration->cloneWithResourcesSnapshot(), 0); blur1->setCompositeOpId(COMPOSITE_OVER); image->addNode(blur1, image->rootLayer()); } if (useFilterMask) { KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); KIS_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(KisGlobalResourcesInterface::instance()); KIS_ASSERT(configuration); KisFilterMaskSP blurMask1 = new KisFilterMask(); blurMask1->initSelection(groupLayer); blurMask1->setFilter(configuration->cloneWithResourcesSnapshot()); image->addNode(blurMask1, testingLayer); } if (useLayerStyle) { KisPSDLayerStyleSP style(new KisPSDLayerStyle()); style->context()->keep_original = true; style->dropShadow()->setEffectEnabled(true); style->dropShadow()->setDistance(9); style->dropShadow()->setSpread(100); style->dropShadow()->setSize(9); style->dropShadow()->setNoise(0); style->dropShadow()->setKnocksOut(false); style->dropShadow()->setOpacity(90); style->dropShadow()->setAngle(0); testingLayer->setLayerStyle(style); } QRect cropRect(image->bounds()); KisFullRefreshWalker walker(cropRect); KisAsyncMerger merger; QVector patchRects; patchRects << QRect(0,0,64,64); patchRects << QRect(64,0,64,64); patchRects << QRect(0,64,64,64); patchRects << QRect(64,64,64,64); Q_FOREACH(const QRect &rc, patchRects) { walker.collectRects(image->rootLayer(), rc); merger.startMerge(walker); } // Wait for additional jobs generated by the clone are finished image->waitForDone(); QString testName = dependentNode == GROUP_LAYER ? "group" : "adj"; if (useFilterMask) { testName += "_mask"; } if (useLayerStyle) { testName += "_style"; } TestUtil::checkQImage(image->projection()->convertToQImage(0, imageRect), "async_merger_test", "dependent", testName, 3); } void KisAsyncMergerTest::testFullRefreshGroupWithMask() { testFullRefreshForDependentNodes(GROUP_LAYER, true, false); } void KisAsyncMergerTest::testFullRefreshGroupWithStyle() { testFullRefreshForDependentNodes(GROUP_LAYER, false, true); } void KisAsyncMergerTest::testFullRefreshGroupWithMaskAndStyle() { testFullRefreshForDependentNodes(GROUP_LAYER, true, true); } void KisAsyncMergerTest::testFullRefreshAdjustmentWithMask() { testFullRefreshForDependentNodes(ADJUSTMENT_LAYER, true, false); } void KisAsyncMergerTest::testFullRefreshAdjustmentWithStyle() { testFullRefreshForDependentNodes(ADJUSTMENT_LAYER, false, true); } +/* + +---------------------------------+ + |root | + | adj 2 (filter 2, color balance) | + | mask 3 (filter 3, blur) | + | paint 1 | + +---------------------------------+ + */ + +void KisAsyncMergerTest::testFilterMaskOnFilterLayer() +{ + { + KisImageConfig cfg(false); + cfg.setUpdatePatchWidth(64); + cfg.setUpdatePatchHeight(64); + cfg.setMaxNumberOfThreads(1); + } + KisImageConfigNotifier::instance()->notifyConfigChanged(); + + + const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->rgb8(); + KisImageSP image = new KisImage(0, 128, 128, colorSpace, "masks test"); + + KisPaintDeviceSP device1 = new KisPaintDevice(colorSpace); + device1->fill(image->bounds(), KoColor(Qt::yellow, colorSpace)); + KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); + image->addNode(paintLayer1, image->rootLayer()); + + + KisFilterSP filter2 = KisFilterRegistry::instance()->value("colorbalance"); + KIS_ASSERT(filter2); + KisFilterConfigurationSP configuration2 = filter2->defaultConfiguration(KisGlobalResourcesInterface::instance()); + KIS_ASSERT(configuration2); + KisLayerSP adjLayer2 = new KisAdjustmentLayer(image, "adj2", configuration2, 0); + image->addNode(adjLayer2, image->rootLayer()); + + + KisFilterSP filter3 = KisFilterRegistry::instance()->value("blur"); + KIS_ASSERT(filter3); + KisFilterConfigurationSP configuration3 = filter3->defaultConfiguration(KisGlobalResourcesInterface::instance()); + KIS_ASSERT(configuration3); + KisFilterMaskSP mask3 = new KisFilterMask(); + mask3->initSelection(adjLayer2); + mask3->setFilter(configuration3); + image->addNode(mask3, adjLayer2); + + image->initialRefreshGraph(); + + QVERIFY(TestUtil::checkQImage(image->projection()->convertToQImage(0), + "async_merger_test", "mask_on_adj", "initial", 3)); +} + QTEST_MAIN(KisAsyncMergerTest) diff --git a/libs/image/tests/kis_async_merger_test.h b/libs/image/tests/kis_async_merger_test.h index 6b99478af9..dff8ccd2b9 100644 --- a/libs/image/tests/kis_async_merger_test.h +++ b/libs/image/tests/kis_async_merger_test.h @@ -1,43 +1,47 @@ /* * Copyright (c) 2009 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_ASYNC_MERGER_TEST_H #define KIS_ASYNC_MERGER_TEST_H #include class KisAsyncMergerTest : public QObject { Q_OBJECT private Q_SLOTS: + void init(); + void testMerger(); void debugObligeChild(); void testFullRefreshWithClones(); void testSubgraphingWithoutUpdatingParent(); void testFullRefreshGroupWithMask(); void testFullRefreshGroupWithStyle(); void testFullRefreshGroupWithMaskAndStyle(); void testFullRefreshAdjustmentWithMask(); void testFullRefreshAdjustmentWithStyle(); + void testFilterMaskOnFilterLayer(); + }; #endif /* KIS_ASYNC_MERGER_TEST_H */ diff --git a/libs/ui/KisDocument.cpp b/libs/ui/KisDocument.cpp index 21bde41d07..02f582d65d 100644 --- a/libs/ui/KisDocument.cpp +++ b/libs/ui/KisDocument.cpp @@ -1,2326 +1,2326 @@ /* This file is part of the Krita project * * Copyright (C) 2014 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisMainWindow.h" // XXX: remove #include // XXX: remove #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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Krita Image #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_layer_utils.h" #include "kis_selection_mask.h" // Local #include "KisViewManager.h" #include "kis_clipboard.h" #include "widgets/kis_custom_image_widget.h" #include "canvas/kis_canvas2.h" #include "flake/kis_shape_controller.h" #include "kis_statusbar.h" #include "widgets/kis_progress_widget.h" #include "kis_canvas_resource_provider.h" #include "KisResourceServerProvider.h" #include "kis_node_manager.h" #include "KisPart.h" #include "KisApplication.h" #include "KisDocument.h" #include "KisImportExportManager.h" #include "KisView.h" #include "kis_grid_config.h" #include "kis_guides_config.h" #include "kis_image_barrier_lock_adapter.h" #include "KisReferenceImagesLayer.h" #include #include "kis_config_notifier.h" #include "kis_async_action_feedback.h" #include "KisCloneDocumentStroke.h" #include #include #include "kis_simple_stroke_strategy.h" // Define the protocol used here for embedded documents' URL // This used to "store" but QUrl didn't like it, // so let's simply make it "tar" ! #define STORE_PROTOCOL "tar" // The internal path is a hack to make QUrl happy and for document children #define INTERNAL_PROTOCOL "intern" #define INTERNAL_PREFIX "intern:/" // Warning, keep it sync in koStore.cc #include using namespace std; namespace { constexpr int errorMessageTimeout = 5000; constexpr int successMessageTimeout = 1000; } /********************************************************** * * KisDocument * **********************************************************/ //static QString KisDocument::newObjectName() { static int s_docIFNumber = 0; QString name; name.setNum(s_docIFNumber++); name.prepend("document_"); return name; } class UndoStack : public KUndo2Stack { public: UndoStack(KisDocument *doc) : KUndo2Stack(doc), m_doc(doc) { } void setIndex(int idx) override { KisImageWSP image = this->image(); image->requestStrokeCancellation(); if(image->tryBarrierLock()) { KUndo2Stack::setIndex(idx); image->unlock(); } } void notifySetIndexChangedOneCommand() override { KisImageWSP image = this->image(); image->unlock(); /** * Some very weird commands may emit blocking signals to * the GUI (e.g. KisGuiContextCommand). Here is the best thing * we can do to avoid the deadlock */ while(!image->tryBarrierLock()) { QApplication::processEvents(); } } void undo() override { KisImageWSP image = this->image(); image->requestUndoDuringStroke(); if (image->tryUndoUnfinishedLod0Stroke() == UNDO_OK) { return; } if(image->tryBarrierLock()) { KUndo2Stack::undo(); image->unlock(); } } void redo() override { KisImageWSP image = this->image(); if(image->tryBarrierLock()) { KUndo2Stack::redo(); image->unlock(); } } private: KisImageWSP image() { KisImageWSP currentImage = m_doc->image(); Q_ASSERT(currentImage); return currentImage; } private: KisDocument *m_doc; }; class Q_DECL_HIDDEN KisDocument::Private { public: Private(KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(_q)) // deleted by QObject , importExportManager(new KisImportExportManager(_q)) // deleted manually , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) // deleted by QObject , m_bAutoDetectedMime(false) , modified(false) , readwrite(true) , firstMod(QDateTime::currentDateTime()) , lastMod(firstMod) , nserver(new KisNameServer(1)) , imageIdleWatcher(2000 /*ms*/) , globalAssistantsColor(KisConfig(true).defaultAssistantsColor()) , savingLock(&savingMutex) , batchMode(false) { if (QLocale().measurementSystem() == QLocale::ImperialSystem) { unit = KoUnit::Inch; } else { unit = KoUnit::Centimeter; } } Private(const Private &rhs, KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(*rhs.docInfo, _q)) , importExportManager(new KisImportExportManager(_q)) , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) , nserver(new KisNameServer(*rhs.nserver)) , preActivatedNode(0) // the node is from another hierarchy! , imageIdleWatcher(2000 /*ms*/) , savingLock(&savingMutex) { copyFromImpl(rhs, _q, CONSTRUCT); } ~Private() { // Don't delete m_d->shapeController because it's in a QObject hierarchy. delete nserver; } KisDocument *q = 0; KoDocumentInfo *docInfo = 0; KoUnit unit; KisImportExportManager *importExportManager = 0; // The filter-manager to use when loading/saving [for the options] QByteArray mimeType; // The actual mimetype of the document QByteArray outputMimeType; // The mimetype to use when saving QTimer *autoSaveTimer; QString lastErrorMessage; // see openFile() QString lastWarningMessage; int autoSaveDelay = 300; // in seconds, 0 to disable. bool modifiedAfterAutosave = false; bool isAutosaving = false; bool disregardAutosaveFailure = false; int autoSaveFailureCount = 0; KUndo2Stack *undoStack = 0; KisGuidesConfig guidesConfig; KisMirrorAxisConfig mirrorAxisConfig; bool m_bAutoDetectedMime = false; // whether the mimetype in the arguments was detected by the part itself QUrl m_url; // local url - the one displayed to the user. QString m_file; // Local file - the only one the part implementation should deal with. QMutex savingMutex; bool modified = false; bool readwrite = false; QDateTime firstMod; QDateTime lastMod; KisNameServer *nserver; KisImageSP image; KisImageSP savingImage; KisNodeWSP preActivatedNode; KisShapeController* shapeController = 0; KoShapeController* koShapeController = 0; KisIdleWatcher imageIdleWatcher; QScopedPointer imageIdleConnection; QList assistants; QColor globalAssistantsColor; KisSharedPtr referenceImagesLayer; KisGridConfig gridConfig; StdLockableWrapper savingLock; bool modifiedWhileSaving = false; QScopedPointer backgroundSaveDocument; QPointer savingUpdater; QFuture childSavingFuture; KritaUtils::ExportFileJob backgroundSaveJob; bool isRecovered = false; bool batchMode { false }; QString documentStorageID {QUuid::createUuid().toString()}; KisResourceStorageSP documentResourceStorage; void syncDecorationsWrapperLayerState(); void setImageAndInitIdleWatcher(KisImageSP _image) { image = _image; imageIdleWatcher.setTrackedImage(image); if (image) { imageIdleConnection.reset( new KisSignalAutoConnection( &imageIdleWatcher, SIGNAL(startedIdleMode()), image.data(), SLOT(explicitRegenerateLevelOfDetail()))); } } void copyFrom(const Private &rhs, KisDocument *q); void copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy); /// clones the palette list oldList /// the ownership of the returned KoColorSet * belongs to the caller class StrippedSafeSavingLocker; }; void KisDocument::Private::syncDecorationsWrapperLayerState() { if (!this->image) return; KisImageSP image = this->image; KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(image->root()); const bool needsDecorationsWrapper = gridConfig.showGrid() || (guidesConfig.showGuides() && guidesConfig.hasGuides()) || !assistants.isEmpty(); struct SyncDecorationsWrapperStroke : public KisSimpleStrokeStrategy { SyncDecorationsWrapperStroke(KisDocument *document, bool needsDecorationsWrapper) : KisSimpleStrokeStrategy(QLatin1String("sync-decorations-wrapper"), kundo2_noi18n("start-isolated-mode")), m_document(document), m_needsDecorationsWrapper(needsDecorationsWrapper) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(m_document->image()->root()); if (m_needsDecorationsWrapper && !decorationsLayer) { m_document->image()->addNode(new KisDecorationsWrapperLayer(m_document)); } else if (!m_needsDecorationsWrapper && decorationsLayer) { m_document->image()->removeNode(decorationsLayer); } } private: KisDocument *m_document = 0; bool m_needsDecorationsWrapper = false; }; KisStrokeId id = image->startStroke(new SyncDecorationsWrapperStroke(q, needsDecorationsWrapper)); image->endStroke(id); } void KisDocument::Private::copyFrom(const Private &rhs, KisDocument *q) { copyFromImpl(rhs, q, KisDocument::REPLACE); } void KisDocument::Private::copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy) { if (policy == REPLACE) { delete docInfo; } docInfo = (new KoDocumentInfo(*rhs.docInfo, q)); unit = rhs.unit; mimeType = rhs.mimeType; outputMimeType = rhs.outputMimeType; if (policy == REPLACE) { q->setGuidesConfig(rhs.guidesConfig); q->setMirrorAxisConfig(rhs.mirrorAxisConfig); q->setModified(rhs.modified); q->setAssistants(KisPaintingAssistant::cloneAssistantList(rhs.assistants)); q->setGridConfig(rhs.gridConfig); } else { // in CONSTRUCT mode, we cannot use the functions of KisDocument // because KisDocument does not yet have a pointer to us. guidesConfig = rhs.guidesConfig; mirrorAxisConfig = rhs.mirrorAxisConfig; modified = rhs.modified; assistants = KisPaintingAssistant::cloneAssistantList(rhs.assistants); gridConfig = rhs.gridConfig; } m_bAutoDetectedMime = rhs.m_bAutoDetectedMime; m_url = rhs.m_url; m_file = rhs.m_file; readwrite = rhs.readwrite; firstMod = rhs.firstMod; lastMod = rhs.lastMod; // XXX: the display properties will be shared between different snapshots globalAssistantsColor = rhs.globalAssistantsColor; batchMode = rhs.batchMode; // CHECK THIS! This is what happened to the palette list -- but is it correct here as well? Ask Dmitry!!! // if (policy == REPLACE) { // QList newPaletteList = clonePaletteList(rhs.paletteList); // q->setPaletteList(newPaletteList, /* emitSignal = */ true); // // we still do not own palettes if we did not // } else { // paletteList = rhs.paletteList; // } if (rhs.documentResourceStorage) { if (policy == REPLACE) { // Clone the resources, but don't add them to the database, only the editable // version of the document should have those resources in the database. documentResourceStorage = rhs.documentResourceStorage->clone(); } else { documentResourceStorage = rhs.documentResourceStorage; } } } class KisDocument::Private::StrippedSafeSavingLocker { public: StrippedSafeSavingLocker(QMutex *savingMutex, KisImageSP image) : m_locked(false) , m_image(image) , m_savingLock(savingMutex) , m_imageLock(image, true) { /** * Initial try to lock both objects. Locking the image guards * us from any image composition threads running in the * background, while savingMutex guards us from entering the * saving code twice by autosave and main threads. * * Since we are trying to lock multiple objects, so we should * do it in a safe manner. */ m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; if (!m_locked) { m_image->requestStrokeEnd(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // one more try... m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; } } ~StrippedSafeSavingLocker() { if (m_locked) { m_imageLock.unlock(); m_savingLock.unlock(); } } bool successfullyLocked() const { return m_locked; } private: bool m_locked; KisImageSP m_image; StdLockableWrapper m_savingLock; KisImageBarrierLockAdapter m_imageLock; }; KisDocument::KisDocument(bool addStorage) : d(new Private(this)) { connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); setObjectName(newObjectName()); if (addStorage) { d->documentResourceStorage.reset(new KisResourceStorage(d->documentStorageID)); KisResourceLocator::instance()->addStorage(d->documentStorageID, d->documentResourceStorage); } // preload the krita resources KisResourceServerProvider::instance(); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); slotConfigChanged(); } KisDocument::KisDocument(const KisDocument &rhs) : QObject(), d(new Private(*rhs.d, this)) { copyFromDocumentImpl(rhs, CONSTRUCT); } KisDocument::~KisDocument() { // wait until all the pending operations are in progress waitForSavingToComplete(); /** * Push a timebomb, which will try to release the memory after * the document has been deleted */ KisPaintDevice::createMemoryReleaseObject()->deleteLater(); d->autoSaveTimer->disconnect(this); d->autoSaveTimer->stop(); delete d->importExportManager; // Despite being QObject they needs to be deleted before the image delete d->shapeController; delete d->koShapeController; if (d->image) { d->image->notifyAboutToBeDeleted(); /** * WARNING: We should wait for all the internal image jobs to * finish before entering KisImage's destructor. The problem is, * while execution of KisImage::~KisImage, all the weak shared * pointers pointing to the image enter an inconsistent * state(!). The shared counter is already zero and destruction * has started, but the weak reference doesn't know about it, * because KisShared::~KisShared hasn't been executed yet. So all * the threads running in background and having weak pointers will * enter the KisImage's destructor as well. */ d->image->requestStrokeCancellation(); d->image->waitForDone(); // clear undo commands that can still point to the image d->undoStack->clear(); d->image->waitForDone(); KisImageWSP sanityCheckPointer = d->image; Q_UNUSED(sanityCheckPointer); // The following line trigger the deletion of the image d->image.clear(); // check if the image has actually been deleted KIS_SAFE_ASSERT_RECOVER_NOOP(!sanityCheckPointer.isValid()); } if (KisResourceLocator::instance()->hasStorage(d->documentStorageID)) { KisResourceLocator::instance()->removeStorage(d->documentStorageID); } delete d; } QString KisDocument::uniqueID() const { return d->documentStorageID; } bool KisDocument::reload() { // XXX: reimplement! return false; } KisDocument *KisDocument::clone() { return new KisDocument(*this); } bool KisDocument::exportDocumentImpl(const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { QFileInfo filePathInfo(job.filePath); if (filePathInfo.exists() && !filePathInfo.isWritable()) { slotCompleteSavingDocument(job, ImportExportCodes::NoAccessToWrite, i18n("%1 cannot be written to. Please save under a different name.", job.filePath)); //return ImportExportCodes::NoAccessToWrite; return false; } KisConfig cfg(true); if (cfg.backupFile() && filePathInfo.exists()) { QString backupDir; switch(cfg.readEntry("backupfilelocation", 0)) { case 1: backupDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); break; case 2: backupDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); break; default: // Do nothing: the empty string is user file location break; } int numOfBackupsKept = cfg.readEntry("numberofbackupfiles", 1); QString suffix = cfg.readEntry("backupfilesuffix", "~"); if (numOfBackupsKept == 1) { if (!KBackup::simpleBackupFile(job.filePath, backupDir, suffix)) { qWarning() << "Failed to create simple backup file!" << job.filePath << backupDir << suffix; KisUsageLogger::log(QString("Failed to create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); return false; } else { KisUsageLogger::log(QString("Create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); } } - else if (numOfBackupsKept > 2) { + else if (numOfBackupsKept > 1) { if (!KBackup::numberedBackupFile(job.filePath, backupDir, suffix, numOfBackupsKept)) { qWarning() << "Failed to create numbered backup file!" << job.filePath << backupDir << suffix; KisUsageLogger::log(QString("Failed to create a numbered backup for %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); return false; } else { KisUsageLogger::log(QString("Create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); } } } //KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!job.mimeType.isEmpty(), false); if (job.mimeType.isEmpty()) { KisImportExportErrorCode error = ImportExportCodes::FileFormatIncorrect; slotCompleteSavingDocument(job, error, error.errorMessage()); return false; } const QString actionName = job.flags & KritaUtils::SaveIsExporting ? i18n("Exporting Document...") : i18n("Saving Document..."); bool started = initiateSavingInBackground(actionName, this, SLOT(slotCompleteSavingDocument(KritaUtils::ExportFileJob, KisImportExportErrorCode ,QString)), job, exportConfiguration); if (!started) { emit canceled(QString()); } return started; } bool KisDocument::exportDocument(const QUrl &url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; SaveFlags flags = SaveIsExporting; if (showWarnings) { flags |= SaveShowWarnings; } KisUsageLogger::log(QString("Exporting Document: %1 as %2. %3 * %4 pixels, %5 layers, %6 frames, %7 framerate. Export configuration: %8") .arg(url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration")); return exportDocumentImpl(KritaUtils::ExportFileJob(url.toLocalFile(), mimeType, flags), exportConfiguration); } bool KisDocument::saveAs(const QUrl &_url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; KisUsageLogger::log(QString("Saving Document %9 as %1 (mime: %2). %3 * %4 pixels, %5 layers. %6 frames, %7 framerate. Export configuration: %8") .arg(_url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration") .arg(url().toLocalFile())); return exportDocumentImpl(ExportFileJob(_url.toLocalFile(), mimeType, showWarnings ? SaveShowWarnings : SaveNone), exportConfiguration); } bool KisDocument::save(bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { return saveAs(url(), mimeType(), showWarnings, exportConfiguration); } QByteArray KisDocument::serializeToNativeByteArray() { QByteArray byteArray; QBuffer buffer(&byteArray); QScopedPointer filter(KisImportExportManager::filterForMimeType(nativeFormatMimeType(), KisImportExportManager::Export)); filter->setBatchMode(true); filter->setMimeType(nativeFormatMimeType()); Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return byteArray; } d->savingImage = d->image; if (!filter->convert(this, &buffer).isOk()) { qWarning() << "serializeToByteArray():: Could not export to our native format"; } return byteArray; } void KisDocument::slotCompleteSavingDocument(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { if (status.isCancelled()) return; const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during saving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); if (!fileBatchMode()) { const QString filePath = job.filePath; QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not save %1\nReason: %2", filePath, exportErrorToUserMessage(status, errorMessage))); } } else { if (!(job.flags & KritaUtils::SaveIsExporting)) { const QString existingAutoSaveBaseName = localFilePath(); const bool wasRecovered = isRecovered(); setUrl(QUrl::fromLocalFile(job.filePath)); setLocalFilePath(job.filePath); setMimeType(job.mimeType); updateEditingTime(true); if (!d->modifiedWhileSaving) { /** * If undo stack is already clean/empty, it doesn't emit any * signals, so we might forget update document modified state * (which was set, e.g. while recovering an autosave file) */ if (d->undoStack->isClean()) { setModified(false); } else { d->undoStack->setClean(); } } setRecovered(false); removeAutoSaveFiles(existingAutoSaveBaseName, wasRecovered); } emit completed(); emit sigSavingFinished(); emit statusBarMessage(i18n("Finished saving %1", fileName), successMessageTimeout); } } QByteArray KisDocument::mimeType() const { return d->mimeType; } void KisDocument::setMimeType(const QByteArray & mimeType) { d->mimeType = mimeType; } bool KisDocument::fileBatchMode() const { return d->batchMode; } void KisDocument::setFileBatchMode(const bool batchMode) { d->batchMode = batchMode; } KisDocument* KisDocument::lockAndCloneForSaving() { // force update of all the asynchronous nodes before cloning QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); KisLayerUtils::forceAllDelayedNodesUpdate(d->image->root()); KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { if (!window->viewManager()->blockUntilOperationsFinished(d->image)) { return 0; } } } Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return 0; } return new KisDocument(*this); } KisDocument *KisDocument::lockAndCreateSnapshot() { KisDocument *doc = lockAndCloneForSaving(); if (doc) { // clone the local resource storage and its contents -- that is, the old palette list if (doc->d->documentResourceStorage) { doc->d->documentResourceStorage = doc->d->documentResourceStorage->clone(); } } return doc; } void KisDocument::copyFromDocument(const KisDocument &rhs) { copyFromDocumentImpl(rhs, REPLACE); } void KisDocument::copyFromDocumentImpl(const KisDocument &rhs, CopyPolicy policy) { if (policy == REPLACE) { d->copyFrom(*(rhs.d), this); d->undoStack->clear(); } else { // in CONSTRUCT mode, d should be already initialized connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); } setObjectName(rhs.objectName()); slotConfigChanged(); if (rhs.d->image) { if (policy == REPLACE) { d->image->barrierLock(/* readOnly = */ false); rhs.d->image->barrierLock(/* readOnly = */ true); d->image->copyFromImage(*(rhs.d->image)); d->image->unlock(); rhs.d->image->unlock(); setCurrentImage(d->image, /* forceInitialUpdate = */ true); } else { // clone the image with keeping the GUIDs of the layers intact // NOTE: we expect the image to be locked! setCurrentImage(rhs.image()->clone(/* exactCopy = */ true), /* forceInitialUpdate = */ false); } } if (rhs.d->preActivatedNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.d->image->root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(d->image->root(), [&linearizedNodes, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (rhs.d->preActivatedNode.data() == refNode.data()) { d->preActivatedNode = node; } }); } // reinitialize references' signal connection KisReferenceImagesLayerSP referencesLayer = this->referenceImagesLayer(); setReferenceImagesLayer(referencesLayer, false); KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(d->image->root()); if (decorationsLayer) { decorationsLayer->setDocument(this); } if (policy == REPLACE) { setModified(true); } } bool KisDocument::exportDocumentSync(const QUrl &url, const QByteArray &mimeType, KisPropertiesConfigurationSP exportConfiguration) { { /** * The caller guarantees that no one else uses the document (usually, * it is a temporary document created specifically for exporting), so * we don't need to copy or lock the document. Instead we should just * ensure the barrier lock is synced and then released. */ Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return false; } } d->savingImage = d->image; const QString fileName = url.toLocalFile(); KisImportExportErrorCode status = d->importExportManager-> exportDocument(fileName, fileName, mimeType, false, exportConfiguration); d->savingImage = 0; return status.isOk(); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { return initiateSavingInBackground(actionName, receiverObject, receiverMethod, job, exportConfiguration, std::unique_ptr()); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration, std::unique_ptr &&optionalClonedDocument) { KIS_ASSERT_RECOVER_RETURN_VALUE(job.isValid(), false); QScopedPointer clonedDocument; if (!optionalClonedDocument) { clonedDocument.reset(lockAndCloneForSaving()); } else { clonedDocument.reset(optionalClonedDocument.release()); } // we block saving until the current saving is finished! if (!clonedDocument || !d->savingMutex.tryLock()) { return false; } auto waitForImage = [] (KisImageSP image) { KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { window->viewManager()->blockUntilOperationsFinishedForced(image); } } }; { KisNodeSP newRoot = clonedDocument->image()->root(); KIS_SAFE_ASSERT_RECOVER(!KisLayerUtils::hasDelayedNodeWithUpdates(newRoot)) { KisLayerUtils::forceAllDelayedNodesUpdate(newRoot); waitForImage(clonedDocument->image()); } } if (clonedDocument->image()->hasOverlaySelectionMask()) { clonedDocument->image()->setOverlaySelectionMask(0); waitForImage(clonedDocument->image()); } KIS_SAFE_ASSERT_RECOVER(clonedDocument->image()->isIdle()) { waitForImage(clonedDocument->image()); } KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveDocument, false); KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveJob.isValid(), false); d->backgroundSaveDocument.reset(clonedDocument.take()); d->backgroundSaveJob = job; d->modifiedWhileSaving = false; if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = true; } connect(d->backgroundSaveDocument.data(), SIGNAL(sigBackgroundSavingFinished(KisImportExportErrorCode, QString)), this, SLOT(slotChildCompletedSavingInBackground(KisImportExportErrorCode, QString))); connect(this, SIGNAL(sigCompleteBackgroundSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), receiverObject, receiverMethod, Qt::UniqueConnection); bool started = d->backgroundSaveDocument->startExportInBackground(actionName, job.filePath, job.filePath, job.mimeType, job.flags & KritaUtils::SaveShowWarnings, exportConfiguration); if (!started) { // the state should have been deinitialized in slotChildCompletedSavingInBackground() KIS_SAFE_ASSERT_RECOVER (!d->backgroundSaveDocument && !d->backgroundSaveJob.isValid()) { d->backgroundSaveDocument.take()->deleteLater(); d->savingMutex.unlock(); d->backgroundSaveJob = KritaUtils::ExportFileJob(); } } return started; } void KisDocument::slotChildCompletedSavingInBackground(KisImportExportErrorCode status, const QString &errorMessage) { KIS_ASSERT_RECOVER_RETURN(isSaving()); KIS_ASSERT_RECOVER(d->backgroundSaveDocument) { d->savingMutex.unlock(); return; } if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = false; } d->backgroundSaveDocument.take()->deleteLater(); KIS_ASSERT_RECOVER(d->backgroundSaveJob.isValid()) { d->savingMutex.unlock(); return; } const KritaUtils::ExportFileJob job = d->backgroundSaveJob; d->backgroundSaveJob = KritaUtils::ExportFileJob(); // unlock at the very end d->savingMutex.unlock(); QFileInfo fi(job.filePath); KisUsageLogger::log(QString("Completed saving %1 (mime: %2). Result: %3. Size: %4. MD5 Hash: %5") .arg(job.filePath) .arg(QString::fromLatin1(job.mimeType)) .arg(!status.isOk() ? exportErrorToUserMessage(status, errorMessage) : "OK") .arg(fi.size()) .arg(QString::fromLatin1(KoMD5Generator().generateHash(job.filePath).toHex()))); emit sigCompleteBackgroundSaving(job, status, errorMessage); } void KisDocument::slotAutoSaveImpl(std::unique_ptr &&optionalClonedDocument) { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); emit statusBarMessage(i18n("Autosaving... %1", autoSaveFileName), successMessageTimeout); KisUsageLogger::log(QString("Autosaving: %1").arg(autoSaveFileName)); const bool hadClonedDocument = bool(optionalClonedDocument); bool started = false; if (d->image->isIdle() || hadClonedDocument) { started = initiateSavingInBackground(i18n("Autosaving..."), this, SLOT(slotCompleteAutoSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), KritaUtils::ExportFileJob(autoSaveFileName, nativeFormatMimeType(), KritaUtils::SaveIsExporting | KritaUtils::SaveInAutosaveMode), 0, std::move(optionalClonedDocument)); } else { emit statusBarMessage(i18n("Autosaving postponed: document is busy..."), errorMessageTimeout); } if (!started && !hadClonedDocument && d->autoSaveFailureCount >= 3) { KisCloneDocumentStroke *stroke = new KisCloneDocumentStroke(this); connect(stroke, SIGNAL(sigDocumentCloned(KisDocument*)), this, SLOT(slotInitiateAsyncAutosaving(KisDocument*)), Qt::BlockingQueuedConnection); KisStrokeId strokeId = d->image->startStroke(stroke); d->image->endStroke(strokeId); setInfiniteAutoSaveInterval(); } else if (!started) { setEmergencyAutoSaveInterval(); } else { d->modifiedAfterAutosave = false; } } void KisDocument::slotAutoSave() { slotAutoSaveImpl(std::unique_ptr()); } void KisDocument::slotInitiateAsyncAutosaving(KisDocument *clonedDocument) { slotAutoSaveImpl(std::unique_ptr(clonedDocument)); } void KisDocument::slotCompleteAutoSaving(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { Q_UNUSED(job); const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { setEmergencyAutoSaveInterval(); emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during autosaving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); } else { KisConfig cfg(true); d->autoSaveDelay = cfg.autoSaveInterval(); if (!d->modifiedWhileSaving) { d->autoSaveTimer->stop(); // until the next change d->autoSaveFailureCount = 0; } else { setNormalAutoSaveInterval(); } emit statusBarMessage(i18n("Finished autosaving %1", fileName), successMessageTimeout); } } bool KisDocument::startExportInBackground(const QString &actionName, const QString &location, const QString &realLocation, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { d->savingImage = d->image; KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { d->savingUpdater = window->viewManager()->createThreadedUpdater(actionName); d->importExportManager->setUpdater(d->savingUpdater); } } KisImportExportErrorCode initializationStatus(ImportExportCodes::OK); d->childSavingFuture = d->importExportManager->exportDocumentAsyc(location, realLocation, mimeType, initializationStatus, showWarnings, exportConfiguration); if (!initializationStatus.isOk()) { if (d->savingUpdater) { d->savingUpdater->cancel(); } d->savingImage.clear(); emit sigBackgroundSavingFinished(initializationStatus, initializationStatus.errorMessage()); return false; } typedef QFutureWatcher StatusWatcher; StatusWatcher *watcher = new StatusWatcher(); watcher->setFuture(d->childSavingFuture); connect(watcher, SIGNAL(finished()), SLOT(finishExportInBackground())); connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater())); return true; } void KisDocument::finishExportInBackground() { KIS_SAFE_ASSERT_RECOVER(d->childSavingFuture.isFinished()) { emit sigBackgroundSavingFinished(ImportExportCodes::InternalError, ""); return; } KisImportExportErrorCode status = d->childSavingFuture.result(); const QString errorMessage = status.errorMessage(); d->savingImage.clear(); d->childSavingFuture = QFuture(); d->lastErrorMessage.clear(); if (d->savingUpdater) { d->savingUpdater->setProgress(100); } emit sigBackgroundSavingFinished(status, errorMessage); } void KisDocument::setReadWrite(bool readwrite) { d->readwrite = readwrite; setNormalAutoSaveInterval(); Q_FOREACH (KisMainWindow *mainWindow, KisPart::instance()->mainWindows()) { mainWindow->setReadWrite(readwrite); } } void KisDocument::setAutoSaveDelay(int delay) { if (isReadWrite() && delay > 0) { d->autoSaveTimer->start(delay * 1000); } else { d->autoSaveTimer->stop(); } } void KisDocument::setNormalAutoSaveInterval() { setAutoSaveDelay(d->autoSaveDelay); d->autoSaveFailureCount = 0; } void KisDocument::setEmergencyAutoSaveInterval() { const int emergencyAutoSaveInterval = 10; /* sec */ setAutoSaveDelay(emergencyAutoSaveInterval); d->autoSaveFailureCount++; } void KisDocument::setInfiniteAutoSaveInterval() { setAutoSaveDelay(-1); } KoDocumentInfo *KisDocument::documentInfo() const { return d->docInfo; } bool KisDocument::isModified() const { return d->modified; } QPixmap KisDocument::generatePreview(const QSize& size) { KisImageSP image = d->image; if (d->savingImage) image = d->savingImage; if (image) { QRect bounds = image->bounds(); QSize newSize = bounds.size(); newSize.scale(size, Qt::KeepAspectRatio); QPixmap px = QPixmap::fromImage(image->convertToQImage(newSize, 0)); if (px.size() == QSize(0,0)) { px = QPixmap(newSize); QPainter gc(&px); QBrush checkBrush = QBrush(KisCanvasWidgetBase::createCheckersImage(newSize.width() / 5)); gc.fillRect(px.rect(), checkBrush); gc.end(); } return px; } return QPixmap(size); } QString KisDocument::generateAutoSaveFileName(const QString & path) const { QString retval; // Using the extension allows to avoid relying on the mime magic when opening const QString extension (".kra"); QString prefix = KisConfig(true).readEntry("autosavefileshidden") ? QString(".") : QString(); QRegularExpression autosavePattern1("^\\..+-autosave.kra$"); QRegularExpression autosavePattern2("^.+-autosave.kra$"); QFileInfo fi(path); QString dir = fi.absolutePath(); QString filename = fi.fileName(); if (path.isEmpty() || autosavePattern1.match(filename).hasMatch() || autosavePattern2.match(filename).hasMatch() || !fi.isWritable()) { // Never saved? #ifdef Q_OS_WIN // On Windows, use the temp location (https://bugs.kde.org/show_bug.cgi?id=314921) retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::tempPath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #else // On Linux, use a temp file in $HOME then. Mark it with the pid so two instances don't overwrite each other's autosave file retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::homePath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #endif } else { retval = QString("%1%2%5%3-autosave%4").arg(dir).arg(QDir::separator()).arg(filename).arg(extension).arg(prefix); } //qDebug() << "generateAutoSaveFileName() for path" << path << ":" << retval; return retval; } bool KisDocument::importDocument(const QUrl &_url) { bool ret; dbgUI << "url=" << _url.url(); // open... ret = openUrl(_url); // reset url & m_file (kindly? set by KisParts::openUrl()) to simulate a // File --> Import if (ret) { dbgUI << "success, resetting url"; resetURL(); setTitleModified(); } return ret; } bool KisDocument::openUrl(const QUrl &_url, OpenFlags flags) { if (!_url.isLocalFile()) { return false; } dbgUI << "url=" << _url.url(); d->lastErrorMessage.clear(); // Reimplemented, to add a check for autosave files and to improve error reporting if (!_url.isValid()) { d->lastErrorMessage = i18n("Malformed URL\n%1", _url.url()); // ## used anywhere ? return false; } QUrl url(_url); bool autosaveOpened = false; if (url.isLocalFile() && !fileBatchMode()) { QString file = url.toLocalFile(); QString asf = generateAutoSaveFileName(file); if (QFile::exists(asf)) { KisApplication *kisApp = static_cast(qApp); kisApp->hideSplashScreen(); //qDebug() <<"asf=" << asf; // ## TODO compare timestamps ? int res = QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("An autosaved file exists for this document.\nDo you want to open the autosaved file instead?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes); switch (res) { case QMessageBox::Yes : url.setPath(asf); autosaveOpened = true; break; case QMessageBox::No : KisUsageLogger::log(QString("Removing autosave file: %1").arg(asf)); QFile::remove(asf); break; default: // Cancel return false; } } } bool ret = openUrlInternal(url); if (autosaveOpened || flags & RecoveryFile) { setReadWrite(true); // enable save button setModified(true); setRecovered(true); } else { if (ret) { if (!(flags & DontAddToRecent)) { KisPart::instance()->addRecentURLToAllMainWindows(_url); } // Detect readonly local-files; remote files are assumed to be writable QFileInfo fi(url.toLocalFile()); setReadWrite(fi.isWritable()); } setRecovered(false); } return ret; } class DlgLoadMessages : public KoDialog { public: DlgLoadMessages(const QString &title, const QString &message, const QStringList &warnings) { setWindowTitle(title); setWindowIcon(KisIconUtils::loadIcon("warning")); QWidget *page = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(page); QHBoxLayout *hlayout = new QHBoxLayout(); QLabel *labelWarning= new QLabel(); labelWarning->setPixmap(KisIconUtils::loadIcon("warning").pixmap(32, 32)); hlayout->addWidget(labelWarning); hlayout->addWidget(new QLabel(message)); layout->addLayout(hlayout); QTextBrowser *browser = new QTextBrowser(); QString warning = "

"; if (warnings.size() == 1) { warning += " Reason:

"; } else { warning += " Reasons:

"; } warning += "

    "; Q_FOREACH(const QString &w, warnings) { warning += "\n
  • " + w + "
  • "; } warning += "
"; browser->setHtml(warning); browser->setMinimumHeight(200); browser->setMinimumWidth(400); layout->addWidget(browser); setMainWidget(page); setButtons(KoDialog::Ok); resize(minimumSize()); } }; bool KisDocument::openFile() { //dbgUI <<"for" << localFilePath(); if (!QFile::exists(localFilePath()) && !fileBatchMode()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("File %1 does not exist.", localFilePath())); return false; } QString filename = localFilePath(); QString typeName = mimeType(); if (typeName.isEmpty()) { typeName = KisMimeDatabase::mimeTypeForFile(filename); } //qDebug() << "mimetypes 4:" << typeName; // Allow to open backup files, don't keep the mimetype application/x-trash. if (typeName == "application/x-trash") { QString path = filename; while (path.length() > 0) { path.chop(1); typeName = KisMimeDatabase::mimeTypeForFile(path); //qDebug() << "\t" << path << typeName; if (!typeName.isEmpty()) { break; } } //qDebug() << "chopped" << filename << "to" << path << "Was trash, is" << typeName; } dbgUI << localFilePath() << "type:" << typeName; KisMainWindow *window = KisPart::instance()->currentMainwindow(); KoUpdaterPtr updater; if (window && window->viewManager()) { updater = window->viewManager()->createUnthreadedUpdater(i18n("Opening document")); d->importExportManager->setUpdater(updater); } KisImportExportErrorCode status = d->importExportManager->importDocument(localFilePath(), typeName); if (!status.isOk()) { if (window && window->viewManager()) { updater->cancel(); } QString msg = status.errorMessage(); if (!msg.isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("Could not open %2.\nReason: %1.", msg, prettyPathOrUrl()), errorMessage().split("\n") + warningMessage().split("\n")); dlg.exec(); } return false; } else if (!warningMessage().isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("There were problems opening %1.", prettyPathOrUrl()), warningMessage().split("\n")); dlg.exec(); setUrl(QUrl()); } setMimeTypeAfterLoading(typeName); d->syncDecorationsWrapperLayerState(); emit sigLoadingFinished(); undoStack()->clear(); return true; } void KisDocument::autoSaveOnPause() { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); QUrl url("file:/" + autoSaveFileName); bool started = exportDocumentSync(url, nativeFormatMimeType()); if (started) { d->modifiedAfterAutosave = false; dbgAndroid << "autoSaveOnPause successful"; } else { qWarning() << "Could not auto-save when paused"; } } // shared between openFile and koMainWindow's "create new empty document" code void KisDocument::setMimeTypeAfterLoading(const QString& mimeType) { d->mimeType = mimeType.toLatin1(); d->outputMimeType = d->mimeType; } bool KisDocument::loadNativeFormat(const QString & file_) { return openUrl(QUrl::fromLocalFile(file_)); } void KisDocument::setModified(bool mod) { if (mod) { updateEditingTime(false); } if (d->isAutosaving) // ignore setModified calls due to autosaving return; if ( !d->readwrite && d->modified ) { errKrita << "Can't set a read-only document to 'modified' !" << endl; return; } //dbgUI<<" url:" << url.path(); //dbgUI<<" mod="<docInfo->aboutInfo("editing-time").toInt() + d->firstMod.secsTo(d->lastMod))); d->firstMod = now; } else if (firstModDelta > 60 || forceStoreElapsed) { d->docInfo->setAboutInfo("editing-time", QString::number(d->docInfo->aboutInfo("editing-time").toInt() + firstModDelta)); d->firstMod = now; } d->lastMod = now; } QString KisDocument::prettyPathOrUrl() const { QString _url(url().toDisplayString()); #ifdef Q_OS_WIN if (url().isLocalFile()) { _url = QDir::toNativeSeparators(_url); } #endif return _url; } // Get caption from document info (title(), in about page) QString KisDocument::caption() const { QString c; const QString _url(url().fileName()); // if URL is empty...it is probably an unsaved file if (_url.isEmpty()) { c = " [" + i18n("Not Saved") + "] "; } else { c = _url; // Fall back to document URL } return c; } void KisDocument::setTitleModified() { emit titleModified(caption(), isModified()); } QDomDocument KisDocument::createDomDocument(const QString& tagName, const QString& version) const { return createDomDocument("krita", tagName, version); } //static QDomDocument KisDocument::createDomDocument(const QString& appName, const QString& tagName, const QString& version) { QDomImplementation impl; QString url = QString("http://www.calligra.org/DTD/%1-%2.dtd").arg(appName).arg(version); QDomDocumentType dtype = impl.createDocumentType(tagName, QString("-//KDE//DTD %1 %2//EN").arg(appName).arg(version), url); // The namespace URN doesn't need to include the version number. QString namespaceURN = QString("http://www.calligra.org/DTD/%1").arg(appName); QDomDocument doc = impl.createDocument(namespaceURN, tagName, dtype); doc.insertBefore(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""), doc.documentElement()); return doc; } bool KisDocument::isNativeFormat(const QByteArray& mimetype) const { if (mimetype == nativeFormatMimeType()) return true; return extraNativeMimeTypes().contains(mimetype); } void KisDocument::setErrorMessage(const QString& errMsg) { d->lastErrorMessage = errMsg; } QString KisDocument::errorMessage() const { return d->lastErrorMessage; } void KisDocument::setWarningMessage(const QString& warningMsg) { d->lastWarningMessage = warningMsg; } QString KisDocument::warningMessage() const { return d->lastWarningMessage; } void KisDocument::removeAutoSaveFiles(const QString &autosaveBaseName, bool wasRecovered) { // Eliminate any auto-save file QString asf = generateAutoSaveFileName(autosaveBaseName); // the one in the current dir if (QFile::exists(asf)) { KisUsageLogger::log(QString("Removing autosave file: %1").arg(asf)); QFile::remove(asf); } asf = generateAutoSaveFileName(QString()); // and the one in $HOME if (QFile::exists(asf)) { KisUsageLogger::log(QString("Removing autosave file: %1").arg(asf)); QFile::remove(asf); } QList expressions; expressions << QRegularExpression("^\\..+-autosave.kra$") << QRegularExpression("^.+-autosave.kra$"); Q_FOREACH(const QRegularExpression &rex, expressions) { if (wasRecovered && !autosaveBaseName.isEmpty() && rex.match(QFileInfo(autosaveBaseName).fileName()).hasMatch() && QFile::exists(autosaveBaseName)) { KisUsageLogger::log(QString("Removing autosave file: %1").arg(autosaveBaseName)); QFile::remove(autosaveBaseName); } } } KoUnit KisDocument::unit() const { return d->unit; } void KisDocument::setUnit(const KoUnit &unit) { if (d->unit != unit) { d->unit = unit; emit unitChanged(unit); } } KUndo2Stack *KisDocument::undoStack() { return d->undoStack; } KisImportExportManager *KisDocument::importExportManager() const { return d->importExportManager; } void KisDocument::addCommand(KUndo2Command *command) { if (command) d->undoStack->push(command); } void KisDocument::beginMacro(const KUndo2MagicString & text) { d->undoStack->beginMacro(text); } void KisDocument::endMacro() { d->undoStack->endMacro(); } void KisDocument::slotUndoStackCleanChanged(bool value) { setModified(!value); } void KisDocument::slotConfigChanged() { KisConfig cfg(true); if (d->undoStack->undoLimit() != cfg.undoStackLimit()) { if (!d->undoStack->isClean()) { d->undoStack->clear(); } d->undoStack->setUndoLimit(cfg.undoStackLimit()); } d->autoSaveDelay = cfg.autoSaveInterval(); setNormalAutoSaveInterval(); } void KisDocument::slotImageRootChanged() { d->syncDecorationsWrapperLayerState(); } void KisDocument::clearUndoHistory() { d->undoStack->clear(); } KisGridConfig KisDocument::gridConfig() const { return d->gridConfig; } void KisDocument::setGridConfig(const KisGridConfig &config) { if (d->gridConfig != config) { d->gridConfig = config; d->syncDecorationsWrapperLayerState(); emit sigGridConfigChanged(config); } } QList KisDocument::paletteList() { qDebug() << "PALETTELIST storage" << d->documentResourceStorage; QList _paletteList; if (d->documentResourceStorage.isNull()) { qWarning() << "No documentstorage for palettes"; return _paletteList; } QSharedPointer iter = d->documentResourceStorage->resources(ResourceType::Palettes); while (iter->hasNext()) { iter->next(); KoResourceSP resource = iter->resource(); if (resource && resource->valid()) { _paletteList << resource.dynamicCast(); } } return _paletteList; } void KisDocument::setPaletteList(const QList &paletteList, bool emitSignal) { qDebug() << "SET PALETTE LIST" << paletteList.size() << "storage" << d->documentResourceStorage; QList oldPaletteList; if (d->documentResourceStorage) { QSharedPointer iter = d->documentResourceStorage->resources(ResourceType::Palettes); while (iter->hasNext()) { iter->next(); KoResourceSP resource = iter->resource(); if (resource && resource->valid()) { oldPaletteList << resource.dynamicCast(); } } if (oldPaletteList != paletteList) { KisResourceModel *resourceModel = KisResourceModelProvider::resourceModel(ResourceType::Palettes); Q_FOREACH(KoColorSetSP palette, oldPaletteList) { resourceModel->removeResource(palette); } Q_FOREACH(KoColorSetSP palette, paletteList) { qDebug()<< "loading palette into document" << palette->filename(); resourceModel->addResource(palette, d->documentStorageID); } if (emitSignal) { emit sigPaletteListChanged(oldPaletteList, paletteList); } } } } const KisGuidesConfig& KisDocument::guidesConfig() const { return d->guidesConfig; } void KisDocument::setGuidesConfig(const KisGuidesConfig &data) { if (d->guidesConfig == data) return; d->guidesConfig = data; d->syncDecorationsWrapperLayerState(); emit sigGuidesConfigChanged(d->guidesConfig); } const KisMirrorAxisConfig& KisDocument::mirrorAxisConfig() const { return d->mirrorAxisConfig; } void KisDocument::setMirrorAxisConfig(const KisMirrorAxisConfig &config) { if (d->mirrorAxisConfig == config) { return; } d->mirrorAxisConfig = config; setModified(true); emit sigMirrorAxisConfigChanged(); } void KisDocument::resetURL() { setUrl(QUrl()); setLocalFilePath(QString()); } KoDocumentInfoDlg *KisDocument::createDocumentInfoDialog(QWidget *parent, KoDocumentInfo *docInfo) const { return new KoDocumentInfoDlg(parent, docInfo); } bool KisDocument::isReadWrite() const { return d->readwrite; } QUrl KisDocument::url() const { return d->m_url; } bool KisDocument::closeUrl(bool promptToSave) { if (promptToSave) { if ( isReadWrite() && isModified()) { Q_FOREACH (KisView *view, KisPart::instance()->views()) { if (view && view->document() == this) { if (!view->queryClose()) { return false; } } } } } // Not modified => ok and delete temp file. d->mimeType = QByteArray(); // It always succeeds for a read-only part, // but the return value exists for reimplementations // (e.g. pressing cancel for a modified read-write part) return true; } void KisDocument::setUrl(const QUrl &url) { d->m_url = url; } QString KisDocument::localFilePath() const { return d->m_file; } void KisDocument::setLocalFilePath( const QString &localFilePath ) { d->m_file = localFilePath; } bool KisDocument::openUrlInternal(const QUrl &url) { if ( !url.isValid() ) { return false; } if (d->m_bAutoDetectedMime) { d->mimeType = QByteArray(); d->m_bAutoDetectedMime = false; } QByteArray mimetype = d->mimeType; if ( !closeUrl() ) { return false; } d->mimeType = mimetype; setUrl(url); d->m_file.clear(); if (d->m_url.isLocalFile()) { d->m_file = d->m_url.toLocalFile(); bool ret; // set the mimetype only if it was not already set (for example, by the host application) if (d->mimeType.isEmpty()) { // get the mimetype of the file // using findByUrl() to avoid another string -> url conversion QString mime = KisMimeDatabase::mimeTypeForFile(d->m_url.toLocalFile()); d->mimeType = mime.toLocal8Bit(); d->m_bAutoDetectedMime = true; } setUrl(d->m_url); ret = openFile(); if (ret) { emit completed(); } else { emit canceled(QString()); } return ret; } return false; } bool KisDocument::newImage(const QString& name, qint32 width, qint32 height, const KoColorSpace* cs, const KoColor &bgColor, KisConfig::BackgroundStyle bgStyle, int numberOfLayers, const QString &description, const double imageResolution) { Q_ASSERT(cs); KisImageSP image; if (!cs) return false; QApplication::setOverrideCursor(Qt::BusyCursor); image = new KisImage(createUndoStore(), width, height, cs, name); Q_CHECK_PTR(image); connect(image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); image->setResolution(imageResolution, imageResolution); image->assignImageProfile(cs->profile()); image->waitForDone(); documentInfo()->setAboutInfo("title", name); documentInfo()->setAboutInfo("abstract", description); KisLayerSP layer; if (bgStyle == KisConfig::RASTER_LAYER || bgStyle == KisConfig::FILL_LAYER) { KoColor strippedAlpha = bgColor; strippedAlpha.setOpacity(OPACITY_OPAQUE_U8); if (bgStyle == KisConfig::RASTER_LAYER) { layer = new KisPaintLayer(image.data(), "Background", OPACITY_OPAQUE_U8, cs);; layer->paintDevice()->setDefaultPixel(strippedAlpha); } else if (bgStyle == KisConfig::FILL_LAYER) { KisFilterConfigurationSP filter_config = KisGeneratorRegistry::instance()->get("color")->defaultConfiguration(KisGlobalResourcesInterface::instance()); filter_config->setProperty("color", strippedAlpha.toQColor()); filter_config->createLocalResourcesSnapshot(); layer = new KisGeneratorLayer(image.data(), "Background Fill", filter_config, image->globalSelection()); } layer->setOpacity(bgColor.opacityU8()); if (numberOfLayers > 1) { //Lock bg layer if others are present. layer->setUserLocked(true); } } else { // KisConfig::CANVAS_COLOR (needs an unlocked starting layer). image->setDefaultProjectionColor(bgColor); layer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, cs); } Q_CHECK_PTR(layer); image->addNode(layer.data(), image->rootLayer().data()); layer->setDirty(QRect(0, 0, width, height)); setCurrentImage(image); for(int i = 1; i < numberOfLayers; ++i) { KisPaintLayerSP layer = new KisPaintLayer(image, image->nextLayerName(), OPACITY_OPAQUE_U8, cs); image->addNode(layer, image->root(), i); layer->setDirty(QRect(0, 0, width, height)); } KisConfig cfg(false); cfg.defImageWidth(width); cfg.defImageHeight(height); cfg.defImageResolution(imageResolution); cfg.defColorModel(image->colorSpace()->colorModelId().id()); cfg.setDefaultColorDepth(image->colorSpace()->colorDepthId().id()); cfg.defColorProfile(image->colorSpace()->profile()->name()); KisUsageLogger::log(QString("Created image \"%1\", %2 * %3 pixels, %4 dpi. Color model: %6 %5 (%7). Layers: %8") .arg(name) .arg(width).arg(height) .arg(imageResolution * 72.0) .arg(image->colorSpace()->colorModelId().name()) .arg(image->colorSpace()->colorDepthId().name()) .arg(image->colorSpace()->profile()->name()) .arg(numberOfLayers)); QApplication::restoreOverrideCursor(); return true; } bool KisDocument::isSaving() const { const bool result = d->savingMutex.tryLock(); if (result) { d->savingMutex.unlock(); } return !result; } void KisDocument::waitForSavingToComplete() { if (isSaving()) { KisAsyncActionFeedback f(i18nc("progress dialog message when the user closes the document that is being saved", "Waiting for saving to complete..."), 0); f.waitForMutex(&d->savingMutex); } } KoShapeControllerBase *KisDocument::shapeController() const { return d->shapeController; } KoShapeLayer* KisDocument::shapeForNode(KisNodeSP layer) const { return d->shapeController->shapeForNode(layer); } QList KisDocument::assistants() const { return d->assistants; } void KisDocument::setAssistants(const QList &value) { if (d->assistants != value) { d->assistants = value; d->syncDecorationsWrapperLayerState(); emit sigAssistantsChanged(); } } KisReferenceImagesLayerSP KisDocument::referenceImagesLayer() const { if (!d->image) return KisReferenceImagesLayerSP(); KisReferenceImagesLayerSP referencesLayer = KisLayerUtils::findNodeByType(d->image->root()); return referencesLayer; } void KisDocument::setReferenceImagesLayer(KisSharedPtr layer, bool updateImage) { KisReferenceImagesLayerSP currentReferenceLayer = referenceImagesLayer(); if (currentReferenceLayer == layer) { return; } if (currentReferenceLayer) { currentReferenceLayer->disconnect(this); } if (updateImage) { if (currentReferenceLayer) { d->image->removeNode(currentReferenceLayer); } if (layer) { d->image->addNode(layer); } } currentReferenceLayer = layer; if (currentReferenceLayer) { connect(currentReferenceLayer, SIGNAL(sigUpdateCanvas(QRectF)), this, SIGNAL(sigReferenceImagesChanged())); } emit sigReferenceImagesLayerChanged(layer); } void KisDocument::setPreActivatedNode(KisNodeSP activatedNode) { d->preActivatedNode = activatedNode; } KisNodeSP KisDocument::preActivatedNode() const { return d->preActivatedNode; } KisImageWSP KisDocument::image() const { return d->image; } KisImageSP KisDocument::savingImage() const { return d->savingImage; } void KisDocument::setCurrentImage(KisImageSP image, bool forceInitialUpdate) { if (d->image) { // Disconnect existing sig/slot connections d->image->setUndoStore(new KisDumbUndoStore()); d->image->disconnect(this); d->shapeController->setImage(0); d->image = 0; } if (!image) return; if (d->documentResourceStorage){ d->documentResourceStorage->setMetaData(KisResourceStorage::s_meta_name, image->objectName()); } d->setImageAndInitIdleWatcher(image); d->image->setUndoStore(new KisDocumentUndoStore(this)); d->shapeController->setImage(image); setModified(false); connect(d->image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); connect(d->image, SIGNAL(sigLayersChangedAsync()), this, SLOT(slotImageRootChanged())); if (forceInitialUpdate) { d->image->initialRefreshGraph(); } } void KisDocument::hackPreliminarySetImage(KisImageSP image) { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->image); // we set image without connecting idle-watcher, because loading // hasn't been finished yet d->image = image; d->shapeController->setImage(image); } void KisDocument::setImageModified() { // we only set as modified if undo stack is not at clean state setModified(!d->undoStack->isClean()); } KisUndoStore* KisDocument::createUndoStore() { return new KisDocumentUndoStore(this); } bool KisDocument::isAutosaving() const { return d->isAutosaving; } QString KisDocument::exportErrorToUserMessage(KisImportExportErrorCode status, const QString &errorMessage) { return errorMessage.isEmpty() ? status.errorMessage() : errorMessage; } void KisDocument::setAssistantsGlobalColor(QColor color) { d->globalAssistantsColor = color; } QColor KisDocument::assistantsGlobalColor() { return d->globalAssistantsColor; } QRectF KisDocument::documentBounds() const { QRectF bounds = d->image->bounds(); KisReferenceImagesLayerSP referenceImagesLayer = this->referenceImagesLayer(); if (referenceImagesLayer) { bounds |= referenceImagesLayer->boundingImageRect(); } return bounds; } diff --git a/libs/ui/KisMainWindow.cpp b/libs/ui/KisMainWindow.cpp index ceff49821f..0ce13fc3ad 100644 --- a/libs/ui/KisMainWindow.cpp +++ b/libs/ui/KisMainWindow.cpp @@ -1,2851 +1,2857 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2006 David Faure Copyright (C) 2007, 2009 Thomas zander Copyright (C) 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisMainWindow.h" #include // qt includes #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 #include #include #include #include #include #include "kis_selection_manager.h" #include "kis_icon_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoDockFactoryBase.h" #include "KoDocumentInfoDlg.h" #include "KoDocumentInfo.h" #include "KoFileDialog.h" #include #include #include #include #include #include "KoToolDocker.h" #include "KoToolBoxDocker_p.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #endif #include #include #include "dialogs/kis_about_application.h" #include "dialogs/kis_delayed_save_dialog.h" #include "dialogs/kis_dlg_preferences.h" #include "kis_action_manager.h" #include "KisApplication.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_canvas_resource_provider.h" #include "kis_clipboard.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_custom_image_widget.h" #include #include "kis_group_layer.h" #include "kis_image_from_clipboard_widget.h" #include "kis_image.h" #include #include "KisImportExportManager.h" #include "kis_mainwindow_observer.h" #include "kis_memory_statistics_server.h" #include "kis_node.h" #include "KisOpenPane.h" #include "kis_paintop_box.h" #include "KisPart.h" #include "KisPrintJob.h" #include "KisResourceServerProvider.h" #include "kis_signal_compressor_with_param.h" #include "kis_statusbar.h" #include "KisView.h" #include "KisViewManager.h" #include "thememanager.h" #include "kis_animation_importer.h" #include "dialogs/kis_dlg_import_image_sequence.h" #include #include "KisWindowLayoutManager.h" #include #include "KisWelcomePageWidget.h" #include #include #include "KisCanvasWindow.h" #include "kis_action.h" #include class ToolDockerFactory : public KoDockFactoryBase { public: ToolDockerFactory() : KoDockFactoryBase() { } QString id() const override { return "sharedtooldocker"; } QDockWidget* createDockWidget() override { KoToolDocker* dockWidget = new KoToolDocker(); return dockWidget; } DockPosition defaultDockPosition() const override { return DockRight; } }; class Q_DECL_HIDDEN KisMainWindow::Private { public: Private(KisMainWindow *parent, QUuid id) : q(parent) , id(id) , dockWidgetMenu(new KActionMenu(i18nc("@action:inmenu", "&Dockers"), parent)) , windowMenu(new KActionMenu(i18nc("@action:inmenu", "&Window"), parent)) , documentMenu(new KActionMenu(i18nc("@action:inmenu", "New &View"), parent)) , workspaceMenu(new KActionMenu(i18nc("@action:inmenu", "Wor&kspace"), parent)) , welcomePage(new KisWelcomePageWidget(parent)) , widgetStack(new QStackedWidget(parent)) , mdiArea(new QMdiArea(parent)) , windowMapper(new KisSignalMapper(parent)) , documentMapper(new KisSignalMapper(parent)) #ifdef Q_OS_ANDROID , fileManager(new KisAndroidFileManager(parent)) #endif { if (id.isNull()) this->id = QUuid::createUuid(); welcomeScroller = new QScrollArea(); welcomeScroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); welcomeScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); welcomeScroller->setWidget(welcomePage); welcomeScroller->setWidgetResizable(true); widgetStack->addWidget(welcomeScroller); widgetStack->addWidget(mdiArea); mdiArea->setTabsMovable(true); mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); } ~Private() { qDeleteAll(toolbarList); } KisMainWindow *q {0}; QUuid id; KisViewManager *viewManager {0}; QPointer activeView; QList toolbarList; bool firstTime {true}; bool windowSizeDirty {false}; bool readOnly {false}; KisAction *showDocumentInfo {0}; KisAction *saveAction {0}; KisAction *saveActionAs {0}; // KisAction *printAction; // KisAction *printActionPreview; // KisAction *exportPdf {0}; KisAction *importAnimation {0}; KisAction *closeAll {0}; // KisAction *reloadFile; KisAction *importFile {0}; KisAction *exportFile {0}; KisAction *undo {0}; KisAction *redo {0}; KisAction *newWindow {0}; KisAction *close {0}; KisAction *mdiCascade {0}; KisAction *mdiTile {0}; KisAction *mdiNextWindow {0}; KisAction *mdiPreviousWindow {0}; KisAction *toggleDockers {0}; KisAction *toggleDockerTitleBars {0}; KisAction *toggleDetachCanvas {0}; KisAction *fullScreenMode {0}; KisAction *showSessionManager {0}; KisAction *expandingSpacers[2]; KActionMenu *dockWidgetMenu; KActionMenu *windowMenu; KActionMenu *documentMenu; KActionMenu *workspaceMenu; KHelpMenu *helpMenu {0}; KRecentFilesAction *recentFiles {0}; KisResourceModel *workspacemodel {0}; QScopedPointer undoActionsUpdateManager; QString lastExportLocation; QMap dockWidgetsMap; QByteArray dockerStateBeforeHiding; KoToolDocker *toolOptionsDocker {0}; QCloseEvent *deferredClosingEvent {0}; Digikam::ThemeManager *themeManager {0}; QScrollArea *welcomeScroller {0}; KisWelcomePageWidget *welcomePage {0}; QStackedWidget *widgetStack {0}; QMdiArea *mdiArea; QMdiSubWindow *activeSubWindow {0}; KisSignalMapper *windowMapper; KisSignalMapper *documentMapper; KisCanvasWindow *canvasWindow {0}; QByteArray lastExportedFormat; QScopedPointer > tabSwitchCompressor; QMutex savingEntryMutex; KConfigGroup windowStateConfig; QUuid workspaceBorrowedBy; KisSignalAutoConnectionsStore screenConnectionsStore; #ifdef Q_OS_ANDROID KisAndroidFileManager *fileManager; #endif KisActionManager * actionManager() { return viewManager->actionManager(); } QTabBar* findTabBarHACK() { QObjectList objects = mdiArea->children(); Q_FOREACH (QObject *object, objects) { QTabBar *bar = qobject_cast(object); if (bar) { return bar; } } return 0; } }; KisMainWindow::KisMainWindow(QUuid uuid) : KXmlGuiWindow() , d(new Private(this, uuid)) { d->workspacemodel = KisResourceModelProvider::resourceModel(ResourceType::Workspaces); connect(d->workspacemodel, SIGNAL(afterResourcesLayoutReset()), this, SLOT(updateWindowMenu())); d->viewManager = new KisViewManager(this, actionCollection()); KConfigGroup group( KSharedConfig::openConfig(), "theme"); d->themeManager = new Digikam::ThemeManager(group.readEntry("Theme", "Krita dark"), this); d->windowStateConfig = KSharedConfig::openConfig()->group("MainWindow"); setStandardToolBarMenuEnabled(true); setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North); setDockNestingEnabled(true); qApp->setStartDragDistance(25); // 25 px is a distance that works well for Tablet and Mouse events #ifdef Q_OS_MACOS setUnifiedTitleAndToolBarOnMac(true); #endif connect(this, SIGNAL(restoringDone()), this, SLOT(forceDockTabFonts())); connect(this, SIGNAL(themeChanged()), d->viewManager, SLOT(updateIcons())); connect(KisPart::instance(), SIGNAL(documentClosed(QString)), SLOT(updateWindowMenu())); connect(KisPart::instance(), SIGNAL(documentOpened(QString)), SLOT(updateWindowMenu())); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), this, SLOT(configChanged())); actionCollection()->addAssociatedWidget(this); KoPluginLoader::instance()->load("Krita/ViewPlugin", "Type == 'Service' and ([X-Krita-Version] == 28)", KoPluginLoader::PluginsConfig(), d->viewManager, false); // Load the per-application plugins (Right now, only Python) We do this only once, when the first mainwindow is being created. KoPluginLoader::instance()->load("Krita/ApplicationPlugin", "Type == 'Service' and ([X-Krita-Version] == 28)", KoPluginLoader::PluginsConfig(), qApp, true); KoToolBoxFactory toolBoxFactory; QDockWidget *toolbox = createDockWidget(&toolBoxFactory); toolbox->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); KisConfig cfg(true); if (cfg.toolOptionsInDocker()) { ToolDockerFactory toolDockerFactory; d->toolOptionsDocker = qobject_cast(createDockWidget(&toolDockerFactory)); d->toolOptionsDocker->toggleViewAction()->setEnabled(true); } QMap dockwidgetActions; dockwidgetActions[toolbox->toggleViewAction()->text()] = toolbox->toggleViewAction(); Q_FOREACH (const QString & docker, KoDockRegistry::instance()->keys()) { KoDockFactoryBase *factory = KoDockRegistry::instance()->value(docker); QDockWidget *dw = createDockWidget(factory); dockwidgetActions[dw->toggleViewAction()->text()] = dw->toggleViewAction(); } if (d->toolOptionsDocker) { dockwidgetActions[d->toolOptionsDocker->toggleViewAction()->text()] = d->toolOptionsDocker->toggleViewAction(); } connect(KoToolManager::instance(), SIGNAL(toolOptionWidgetsChanged(KoCanvasController*,QList >)), this, SLOT(newOptionWidgets(KoCanvasController*,QList >))); Q_FOREACH (QString title, dockwidgetActions.keys()) { d->dockWidgetMenu->addAction(dockwidgetActions[title]); } Q_FOREACH (QDockWidget *wdg, dockWidgets()) { if ((wdg->features() & QDockWidget::DockWidgetClosable) == 0) { wdg->setVisible(true); } } Q_FOREACH (KoCanvasObserverBase* observer, canvasObservers()) { observer->setObservedCanvas(0); KisMainwindowObserver* mainwindowObserver = dynamic_cast(observer); if (mainwindowObserver) { mainwindowObserver->setViewManager(d->viewManager); } } // Load all the actions from the tool plugins Q_FOREACH(KoToolFactoryBase *toolFactory, KoToolRegistry::instance()->values()) { toolFactory->createActions(actionCollection()); } d->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setTabPosition(QTabWidget::North); d->mdiArea->setTabsClosable(true); // Tab close button override // Windows just has a black X, and Ubuntu has a dark x that is hard to read // just switch this icon out for all OSs so it is easier to see d->mdiArea->setStyleSheet("QTabBar::close-button { image: url(:/pics/broken-preset.png) }"); setCentralWidget(d->widgetStack); d->widgetStack->setCurrentIndex(0); connect(d->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(subWindowActivated())); connect(d->windowMapper, SIGNAL(mapped(QWidget*)), this, SLOT(setActiveSubWindow(QWidget*))); connect(d->documentMapper, SIGNAL(mapped(QObject*)), this, SLOT(newView(QObject*))); d->canvasWindow = new KisCanvasWindow(this); actionCollection()->addAssociatedWidget(d->canvasWindow); createActions(); // the welcome screen needs to grab actions...so make sure this line goes after the createAction() so they exist d->welcomePage->setMainWindow(this); setAutoSaveSettings(d->windowStateConfig, false); subWindowActivated(); updateWindowMenu(); if (isHelpMenuEnabled() && !d->helpMenu) { // workaround for KHelpMenu (or rather KAboutData::applicationData()) internally // not using the Q*Application metadata ATM, which results e.g. in the bugreport wizard // not having the app version preset // fixed hopefully in KF5 5.22.0, patch pending QGuiApplication *app = qApp; KAboutData aboutData(app->applicationName(), app->applicationDisplayName(), app->applicationVersion()); aboutData.setOrganizationDomain(app->organizationDomain().toUtf8()); d->helpMenu = new KHelpMenu(this, aboutData, false); // workaround-less version: // d->helpMenu = new KHelpMenu(this, QString()/*unused*/, false); // The difference between using KActionCollection->addAction() is that // these actions do not get tied to the MainWindow. What does this all do? KActionCollection *actions = d->viewManager->actionCollection(); QAction *helpContentsAction = d->helpMenu->action(KHelpMenu::menuHelpContents); QAction *whatsThisAction = d->helpMenu->action(KHelpMenu::menuWhatsThis); QAction *reportBugAction = d->helpMenu->action(KHelpMenu::menuReportBug); QAction *switchLanguageAction = d->helpMenu->action(KHelpMenu::menuSwitchLanguage); QAction *aboutAppAction = d->helpMenu->action(KHelpMenu::menuAboutApp); QAction *aboutKdeAction = d->helpMenu->action(KHelpMenu::menuAboutKDE); if (helpContentsAction) { actions->addAction(helpContentsAction->objectName(), helpContentsAction); } if (whatsThisAction) { actions->addAction(whatsThisAction->objectName(), whatsThisAction); } if (reportBugAction) { actions->addAction(reportBugAction->objectName(), reportBugAction); } if (switchLanguageAction) { actions->addAction(switchLanguageAction->objectName(), switchLanguageAction); } if (aboutAppAction) { actions->addAction(aboutAppAction->objectName(), aboutAppAction); } if (aboutKdeAction) { actions->addAction(aboutKdeAction->objectName(), aboutKdeAction); } connect(d->helpMenu, SIGNAL(showAboutApplication()), SLOT(showAboutApplication())); } // KDE' libs 4''s help contents action is broken outside kde, for some reason... We can handle it just as easily ourselves QAction *helpAction = actionCollection()->action("help_contents"); helpAction->disconnect(); connect(helpAction, SIGNAL(triggered()), this, SLOT(showManual())); #if 0 //check for colliding shortcuts QSet existingShortcuts; Q_FOREACH (QAction* action, actionCollection()->actions()) { if(action->shortcut() == QKeySequence(0)) { continue; } dbgKrita << "shortcut " << action->text() << " " << action->shortcut(); Q_ASSERT(!existingShortcuts.contains(action->shortcut())); existingShortcuts.insert(action->shortcut()); } #endif configChanged(); // If we have customized the toolbars, load that first setLocalXMLFile(KoResourcePaths::locateLocal("data", "krita4.xmlgui")); setXMLFile(":/kxmlgui5/krita4.xmlgui"); guiFactory()->addClient(this); connect(guiFactory(), SIGNAL(makingChanges(bool)), SLOT(slotXmlGuiMakingChanges(bool))); // Create and plug toolbar list for Settings menu QList toolbarList; Q_FOREACH (QWidget* it, guiFactory()->containers("ToolBar")) { KToolBar * toolBar = ::qobject_cast(it); toolBar->setMovable(KisConfig(true).readEntry("LockAllDockerPanels", false)); if (toolBar) { if (toolBar->objectName() == "BrushesAndStuff") { toolBar->setEnabled(false); } KToggleAction* act = new KToggleAction(i18n("Show %1 Toolbar", toolBar->windowTitle()), this); actionCollection()->addAction(toolBar->objectName().toUtf8(), act); act->setCheckedState(KGuiItem(i18n("Hide %1 Toolbar", toolBar->windowTitle()))); connect(act, SIGNAL(toggled(bool)), this, SLOT(slotToolbarToggled(bool))); act->setChecked(!toolBar->isHidden()); toolbarList.append(act); } else { warnUI << "Toolbar list contains a " << it->metaObject()->className() << " which is not a toolbar!"; } } KToolBar::setToolBarsLocked(KisConfig(true).readEntry("LockAllDockerPanels", false)); plugActionList("toolbarlist", toolbarList); d->toolbarList = toolbarList; applyToolBarLayout(); d->viewManager->updateGUI(); d->viewManager->updateIcons(); QTimer::singleShot(1000, this, SLOT(checkSanity())); { using namespace std::placeholders; // For _1 placeholder std::function callback( std::bind(&KisMainWindow::switchTab, this, _1)); d->tabSwitchCompressor.reset( new KisSignalCompressorWithParam(500, callback, KisSignalCompressor::FIRST_INACTIVE)); } if (cfg.readEntry("CanvasOnlyActive", false)) { QString currentWorkspace = cfg.readEntry("CurrentWorkspace", "Default"); KoResourceServer * rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResourceSP workspace = rserver->resourceByName(currentWorkspace); if (workspace) { restoreWorkspace(workspace->resourceId()); } cfg.writeEntry("CanvasOnlyActive", false); menuBar()->setVisible(true); } this->winId(); // Ensures the native window has been created. QWindow *window = this->windowHandle(); connect(window, SIGNAL(screenChanged(QScreen *)), this, SLOT(windowScreenChanged(QScreen *))); } KisMainWindow::~KisMainWindow() { // Q_FOREACH (QAction *ac, actionCollection()->actions()) { // QAction *action = qobject_cast(ac); // if (action) { // qDebug() << "", "").replace("", "") // << "\n\ticonText=" << action->iconText().replace("&", "&") // << "\n\tshortcut=" << action->shortcut().toString() // << "\n\tisCheckable=" << QString((action->isChecked() ? "true" : "false")) // << "\n\tstatusTip=" << action->statusTip() // << "\n/>\n" ; // } // else { // dbgKrita << "Got a non-qaction:" << ac->objectName(); // } // } // The doc and view might still exist (this is the case when closing the window) KisPart::instance()->removeMainWindow(this); delete d->viewManager; delete d; } QUuid KisMainWindow::id() const { return d->id; } void KisMainWindow::addView(KisView *view, QMdiSubWindow *subWindow) { if (d->activeView == view && !subWindow) return; if (d->activeView) { d->activeView->disconnect(this); } // register the newly created view in the input manager viewManager()->inputManager()->addTrackedCanvas(view->canvasBase()); showView(view, subWindow); updateCaption(); emit restoringDone(); if (d->activeView) { connect(d->activeView, SIGNAL(titleModified(QString,bool)), SLOT(slotDocumentTitleModified())); connect(d->viewManager->statusBar(), SIGNAL(memoryStatusUpdated()), this, SLOT(updateCaption())); } } void KisMainWindow::notifyChildViewDestroyed(KisView *view) { /** * If we are the last view of the window, Qt will not activate another tab * before destroying tab/window. In this case we should clear all the dangling * pointers manually by setting the current view to null */ viewManager()->inputManager()->removeTrackedCanvas(view->canvasBase()); if (view->canvasBase() == viewManager()->canvasBase()) { viewManager()->setCurrentView(0); } } void KisMainWindow::showView(KisView *imageView, QMdiSubWindow *subwin) { if (imageView && activeView() != imageView) { // XXX: find a better way to initialize this! imageView->setViewManager(d->viewManager); imageView->canvasBase()->setFavoriteResourceManager(d->viewManager->paintOpBox()->favoriteResourcesManager()); imageView->slotLoadingFinished(); if (!subwin) { subwin = d->mdiArea->addSubWindow(imageView); } else { subwin->setWidget(imageView); } imageView->setSubWindow(subwin); subwin->setAttribute(Qt::WA_DeleteOnClose, true); connect(subwin, SIGNAL(destroyed()), SLOT(updateWindowMenu())); KisConfig cfg(true); subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setWindowIcon(qApp->windowIcon()); #ifdef Q_OS_MACOS connect(subwin, SIGNAL(destroyed()), SLOT(updateSubwindowFlags())); updateSubwindowFlags(); #endif if (d->mdiArea->subWindowList().size() == 1) { imageView->showMaximized(); } else { imageView->show(); } /** * Hack alert! * * Here we explicitly request KoToolManager to emit all the tool * activation signals, to reinitialize the tool options docker. * * That is needed due to a design flaw we have in the * initialization procedure. The tool in the KoToolManager is * initialized in KisView::setViewManager() calls, which * happens early enough. During this call the tool manager * requests KoCanvasControllerWidget to emit the signal to * update the widgets in the tool docker. *But* at that moment * of time the view is not yet connected to the main window, * because it happens in KisViewManager::setCurrentView a bit * later. This fact makes the widgets updating signals be lost * and never reach the tool docker. * * So here we just explicitly call the tool activation stub. */ KoToolManager::instance()->initializeCurrentToolForCanvas(); // No, no, no: do not try to call this _before_ the show() has // been called on the view; only when that has happened is the // opengl context active, and very bad things happen if we tell // the dockers to update themselves with a view if the opengl // context is not active. setActiveView(imageView); updateWindowMenu(); updateCaption(); } } void KisMainWindow::slotPreferences() { QScopedPointer dlgPreferences(new KisDlgPreferences(this)); if (dlgPreferences->editPreferences()) { KisConfigNotifier::instance()->notifyConfigChanged(); KisConfigNotifier::instance()->notifyPixelGridModeChanged(); KisImageConfigNotifier::instance()->notifyConfigChanged(); // XXX: should this be changed for the views in other windows as well? Q_FOREACH (QPointer koview, KisPart::instance()->views()) { KisViewManager *view = qobject_cast(koview); if (view) { // Update the settings for all nodes -- they don't query // KisConfig directly because they need the settings during // compositing, and they don't connect to the config notifier // because nodes are not QObjects (because only one base class // can be a QObject). KisNode* node = dynamic_cast(view->image()->rootLayer().data()); node->updateSettings(); } } updateWindowMenu(); d->viewManager->showHideScrollbars(); } } void KisMainWindow::slotThemeChanged() { // save theme changes instantly KConfigGroup group( KSharedConfig::openConfig(), "theme"); group.writeEntry("Theme", d->themeManager->currentThemeName()); // reload action icons! Q_FOREACH (QAction *action, actionCollection()->actions()) { KisIconUtils::updateIcon(action); } if (d->mdiArea) { d->mdiArea->setPalette(qApp->palette()); for (int i=0; imdiArea->subWindowList().size(); i++) { QMdiSubWindow *window = d->mdiArea->subWindowList().at(i); if (window) { window->setPalette(qApp->palette()); KisView *view = qobject_cast(window->widget()); if (view) { view->slotThemeChanged(qApp->palette()); } } } } emit themeChanged(); } bool KisMainWindow::canvasDetached() const { return centralWidget() != d->widgetStack; } void KisMainWindow::setCanvasDetached(bool detach) { if (detach == canvasDetached()) return; QWidget *outgoingWidget = centralWidget() ? takeCentralWidget() : nullptr; QWidget *incomingWidget = d->canvasWindow->swapMainWidget(outgoingWidget); if (incomingWidget) { setCentralWidget(incomingWidget); } if (detach) { d->canvasWindow->show(); } else { d->canvasWindow->hide(); } } void KisMainWindow::slotFileSelected(QString path) { QString url = path; if (!url.isEmpty()) { bool res = openDocument(QUrl::fromLocalFile(url), Import); if (!res) { warnKrita << "Loading" << url << "failed"; } } } void KisMainWindow::slotEmptyFilePath() { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The chosen file's location could not be found. Does it exist?")); } QWidget * KisMainWindow::canvasWindow() const { return d->canvasWindow; } void KisMainWindow::updateReloadFileAction(KisDocument *doc) { Q_UNUSED(doc); // d->reloadFile->setEnabled(doc && !doc->url().isEmpty()); } void KisMainWindow::setReadWrite(bool readwrite) { d->saveAction->setEnabled(readwrite); d->importFile->setEnabled(readwrite); d->readOnly = !readwrite; updateCaption(); } -void KisMainWindow::addRecentURL(const QUrl &url) +void KisMainWindow::addRecentURL(const QUrl &url, const QUrl &oldUrl) { // Add entry to recent documents list // (call coming from KisDocument because it must work with cmd line, template dlg, file/open, etc.) if (!url.isEmpty()) { bool ok = true; if (url.isLocalFile()) { QString path = url.adjusted(QUrl::StripTrailingSlash).toLocalFile(); const QStringList tmpDirs = KoResourcePaths::resourceDirs("tmp"); for (QStringList::ConstIterator it = tmpDirs.begin() ; ok && it != tmpDirs.end() ; ++it) { if (path.contains(*it)) { ok = false; // it's in the tmp resource } } const QStringList templateDirs = KoResourcePaths::findDirs("templates"); for (QStringList::ConstIterator it = templateDirs.begin() ; ok && it != templateDirs.end() ; ++it) { if (path.contains(*it)) { ok = false; // it's in the templates directory. break; } } } if (ok) { + if (!oldUrl.isEmpty()) { + d->recentFiles->removeUrl(oldUrl); + } d->recentFiles->addUrl(url); } saveRecentFiles(); } } void KisMainWindow::saveRecentFiles() { // Save list of recent files KSharedConfigPtr config = KSharedConfig::openConfig(); d->recentFiles->saveEntries(config->group("RecentFiles")); config->sync(); // Tell all windows to reload their list, after saving // Doesn't work multi-process, but it's a start Q_FOREACH (KisMainWindow *mw, KisPart::instance()->mainWindows()) { if (mw != this) { mw->reloadRecentFileList(); } } } QList KisMainWindow::recentFilesUrls() { return d->recentFiles->urls(); } void KisMainWindow::clearRecentFiles() { d->recentFiles->clear(); d->welcomePage->populateRecentDocuments(); } void KisMainWindow::removeRecentUrl(const QUrl &url) { d->recentFiles->removeUrl(url); KSharedConfigPtr config = KSharedConfig::openConfig(); d->recentFiles->saveEntries(config->group("RecentFiles")); config->sync(); } void KisMainWindow::reloadRecentFileList() { d->recentFiles->loadEntries(KSharedConfig::openConfig()->group("RecentFiles")); } void KisMainWindow::updateCaption() { if (!d->mdiArea->activeSubWindow()) { updateCaption(QString(), false); } else if (d->activeView && d->activeView->document() && d->activeView->image()){ KisDocument *doc = d->activeView->document(); QString caption(doc->caption()); caption = "RESOURCES REWRITE GOING ON " + caption; if (d->readOnly) { caption += " [" + i18n("Write Protected") + "] "; } if (doc->isRecovered()) { caption += " [" + i18n("Recovered") + "] "; } // show the file size for the document KisMemoryStatisticsServer::Statistics m_fileSizeStats = KisMemoryStatisticsServer::instance()->fetchMemoryStatistics(d->activeView ? d->activeView->image() : 0); if (m_fileSizeStats.imageSize) { caption += QString(" (").append( KFormat().formatByteSize(m_fileSizeStats.imageSize)).append( ")"); } updateCaption(caption, doc->isModified()); if (!doc->url().fileName().isEmpty()) { d->saveAction->setToolTip(i18n("Save as %1", doc->url().fileName())); } else { d->saveAction->setToolTip(i18n("Save")); } } } void KisMainWindow::updateCaption(const QString &caption, bool modified) { QString versionString = KritaVersionWrapper::versionString(true); QString title = caption; if (!title.contains(QStringLiteral("[*]"))) { // append the placeholder so that the modified mechanism works title.append(QStringLiteral(" [*]")); } if (d->mdiArea->activeSubWindow()) { #if defined(KRITA_ALPHA) || defined (KRITA_BETA) || defined (KRITA_RC) d->mdiArea->activeSubWindow()->setWindowTitle(QString("%1: %2").arg(versionString).arg(title)); #else d->mdiArea->activeSubWindow()->setWindowTitle(title); #endif d->mdiArea->activeSubWindow()->setWindowModified(modified); } else { #if defined(KRITA_ALPHA) || defined (KRITA_BETA) || defined (KRITA_RC) setWindowTitle(QString("%1: %2").arg(versionString).arg(title)); #else setWindowTitle(title); #endif } setWindowModified(modified); } KisView *KisMainWindow::activeView() const { if (d->activeView) { return d->activeView; } return 0; } bool KisMainWindow::openDocument(const QUrl &url, OpenFlags flags) { if (!QFile(url.toLocalFile()).exists()) { if (!(flags & BatchMode)) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The file %1 does not exist.", url.url())); } d->recentFiles->removeUrl(url); //remove the file from the recent-opened-file-list saveRecentFiles(); return false; } return openDocumentInternal(url, flags); } bool KisMainWindow::openDocumentInternal(const QUrl &url, OpenFlags flags) { if (!url.isLocalFile()) { qWarning() << "KisMainWindow::openDocumentInternal. Not a local file:" << url; return false; } KisDocument *newdoc = KisPart::instance()->createDocument(); if (flags & BatchMode) { newdoc->setFileBatchMode(true); } d->firstTime = true; connect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); connect(newdoc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); KisDocument::OpenFlags openFlags = KisDocument::None; // XXX: Why this duplication of of OpenFlags... if (flags & RecoveryFile) { openFlags |= KisDocument::RecoveryFile; } bool openRet = !(flags & Import) ? newdoc->openUrl(url, openFlags) : newdoc->importDocument(url); if (!openRet) { delete newdoc; return false; } KisPart::instance()->addDocument(newdoc); updateReloadFileAction(newdoc); if (!QFileInfo(url.toLocalFile()).isWritable()) { setReadWrite(false); } if (flags & RecoveryFile && ( url.toLocalFile().startsWith(QDir::tempPath()) || url.toLocalFile().startsWith(QDir::homePath())) ) { newdoc->setUrl(QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + "/" + QFileInfo(url.toLocalFile()).fileName())); newdoc->save(false, 0); } return true; } void KisMainWindow::showDocument(KisDocument *document) { Q_FOREACH(QMdiSubWindow *subwindow, d->mdiArea->subWindowList()) { KisView *view = qobject_cast(subwindow->widget()); KIS_SAFE_ASSERT_RECOVER_NOOP(view); if (view) { if (view->document() == document) { setActiveSubWindow(subwindow); return; } } } addViewAndNotifyLoadingCompleted(document); } KisView* KisMainWindow::addViewAndNotifyLoadingCompleted(KisDocument *document, QMdiSubWindow *subWindow) { showWelcomeScreen(false); // see workaround in function header KisView *view = KisPart::instance()->createView(document, d->viewManager, this); addView(view, subWindow); emit guiLoadingFinished(); return view; } QStringList KisMainWindow::showOpenFileDialog(bool isImporting) { KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument"); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import)); dialog.setCaption(isImporting ? i18n("Import Images") : i18n("Open Images")); return dialog.filenames(); } // Separate from openDocument to handle async loading (remote URLs) void KisMainWindow::slotLoadCompleted() { KisDocument *newdoc = qobject_cast(sender()); if (newdoc && newdoc->image()) { addViewAndNotifyLoadingCompleted(newdoc); disconnect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); disconnect(newdoc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); emit loadCompleted(); } } void KisMainWindow::slotLoadCanceled(const QString & errMsg) { KisUsageLogger::log(QString("Loading canceled. Error:").arg(errMsg)); if (!errMsg.isEmpty()) // empty when canceled by user QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg); // ... can't delete the document, it's the one who emitted the signal... KisDocument* doc = qobject_cast(sender()); Q_ASSERT(doc); disconnect(doc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); disconnect(doc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); } void KisMainWindow::slotSaveCanceled(const QString &errMsg) { KisUsageLogger::log(QString("Saving canceled. Error:").arg(errMsg)); if (!errMsg.isEmpty()) { // empty when canceled by user QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg); } slotSaveCompleted(); } void KisMainWindow::slotSaveCompleted() { KisUsageLogger::log(QString("Saving Completed")); KisDocument* doc = qobject_cast(sender()); Q_ASSERT(doc); disconnect(doc, SIGNAL(completed()), this, SLOT(slotSaveCompleted())); disconnect(doc, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); if (d->deferredClosingEvent) { KXmlGuiWindow::closeEvent(d->deferredClosingEvent); } } bool KisMainWindow::hackIsSaving() const { StdLockableWrapper wrapper(&d->savingEntryMutex); std::unique_lock> l(wrapper, std::try_to_lock); return !l.owns_lock(); } bool KisMainWindow::installBundle(const QString &fileName) const { QFileInfo from(fileName); QFileInfo to(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/" + from.fileName()); if (to.exists()) { QFile::remove(to.canonicalFilePath()); } return QFile::copy(fileName, QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/" + from.fileName()); } QImage KisMainWindow::layoutThumbnail() { int size = 256; qreal scale = qreal(size)/qreal(qMax(geometry().width(), geometry().height())); QImage layoutThumbnail = QImage(qRound(geometry().width()*scale), qRound(geometry().height()*scale), QImage::Format_ARGB32); QPainter gc(&layoutThumbnail); gc.fillRect(0, 0, layoutThumbnail.width(), layoutThumbnail.height(), this->palette().dark()); for (int childW = 0; childW< children().size(); childW++) { if (children().at(childW)->isWidgetType()) { QWidget *w = dynamic_cast(children().at(childW)); if (w->isVisible()) { QRect wRect = QRectF(w->geometry().x()*scale , w->geometry().y()*scale , w->geometry().width()*scale , w->geometry().height()*scale ).toRect(); wRect = wRect.intersected(layoutThumbnail.rect().adjusted(-1, -1, -1, -1)); gc.setBrush(this->palette().window()); if (w == d->widgetStack) { gc.setBrush(d->mdiArea->background()); } gc.setPen(this->palette().windowText().color()); gc.drawRect(wRect); } } } gc.end(); return layoutThumbnail; } bool KisMainWindow::saveDocument(KisDocument *document, bool saveas, bool isExporting) { if (!document) { return true; } /** * Make sure that we cannot enter this method twice! * * The lower level functions may call processEvents() so * double-entry is quite possible to achieve. Here we try to lock * the mutex, and if it is failed, just cancel saving. */ StdLockableWrapper wrapper(&d->savingEntryMutex); std::unique_lock> l(wrapper, std::try_to_lock); if (!l.owns_lock()) return false; // no busy wait for saving because it is dangerous! KisDelayedSaveDialog dlg(document->image(), KisDelayedSaveDialog::SaveDialog, 0, this); dlg.blockIfImageIsBusy(); if (dlg.result() == KisDelayedSaveDialog::Rejected) { return false; } else if (dlg.result() == KisDelayedSaveDialog::Ignored) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("You are saving a file while the image is " "still rendering. The saved file may be " "incomplete or corrupted.\n\n" "Please select a location where the original " "file will not be overridden!")); saveas = true; } if (document->isRecovered()) { saveas = true; } if (document->url().isEmpty()) { saveas = true; } connect(document, SIGNAL(completed()), this, SLOT(slotSaveCompleted())); connect(document, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); QByteArray nativeFormat = document->nativeFormatMimeType(); QByteArray oldMimeFormat = document->mimeType(); QUrl suggestedURL = document->url(); QStringList mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); if (!mimeFilter.contains(oldMimeFormat)) { dbgUI << "KisMainWindow::saveDocument no export filter for" << oldMimeFormat; // --- don't setOutputMimeType in case the user cancels the Save As // dialog and then tries to just plain Save --- // suggest a different filename extension (yes, we fortunately don't all live in a world of magic :)) QString suggestedFilename = QFileInfo(suggestedURL.toLocalFile()).completeBaseName(); if (!suggestedFilename.isEmpty()) { // ".kra" looks strange for a name suggestedFilename = suggestedFilename + "." + KisMimeDatabase::suffixesForMimeType(KIS_MIME_TYPE).first(); suggestedURL = suggestedURL.adjusted(QUrl::RemoveFilename); suggestedURL.setPath(suggestedURL.path() + suggestedFilename); } // force the user to choose outputMimeType saveas = true; } bool ret = false; if (document->url().isEmpty() || isExporting || saveas) { // if you're just File/Save As'ing to change filter options you // don't want to be reminded about overwriting files etc. bool justChangingFilterOptions = false; KoFileDialog dialog(this, KoFileDialog::SaveFile, "SaveAs"); dialog.setCaption(isExporting ? i18n("Exporting") : i18n("Saving As")); //qDebug() << ">>>>>" << isExporting << d->lastExportLocation << d->lastExportedFormat << QString::fromLatin1(document->mimeType()); if (isExporting && !d->lastExportLocation.isEmpty() && !d->lastExportLocation.contains(QDir::tempPath())) { // Use the location where we last exported to, if it's set, as the opening location for the file dialog QString proposedPath = QFileInfo(d->lastExportLocation).absolutePath(); // If the document doesn't have a filename yet, use the title QString proposedFileName = suggestedURL.isEmpty() ? document->documentInfo()->aboutInfo("title") : QFileInfo(suggestedURL.toLocalFile()).completeBaseName(); // Use the last mimetype we exported to by default QString proposedMimeType = d->lastExportedFormat.isEmpty() ? "" : d->lastExportedFormat; QString proposedExtension = KisMimeDatabase::suffixesForMimeType(proposedMimeType).first().remove("*,"); // Set the default dir: this overrides the one loaded from the config file, since we're exporting and the lastExportLocation is not empty dialog.setDefaultDir(proposedPath + "/" + proposedFileName + "." + proposedExtension, true); dialog.setMimeTypeFilters(mimeFilter, proposedMimeType); } else { // Get the last used location for saving KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); QString proposedPath = group.readEntry("SaveAs", ""); // if that is empty, get the last used location for loading if (proposedPath.isEmpty()) { proposedPath = group.readEntry("OpenDocument", ""); } // If that is empty, too, use the Pictures location. if (proposedPath.isEmpty()) { proposedPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } // But only use that if the suggestedUrl, that is, the document's own url is empty, otherwise // open the location where the document currently is. dialog.setDefaultDir(suggestedURL.isEmpty() ? proposedPath : suggestedURL.toLocalFile(), true); // If exporting, default to all supported file types if user is exporting QByteArray default_mime_type = ""; if (!isExporting) { // otherwise use the document's mimetype, or if that is empty, kra, which is the savest. default_mime_type = document->mimeType().isEmpty() ? nativeFormat : document->mimeType(); } dialog.setMimeTypeFilters(mimeFilter, QString::fromLatin1(default_mime_type)); } QUrl newURL = QUrl::fromUserInput(dialog.filename()); if (newURL.isLocalFile()) { QString fn = newURL.toLocalFile(); if (QFileInfo(fn).completeSuffix().isEmpty()) { fn.append(KisMimeDatabase::suffixesForMimeType(nativeFormat).first()); newURL = QUrl::fromLocalFile(fn); } } if (document->documentInfo()->aboutInfo("title") == i18n("Unnamed")) { QString fn = newURL.toLocalFile(); QFileInfo info(fn); document->documentInfo()->setAboutInfo("title", info.completeBaseName()); } QByteArray outputFormat = nativeFormat; QString outputFormatString = KisMimeDatabase::mimeTypeForFile(newURL.toLocalFile(), false); outputFormat = outputFormatString.toLatin1(); if (!isExporting) { justChangingFilterOptions = (newURL == document->url()) && (outputFormat == document->mimeType()); } else { QString path = QFileInfo(d->lastExportLocation).absolutePath(); QString filename = QFileInfo(document->url().toLocalFile()).completeBaseName(); justChangingFilterOptions = (QFileInfo(newURL.toLocalFile()).absolutePath() == path) && (QFileInfo(newURL.toLocalFile()).completeBaseName() == filename) && (outputFormat == d->lastExportedFormat); } bool bOk = true; if (newURL.isEmpty()) { bOk = false; } if (bOk) { bool wantToSave = true; // don't change this line unless you know what you're doing :) if (!justChangingFilterOptions) { if (!document->isNativeFormat(outputFormat)) wantToSave = true; } if (wantToSave) { if (!isExporting) { // Save As ret = document->saveAs(newURL, outputFormat, true); if (ret) { dbgUI << "Successful Save As!"; KisPart::instance()->addRecentURLToAllMainWindows(newURL); setReadWrite(true); } else { dbgUI << "Failed Save As!"; } } else { // Export ret = document->exportDocument(newURL, outputFormat); if (ret) { d->lastExportLocation = newURL.toLocalFile(); d->lastExportedFormat = outputFormat; } } } // if (wantToSave) { else ret = false; } // if (bOk) { else ret = false; } else { // saving // We cannot "export" into the currently // opened document. We are not Gimp. KIS_ASSERT_RECOVER_NOOP(!isExporting); // be sure document has the correct outputMimeType! if (document->isModified()) { ret = document->save(true, 0); } if (!ret) { dbgUI << "Failed Save!"; } } updateReloadFileAction(document); updateCaption(); return ret; } void KisMainWindow::undo() { if (activeView()) { activeView()->document()->undoStack()->undo(); } } void KisMainWindow::redo() { if (activeView()) { activeView()->document()->undoStack()->redo(); } } void KisMainWindow::closeEvent(QCloseEvent *e) { if (hackIsSaving()) { e->setAccepted(false); return; } if (!KisPart::instance()->closingSession()) { QAction *action= d->viewManager->actionCollection()->action("view_show_canvas_only"); if ((action) && (action->isChecked())) { action->setChecked(false); } // Save session when last window is closed if (KisPart::instance()->mainwindowCount() == 1) { bool closeAllowed = KisPart::instance()->closeSession(); if (!closeAllowed) { e->setAccepted(false); return; } } } d->mdiArea->closeAllSubWindows(); QList childrenList = d->mdiArea->subWindowList(); if (childrenList.isEmpty()) { d->deferredClosingEvent = e; saveWindowState(true); d->canvasWindow->close(); } else { e->setAccepted(false); } } void KisMainWindow::saveWindowSettings() { KSharedConfigPtr config = KSharedConfig::openConfig(); if (d->windowSizeDirty ) { dbgUI << "KisMainWindow::saveWindowSettings"; KConfigGroup group = d->windowStateConfig; KWindowConfig::saveWindowSize(windowHandle(), group); config->sync(); d->windowSizeDirty = false; } if (!d->activeView || d->activeView->document()) { // Save toolbar position into the config file of the app, under the doc's component name KConfigGroup group = d->windowStateConfig; saveMainWindowSettings(group); // Save collapsible state of dock widgets for (QMap::const_iterator i = d->dockWidgetsMap.constBegin(); i != d->dockWidgetsMap.constEnd(); ++i) { if (i.value()->widget()) { KConfigGroup dockGroup = group.group(QString("DockWidget ") + i.key()); dockGroup.writeEntry("Collapsed", i.value()->widget()->isHidden()); dockGroup.writeEntry("Locked", i.value()->property("Locked").toBool()); dockGroup.writeEntry("DockArea", (int) dockWidgetArea(i.value())); dockGroup.writeEntry("xPosition", (int) i.value()->widget()->x()); dockGroup.writeEntry("yPosition", (int) i.value()->widget()->y()); dockGroup.writeEntry("width", (int) i.value()->widget()->width()); dockGroup.writeEntry("height", (int) i.value()->widget()->height()); } } } KSharedConfig::openConfig()->sync(); resetAutoSaveSettings(); // Don't let KMainWindow override the good stuff we wrote down } void KisMainWindow::resizeEvent(QResizeEvent * e) { d->windowSizeDirty = true; KXmlGuiWindow::resizeEvent(e); } void KisMainWindow::setActiveView(KisView* view) { d->activeView = view; updateCaption(); if (d->undoActionsUpdateManager) { d->undoActionsUpdateManager->setCurrentDocument(view ? view->document() : 0); } d->viewManager->setCurrentView(view); KisWindowLayoutManager::instance()->activeDocumentChanged(view->document()); } void KisMainWindow::dragMove(QDragMoveEvent * event) { QTabBar *tabBar = d->findTabBarHACK(); if (!tabBar && d->mdiArea->viewMode() == QMdiArea::TabbedView) { qWarning() << "WARNING!!! Cannot find QTabBar in the main window! Looks like Qt has changed behavior. Drag & Drop between multiple tabs might not work properly (tabs will not switch automatically)!"; } if (tabBar && tabBar->isVisible()) { QPoint pos = tabBar->mapFromGlobal(mapToGlobal(event->pos())); if (tabBar->rect().contains(pos)) { const int tabIndex = tabBar->tabAt(pos); if (tabIndex >= 0 && tabBar->currentIndex() != tabIndex) { d->tabSwitchCompressor->start(tabIndex); } } else if (d->tabSwitchCompressor->isActive()) { d->tabSwitchCompressor->stop(); } } } void KisMainWindow::dragLeave() { if (d->tabSwitchCompressor->isActive()) { d->tabSwitchCompressor->stop(); } } void KisMainWindow::switchTab(int index) { QTabBar *tabBar = d->findTabBarHACK(); if (!tabBar) return; tabBar->setCurrentIndex(index); } void KisMainWindow::showWelcomeScreen(bool show) { d->widgetStack->setCurrentIndex(!show); } void KisMainWindow::slotFileNew() { const QStringList mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import); KisOpenPane *startupWidget = new KisOpenPane(this, mimeFilter, QStringLiteral("templates/")); startupWidget->setWindowModality(Qt::WindowModal); startupWidget->setWindowTitle(i18n("Create new document")); KisConfig cfg(true); int w = cfg.defImageWidth(); int h = cfg.defImageHeight(); const double resolution = cfg.defImageResolution(); const QString colorModel = cfg.defColorModel(); const QString colorDepth = cfg.defaultColorDepth(); const QString colorProfile = cfg.defColorProfile(); CustomDocumentWidgetItem item; item.widget = new KisCustomImageWidget(startupWidget, w, h, resolution, colorModel, colorDepth, colorProfile, i18n("Unnamed")); item.icon = "document-new"; item.title = i18n("Custom Document"); startupWidget->addCustomDocumentWidget(item.widget, item.title, "Custom Document", item.icon); QSize sz = KisClipboard::instance()->clipSize(); if (sz.isValid() && sz.width() != 0 && sz.height() != 0) { w = sz.width(); h = sz.height(); } item.widget = new KisImageFromClipboard(startupWidget, w, h, resolution, colorModel, colorDepth, colorProfile, i18n("Unnamed")); item.title = i18n("Create from Clipboard"); item.icon = "tab-new"; startupWidget->addCustomDocumentWidget(item.widget, item.title, "Create from ClipBoard", item.icon); // calls deleteLater connect(startupWidget, SIGNAL(documentSelected(KisDocument*)), KisPart::instance(), SLOT(startCustomDocument(KisDocument*))); // calls deleteLater connect(startupWidget, SIGNAL(openTemplate(QUrl)), KisPart::instance(), SLOT(openTemplate(QUrl))); startupWidget->exec(); // Cancel calls deleteLater... } void KisMainWindow::slotImportFile() { dbgUI << "slotImportFile()"; slotFileOpen(true); } void KisMainWindow::slotFileOpen(bool isImporting) { #ifndef Q_OS_ANDROID QStringList urls = showOpenFileDialog(isImporting); if (urls.isEmpty()) return; Q_FOREACH (const QString& url, urls) { if (!url.isEmpty()) { OpenFlags flags = isImporting ? Import : None; bool res = openDocument(QUrl::fromLocalFile(url), flags); if (!res) { warnKrita << "Loading" << url << "failed"; } } } #else Q_UNUSED(isImporting) d->fileManager->openImportFile(); connect(d->fileManager, SIGNAL(sigFileSelected(QString)), this, SLOT(slotFileSelected(QString))); connect(d->fileManager, SIGNAL(sigEmptyFilePath()), this, SLOT(slotEmptyFilePath())); #endif } void KisMainWindow::slotFileOpenRecent(const QUrl &url) { (void) openDocument(QUrl::fromLocalFile(url.toLocalFile()), None); } void KisMainWindow::slotFileSave() { if (saveDocument(d->activeView->document(), false, false)) { emit documentSaved(); } } void KisMainWindow::slotFileSaveAs() { if (saveDocument(d->activeView->document(), true, false)) { emit documentSaved(); } } void KisMainWindow::slotExportFile() { if (saveDocument(d->activeView->document(), true, true)) { emit documentSaved(); } } void KisMainWindow::slotShowSessionManager() { KisPart::instance()->showSessionManager(); } KoCanvasResourceProvider *KisMainWindow::resourceManager() const { return d->viewManager->canvasResourceProvider()->resourceManager(); } int KisMainWindow::viewCount() const { return d->mdiArea->subWindowList().size(); } const KConfigGroup &KisMainWindow::windowStateConfig() const { return d->windowStateConfig; } void KisMainWindow::saveWindowState(bool restoreNormalState) { if (restoreNormalState) { QAction *showCanvasOnly = d->viewManager->actionCollection()->action("view_show_canvas_only"); if (showCanvasOnly && showCanvasOnly->isChecked()) { showCanvasOnly->setChecked(false); } d->windowStateConfig.writeEntry("ko_geometry", saveGeometry().toBase64()); d->windowStateConfig.writeEntry("State", saveState().toBase64()); if (!d->dockerStateBeforeHiding.isEmpty()) { restoreState(d->dockerStateBeforeHiding); } statusBar()->setVisible(true); menuBar()->setVisible(true); saveWindowSettings(); } else { saveMainWindowSettings(d->windowStateConfig); } } bool KisMainWindow::restoreWorkspaceState(const QByteArray &state) { QByteArray oldState = saveState(); // needed because otherwise the layout isn't correctly restored in some situations Q_FOREACH (QDockWidget *dock, dockWidgets()) { dock->toggleViewAction()->setEnabled(true); dock->hide(); } bool success = KXmlGuiWindow::restoreState(state); if (!success) { KXmlGuiWindow::restoreState(oldState); return false; } return success; } bool KisMainWindow::restoreWorkspace(int workspaceId) { KisWorkspaceResourceSP workspace = KisResourceModelProvider::resourceModel(ResourceType::Workspaces) ->resourceForId(workspaceId).dynamicCast(); bool success = restoreWorkspaceState(workspace->dockerState()); if (activeKisView()) { activeKisView()->resourceProvider()->notifyLoadingWorkspace(workspace); } return success; } QByteArray KisMainWindow::borrowWorkspace(KisMainWindow *other) { QByteArray currentWorkspace = saveState(); if (!d->workspaceBorrowedBy.isNull()) { if (other->id() == d->workspaceBorrowedBy) { // We're swapping our original workspace back d->workspaceBorrowedBy = QUuid(); return currentWorkspace; } else { // Get our original workspace back before swapping with a third window KisMainWindow *borrower = KisPart::instance()->windowById(d->workspaceBorrowedBy); if (borrower) { QByteArray originalLayout = borrower->borrowWorkspace(this); borrower->restoreWorkspaceState(currentWorkspace); d->workspaceBorrowedBy = other->id(); return originalLayout; } } } d->workspaceBorrowedBy = other->id(); return currentWorkspace; } void KisMainWindow::swapWorkspaces(KisMainWindow *a, KisMainWindow *b) { QByteArray workspaceA = a->borrowWorkspace(b); QByteArray workspaceB = b->borrowWorkspace(a); a->restoreWorkspaceState(workspaceB); b->restoreWorkspaceState(workspaceA); } KisViewManager *KisMainWindow::viewManager() const { return d->viewManager; } void KisMainWindow::slotDocumentInfo() { if (!d->activeView->document()) return; KoDocumentInfo *docInfo = d->activeView->document()->documentInfo(); if (!docInfo) return; KoDocumentInfoDlg *dlg = d->activeView->document()->createDocumentInfoDialog(this, docInfo); if (dlg->exec()) { if (dlg->isDocumentSaved()) { d->activeView->document()->setModified(false); } else { d->activeView->document()->setModified(true); } d->activeView->document()->setTitleModified(); } delete dlg; } bool KisMainWindow::slotFileCloseAll() { Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { if (subwin) { if(!subwin->close()) return false; } } updateCaption(); return true; } void KisMainWindow::slotFileQuit() { // Do not close while KisMainWindow has the savingEntryMutex locked, bug409395. // After the background saving job is initiated, KisDocument blocks closing // while it saves itself. if (hackIsSaving()) { return; } KisPart::instance()->closeSession(); } void KisMainWindow::slotFilePrint() { if (!activeView()) return; KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return; applyDefaultSettings(printJob->printer()); QPrintDialog *printDialog = activeView()->createPrintDialog( printJob, this ); if (printDialog && printDialog->exec() == QDialog::Accepted) { printJob->printer().setPageMargins(0.0, 0.0, 0.0, 0.0, QPrinter::Point); printJob->printer().setPaperSize(QSizeF(activeView()->image()->width() / (72.0 * activeView()->image()->xRes()), activeView()->image()->height()/ (72.0 * activeView()->image()->yRes())), QPrinter::Inch); printJob->startPrinting(KisPrintJob::DeleteWhenDone); } else { delete printJob; } delete printDialog; } void KisMainWindow::slotFilePrintPreview() { if (!activeView()) return; KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return; /* Sets the startPrinting() slot to be blocking. The Qt print-preview dialog requires the printing to be completely blocking and only return when the full document has been printed. By default the KisPrintingDialog is non-blocking and multithreading, setting blocking to true will allow it to be used in the preview dialog */ printJob->setProperty("blocking", true); QPrintPreviewDialog *preview = new QPrintPreviewDialog(&printJob->printer(), this); printJob->setParent(preview); // will take care of deleting the job connect(preview, SIGNAL(paintRequested(QPrinter*)), printJob, SLOT(startPrinting())); preview->exec(); delete preview; } KisPrintJob* KisMainWindow::exportToPdf(QString pdfFileName) { if (!activeView()) return 0; if (!activeView()->document()) return 0; KoPageLayout pageLayout; pageLayout.width = 0; pageLayout.height = 0; pageLayout.topMargin = 0; pageLayout.bottomMargin = 0; pageLayout.leftMargin = 0; pageLayout.rightMargin = 0; if (pdfFileName.isEmpty()) { KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); QString defaultDir = group.readEntry("SavePdfDialog"); if (defaultDir.isEmpty()) defaultDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); QUrl startUrl = QUrl::fromLocalFile(defaultDir); KisDocument* pDoc = d->activeView->document(); /** if document has a file name, take file name and replace extension with .pdf */ if (pDoc && pDoc->url().isValid()) { startUrl = pDoc->url(); QString fileName = startUrl.toLocalFile(); fileName = fileName.replace( QRegExp( "\\.\\w{2,5}$", Qt::CaseInsensitive ), ".pdf" ); startUrl = startUrl.adjusted(QUrl::RemoveFilename); startUrl.setPath(startUrl.path() + fileName ); } QPointer layoutDlg(new KoPageLayoutDialog(this, pageLayout)); layoutDlg->setWindowModality(Qt::WindowModal); if (layoutDlg->exec() != QDialog::Accepted || !layoutDlg) { delete layoutDlg; return 0; } pageLayout = layoutDlg->pageLayout(); delete layoutDlg; KoFileDialog dialog(this, KoFileDialog::SaveFile, "OpenDocument"); dialog.setCaption(i18n("Export as PDF")); dialog.setDefaultDir(startUrl.toLocalFile()); dialog.setMimeTypeFilters(QStringList() << "application/pdf"); QUrl url = QUrl::fromUserInput(dialog.filename()); pdfFileName = url.toLocalFile(); if (pdfFileName.isEmpty()) return 0; } KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return 0; if (isHidden()) { printJob->setProperty("noprogressdialog", true); } applyDefaultSettings(printJob->printer()); // TODO for remote files we have to first save locally and then upload. printJob->printer().setOutputFileName(pdfFileName); printJob->printer().setDocName(pdfFileName); printJob->printer().setColorMode(QPrinter::Color); if (pageLayout.format == KoPageFormat::CustomSize) { printJob->printer().setPaperSize(QSizeF(pageLayout.width, pageLayout.height), QPrinter::Millimeter); } else { printJob->printer().setPaperSize(KoPageFormat::printerPageSize(pageLayout.format)); } printJob->printer().setPageMargins(pageLayout.leftMargin, pageLayout.topMargin, pageLayout.rightMargin, pageLayout.bottomMargin, QPrinter::Millimeter); switch (pageLayout.orientation) { case KoPageFormat::Portrait: printJob->printer().setOrientation(QPrinter::Portrait); break; case KoPageFormat::Landscape: printJob->printer().setOrientation(QPrinter::Landscape); break; } //before printing check if the printer can handle printing if (!printJob->canPrint()) { QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Cannot export to the specified file")); } printJob->startPrinting(KisPrintJob::DeleteWhenDone); return printJob; } void KisMainWindow::importAnimation() { if (!activeView()) return; KisDocument *document = activeView()->document(); if (!document) return; KisDlgImportImageSequence dlg(this, document); if (dlg.exec() == QDialog::Accepted) { QStringList files = dlg.files(); int firstFrame = dlg.firstFrame(); int step = dlg.step(); KoUpdaterPtr updater = !document->fileBatchMode() ? viewManager()->createUnthreadedUpdater(i18n("Import frames")) : 0; KisAnimationImporter importer(document->image(), updater); KisImportExportErrorCode status = importer.import(files, firstFrame, step); if (!status.isOk() && !status.isInternalError()) { QString msg = status.errorMessage(); if (!msg.isEmpty()) QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not finish import animation:\n%1", msg)); } activeView()->canvasBase()->refetchDataFromImage(); } } void KisMainWindow::slotConfigureToolbars() { saveWindowState(); KEditToolBar edit(factory(), this); connect(&edit, SIGNAL(newToolBarConfig()), this, SLOT(slotNewToolbarConfig())); (void) edit.exec(); applyToolBarLayout(); } void KisMainWindow::slotNewToolbarConfig() { applyMainWindowSettings(d->windowStateConfig); KXMLGUIFactory *factory = guiFactory(); Q_UNUSED(factory); // Check if there's an active view if (!d->activeView) return; plugActionList("toolbarlist", d->toolbarList); applyToolBarLayout(); } void KisMainWindow::slotToolbarToggled(bool toggle) { //dbgUI <<"KisMainWindow::slotToolbarToggled" << sender()->name() <<" toggle=" << true; // The action (sender) and the toolbar have the same name KToolBar * bar = toolBar(sender()->objectName()); if (bar) { if (toggle) { bar->show(); } else { bar->hide(); } if (d->activeView && d->activeView->document()) { saveWindowState(); } } else warnUI << "slotToolbarToggled : Toolbar " << sender()->objectName() << " not found!"; } void KisMainWindow::viewFullscreen(bool fullScreen) { KisConfig cfg(false); cfg.setFullscreenMode(fullScreen); if (fullScreen) { setWindowState(windowState() | Qt::WindowFullScreen); // set } else { setWindowState(windowState() & ~Qt::WindowFullScreen); // reset } d->fullScreenMode->setChecked(isFullScreen()); } void KisMainWindow::setMaxRecentItems(uint _number) { d->recentFiles->setMaxItems(_number); } void KisMainWindow::slotReloadFile() { KisDocument* document = d->activeView->document(); if (!document || document->url().isEmpty()) return; if (document->isModified()) { bool ok = QMessageBox::question(this, i18nc("@title:window", "Krita"), i18n("You will lose all changes made since your last save\n" "Do you want to continue?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes; if (!ok) return; } QUrl url = document->url(); saveWindowSettings(); if (!document->reload()) { QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Error: Could not reload this document")); } return; } QDockWidget* KisMainWindow::createDockWidget(KoDockFactoryBase* factory) { QDockWidget* dockWidget = 0; bool lockAllDockers = KisConfig(true).readEntry("LockAllDockerPanels", false); if (!d->dockWidgetsMap.contains(factory->id())) { dockWidget = factory->createDockWidget(); // It is quite possible that a dock factory cannot create the dock; don't // do anything in that case. if (!dockWidget) { warnKrita << "Could not create docker for" << factory->id(); return 0; } dockWidget->setFont(KoDockRegistry::dockFont()); dockWidget->setObjectName(factory->id()); dockWidget->setParent(this); if (lockAllDockers) { if (dockWidget->titleBarWidget()) { dockWidget->titleBarWidget()->setVisible(false); } dockWidget->setFeatures(QDockWidget::NoDockWidgetFeatures); } if (dockWidget->widget() && dockWidget->widget()->layout()) dockWidget->widget()->layout()->setContentsMargins(1, 1, 1, 1); Qt::DockWidgetArea side = Qt::RightDockWidgetArea; bool visible = true; switch (factory->defaultDockPosition()) { case KoDockFactoryBase::DockTornOff: dockWidget->setFloating(true); // position nicely? break; case KoDockFactoryBase::DockTop: side = Qt::TopDockWidgetArea; break; case KoDockFactoryBase::DockLeft: side = Qt::LeftDockWidgetArea; break; case KoDockFactoryBase::DockBottom: side = Qt::BottomDockWidgetArea; break; case KoDockFactoryBase::DockRight: side = Qt::RightDockWidgetArea; break; case KoDockFactoryBase::DockMinimized: default: side = Qt::RightDockWidgetArea; visible = false; } KConfigGroup group = d->windowStateConfig.group("DockWidget " + factory->id()); side = static_cast(group.readEntry("DockArea", static_cast(side))); if (side == Qt::NoDockWidgetArea) side = Qt::RightDockWidgetArea; addDockWidget(side, dockWidget); if (!visible) { dockWidget->hide(); } d->dockWidgetsMap.insert(factory->id(), dockWidget); } else { dockWidget = d->dockWidgetsMap[factory->id()]; } #ifdef Q_OS_MACOS dockWidget->setAttribute(Qt::WA_MacSmallSize, true); #endif dockWidget->setFont(KoDockRegistry::dockFont()); connect(dockWidget, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), this, SLOT(forceDockTabFonts())); return dockWidget; } void KisMainWindow::forceDockTabFonts() { Q_FOREACH (QObject *child, children()) { if (child->inherits("QTabBar")) { ((QTabBar *)child)->setFont(KoDockRegistry::dockFont()); } } } QList KisMainWindow::dockWidgets() const { return d->dockWidgetsMap.values(); } QDockWidget* KisMainWindow::dockWidget(const QString &id) { if (!d->dockWidgetsMap.contains(id)) return 0; return d->dockWidgetsMap[id]; } QList KisMainWindow::canvasObservers() const { QList observers; Q_FOREACH (QDockWidget *docker, dockWidgets()) { KoCanvasObserverBase *observer = dynamic_cast(docker); if (observer) { observers << observer; } else { warnKrita << docker << "is not a canvas observer"; } } return observers; } void KisMainWindow::toggleDockersVisibility(bool visible) { if (!visible) { d->dockerStateBeforeHiding = saveState(); Q_FOREACH (QObject* widget, children()) { if (widget->inherits("QDockWidget")) { QDockWidget* dw = static_cast(widget); if (dw->isVisible()) { dw->hide(); } } } } else { restoreState(d->dockerStateBeforeHiding); } } void KisMainWindow::slotDocumentTitleModified() { updateCaption(); updateReloadFileAction(d->activeView ? d->activeView->document() : 0); } void KisMainWindow::subWindowActivated() { bool enabled = (activeKisView() != 0); d->mdiCascade->setEnabled(enabled); d->mdiNextWindow->setEnabled(enabled); d->mdiPreviousWindow->setEnabled(enabled); d->mdiTile->setEnabled(enabled); d->close->setEnabled(enabled); d->closeAll->setEnabled(enabled); setActiveSubWindow(d->mdiArea->activeSubWindow()); Q_FOREACH (QToolBar *tb, toolBars()) { if (tb->objectName() == "BrushesAndStuff") { tb->setEnabled(enabled); } } /** * Qt has a weirdness, it has hardcoded shortcuts added to an action * in the window menu. We need to reset the shortcuts for that menu * to nothing, otherwise the shortcuts cannot be made configurable. * * See: https://bugs.kde.org/show_bug.cgi?id=352205 * https://bugs.kde.org/show_bug.cgi?id=375524 * https://bugs.kde.org/show_bug.cgi?id=398729 */ QMdiSubWindow *subWindow = d->mdiArea->currentSubWindow(); if (subWindow) { QMenu *menu = subWindow->systemMenu(); if (menu && menu->actions().size() == 8) { Q_FOREACH (QAction *action, menu->actions()) { action->setShortcut(QKeySequence()); } menu->actions().last()->deleteLater(); } } updateCaption(); d->actionManager()->updateGUI(); } void KisMainWindow::windowFocused() { /** * Notify selection manager so that it could update selection mask overlay */ if (viewManager() && viewManager()->selectionManager()) { viewManager()->selectionManager()->selectionChanged(); } KisPart *kisPart = KisPart::instance(); KisWindowLayoutManager *layoutManager = KisWindowLayoutManager::instance(); if (!layoutManager->primaryWorkspaceFollowsFocus()) return; QUuid primary = layoutManager->primaryWindowId(); if (primary.isNull()) return; if (d->id == primary) { if (!d->workspaceBorrowedBy.isNull()) { KisMainWindow *borrower = kisPart->windowById(d->workspaceBorrowedBy); if (!borrower) return; swapWorkspaces(this, borrower); } } else { if (d->workspaceBorrowedBy == primary) return; KisMainWindow *primaryWindow = kisPart->windowById(primary); if (!primaryWindow) return; swapWorkspaces(this, primaryWindow); } } void KisMainWindow::updateWindowMenu() { QMenu *menu = d->windowMenu->menu(); menu->clear(); menu->addAction(d->newWindow); menu->addAction(d->documentMenu); QMenu *docMenu = d->documentMenu->menu(); docMenu->clear(); QFontMetrics fontMetrics = docMenu->fontMetrics(); #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QScreen *screen = qApp->screenAt(this->geometry().topLeft()); - int fileStringWidth = int(screen->availableGeometry().width() * .40f); + int fileStringWidth = 300; + if (screen) { + fileStringWidth = int(screen->availableGeometry().width() * .40f); + } #else int fileStringWidth = int(QApplication::desktop()->screenGeometry(this).width() * .40f); #endif Q_FOREACH (QPointer doc, KisPart::instance()->documents()) { if (doc) { QString title = fontMetrics.elidedText(doc->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth); if (title.isEmpty() && doc->image()) { title = doc->image()->objectName(); } QAction *action = docMenu->addAction(title); action->setIcon(qApp->windowIcon()); connect(action, SIGNAL(triggered()), d->documentMapper, SLOT(map())); d->documentMapper->setMapping(action, doc); } } menu->addAction(d->workspaceMenu); QMenu *workspaceMenu = d->workspaceMenu->menu(); workspaceMenu->clear(); KisResourceIterator resourceIterator(KisResourceModelProvider::resourceModel(ResourceType::Workspaces)); KisMainWindow *m_this = this; while (resourceIterator.hasNext()) { KisResourceItemSP resource = resourceIterator.next(); QAction *action = workspaceMenu->addAction(resource->name()); connect(action, &QAction::triggered, this, [=]() { m_this->restoreWorkspace(resource->id()); }); } workspaceMenu->addSeparator(); connect(workspaceMenu->addAction(i18nc("@action:inmenu", "&Import Workspace...")), &QAction::triggered, this, [&]() { QStringList mimeTypes = KisResourceLoaderRegistry::instance()->mimeTypes(ResourceType::Workspaces); KoFileDialog dialog(0, KoFileDialog::OpenFile, "OpenDocument"); dialog.setMimeTypeFilters(mimeTypes); dialog.setCaption(i18nc("@title:window", "Choose File to Add")); QString filename = dialog.filename(); d->workspacemodel->importResourceFile(filename); }); connect(workspaceMenu->addAction(i18nc("@action:inmenu", "&New Workspace...")), &QAction::triggered, [=]() { QString name; auto rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResourceSP workspace(new KisWorkspaceResource("")); workspace->setDockerState(m_this->saveState()); d->viewManager->canvasResourceProvider()->notifySavingWorkspace(workspace); workspace->setValid(true); QString saveLocation = rserver->saveLocation(); QFileInfo fileInfo(saveLocation + name + workspace->defaultFileExtension()); bool fileOverWriteAccepted = false; while(!fileOverWriteAccepted) { name = QInputDialog::getText(this, i18nc("@title:window", "New Workspace..."), i18nc("@label:textbox", "Name:")); if (name.isNull() || name.isEmpty()) { return; } else { fileInfo = QFileInfo(saveLocation + name.split(" ").join("_") + workspace->defaultFileExtension()); if (fileInfo.exists()) { int res = QMessageBox::warning(this, i18nc("@title:window", "Name Already Exists") , i18n("The name '%1' already exists, do you wish to overwrite it?", name) , QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (res == QMessageBox::Yes) fileOverWriteAccepted = true; } else { fileOverWriteAccepted = true; } } } workspace->setFilename(fileInfo.fileName()); workspace->setName(name); rserver->addResource(workspace); }); // TODO: What to do about delete? // workspaceMenu->addAction(i18nc("@action:inmenu", "&Delete Workspace...")); menu->addSeparator(); menu->addAction(d->close); menu->addAction(d->closeAll); if (d->mdiArea->viewMode() == QMdiArea::SubWindowView) { menu->addSeparator(); menu->addAction(d->mdiTile); menu->addAction(d->mdiCascade); } menu->addSeparator(); menu->addAction(d->mdiNextWindow); menu->addAction(d->mdiPreviousWindow); menu->addSeparator(); QList windows = d->mdiArea->subWindowList(); for (int i = 0; i < windows.size(); ++i) { QPointerchild = qobject_cast(windows.at(i)->widget()); if (child && child->document()) { QString text; if (i < 9) { text = i18n("&%1 %2", i + 1, fontMetrics.elidedText(child->document()->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth)); } else { text = i18n("%1 %2", i + 1, fontMetrics.elidedText(child->document()->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth)); } QAction *action = menu->addAction(text); action->setIcon(qApp->windowIcon()); action->setCheckable(true); action->setChecked(child == activeKisView()); connect(action, SIGNAL(triggered()), d->windowMapper, SLOT(map())); d->windowMapper->setMapping(action, windows.at(i)); } } bool showMdiArea = windows.count( ) > 0; if (!showMdiArea) { showWelcomeScreen(true); // see workaround in function in header // keep the recent file list updated when going back to welcome screen reloadRecentFileList(); d->welcomePage->populateRecentDocuments(); } // enable/disable the toolbox docker if there are no documents open Q_FOREACH (QObject* widget, children()) { if (widget->inherits("QDockWidget")) { QDockWidget* dw = static_cast(widget); if ( dw->objectName() == "ToolBox") { dw->setEnabled(showMdiArea); } } } updateCaption(); } void KisMainWindow::updateSubwindowFlags() { bool onlyOne = false; if (d->mdiArea->subWindowList().size() == 1 && d->mdiArea->viewMode() == QMdiArea::SubWindowView) { onlyOne = true; } Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { if (onlyOne) { subwin->setWindowFlags(subwin->windowFlags() | Qt::FramelessWindowHint); subwin->showMaximized(); } else { subwin->setWindowFlags((subwin->windowFlags() | Qt::FramelessWindowHint) ^ Qt::FramelessWindowHint); } } } void KisMainWindow::setActiveSubWindow(QWidget *window) { if (!window) return; QMdiSubWindow *subwin = qobject_cast(window); //dbgKrita << "setActiveSubWindow();" << subwin << d->activeSubWindow; if (subwin && subwin != d->activeSubWindow) { KisView *view = qobject_cast(subwin->widget()); //dbgKrita << "\t" << view << activeView(); if (view && view != activeView()) { d->mdiArea->setActiveSubWindow(subwin); setActiveView(view); } d->activeSubWindow = subwin; } updateWindowMenu(); d->actionManager()->updateGUI(); } void KisMainWindow::configChanged() { KisConfig cfg(true); QMdiArea::ViewMode viewMode = (QMdiArea::ViewMode)cfg.readEntry("mdi_viewmode", (int)QMdiArea::TabbedView); d->mdiArea->setViewMode(viewMode); Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); /** * Dirty workaround for a bug in Qt (checked on Qt 5.6.1): * * If you make a window "Show on top" and then switch to the tabbed mode * the window will continue to be painted in its initial "mid-screen" * position. It will persist here until you explicitly switch to its tab. */ if (viewMode == QMdiArea::TabbedView) { Qt::WindowFlags oldFlags = subwin->windowFlags(); Qt::WindowFlags flags = oldFlags; flags &= ~Qt::WindowStaysOnTopHint; flags &= ~Qt::WindowStaysOnBottomHint; if (flags != oldFlags) { subwin->setWindowFlags(flags); subwin->showMaximized(); } } } #ifdef Q_OS_MACOS updateSubwindowFlags(); #endif KConfigGroup group( KSharedConfig::openConfig(), "theme"); d->themeManager->setCurrentTheme(group.readEntry("Theme", "Krita dark")); d->actionManager()->updateGUI(); QString s = cfg.getMDIBackgroundColor(); KoColor c = KoColor::fromXML(s); QBrush brush(c.toQColor()); d->mdiArea->setBackground(brush); QString backgroundImage = cfg.getMDIBackgroundImage(); if (backgroundImage != "") { QImage image(backgroundImage); QBrush brush(image); d->mdiArea->setBackground(brush); } d->mdiArea->update(); } KisView* KisMainWindow::newView(QObject *document, QMdiSubWindow *subWindow) { KisDocument *doc = qobject_cast(document); KisView *view = addViewAndNotifyLoadingCompleted(doc, subWindow); d->actionManager()->updateGUI(); return view; } void KisMainWindow::newWindow() { KisMainWindow *mainWindow = KisPart::instance()->createMainWindow(); mainWindow->initializeGeometry(); mainWindow->show(); } void KisMainWindow::closeCurrentWindow() { if (d->mdiArea->currentSubWindow()) { d->mdiArea->currentSubWindow()->close(); d->actionManager()->updateGUI(); } } void KisMainWindow::checkSanity() { // print error if the lcms engine is not available if (!KoColorSpaceEngineRegistry::instance()->contains("icc")) { // need to wait 1 event since exiting here would not work. m_errorMessage = i18n("The Krita LittleCMS color management plugin is not installed. Krita will quit now."); m_dieOnError = true; QTimer::singleShot(0, this, SLOT(showErrorAndDie())); return; } KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); if (rserver->resourceCount() == 0) { m_errorMessage = i18n("Krita cannot find any brush presets! Krita will quit now."); m_dieOnError = true; QTimer::singleShot(0, this, SLOT(showErrorAndDie())); return; } } void KisMainWindow::showErrorAndDie() { QMessageBox::critical(0, i18nc("@title:window", "Installation error"), m_errorMessage); if (m_dieOnError) { exit(10); } } void KisMainWindow::showAboutApplication() { KisAboutApplication dlg(this); dlg.exec(); } QPointer KisMainWindow::activeKisView() { if (!d->mdiArea) return 0; QMdiSubWindow *activeSubWindow = d->mdiArea->activeSubWindow(); //dbgKrita << "activeKisView" << activeSubWindow; if (!activeSubWindow) return 0; return qobject_cast(activeSubWindow->widget()); } void KisMainWindow::newOptionWidgets(KoCanvasController *controller, const QList > &optionWidgetList) { KIS_ASSERT_RECOVER_NOOP(controller == KoToolManager::instance()->activeCanvasController()); bool isOurOwnView = false; Q_FOREACH (QPointer view, KisPart::instance()->views()) { if (view && view->canvasController() == controller) { isOurOwnView = view->mainWindow() == this; } } if (!isOurOwnView) return; Q_FOREACH (QWidget *w, optionWidgetList) { #ifdef Q_OS_MACOS w->setAttribute(Qt::WA_MacSmallSize, true); #endif w->setFont(KoDockRegistry::dockFont()); } if (d->toolOptionsDocker) { d->toolOptionsDocker->setOptionWidgets(optionWidgetList); } else { d->viewManager->paintOpBox()->newOptionWidgets(optionWidgetList); } } void KisMainWindow::applyDefaultSettings(QPrinter &printer) { if (!d->activeView) return; QString title = d->activeView->document()->documentInfo()->aboutInfo("title"); if (title.isEmpty()) { QFileInfo info(d->activeView->document()->url().fileName()); title = info.completeBaseName(); } if (title.isEmpty()) { // #139905 title = i18n("%1 unsaved document (%2)", qApp->applicationDisplayName(), QLocale().toString(QDate::currentDate(), QLocale::ShortFormat)); } printer.setDocName(title); } void KisMainWindow::createActions() { KisActionManager *actionManager = d->actionManager(); actionManager->createStandardAction(KStandardAction::New, this, SLOT(slotFileNew())); actionManager->createStandardAction(KStandardAction::Open, this, SLOT(slotFileOpen())); actionManager->createStandardAction(KStandardAction::Quit, this, SLOT(slotFileQuit())); actionManager->createStandardAction(KStandardAction::ConfigureToolbars, this, SLOT(slotConfigureToolbars())); d->fullScreenMode = actionManager->createStandardAction(KStandardAction::FullScreen, this, SLOT(viewFullscreen(bool))); d->recentFiles = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(QUrl)), actionCollection()); connect(d->recentFiles, SIGNAL(recentListCleared()), this, SLOT(saveRecentFiles())); KSharedConfigPtr configPtr = KSharedConfig::openConfig(); d->recentFiles->loadEntries(configPtr->group("RecentFiles")); d->saveAction = actionManager->createStandardAction(KStandardAction::Save, this, SLOT(slotFileSave())); d->saveAction->setActivationFlags(KisAction::ACTIVE_IMAGE); d->saveActionAs = actionManager->createStandardAction(KStandardAction::SaveAs, this, SLOT(slotFileSaveAs())); d->saveActionAs->setActivationFlags(KisAction::ACTIVE_IMAGE); // d->printAction = actionManager->createStandardAction(KStandardAction::Print, this, SLOT(slotFilePrint())); // d->printAction->setActivationFlags(KisAction::ACTIVE_IMAGE); // d->printActionPreview = actionManager->createStandardAction(KStandardAction::PrintPreview, this, SLOT(slotFilePrintPreview())); // d->printActionPreview->setActivationFlags(KisAction::ACTIVE_IMAGE); d->undo = actionManager->createStandardAction(KStandardAction::Undo, this, SLOT(undo())); d->undo->setActivationFlags(KisAction::ACTIVE_IMAGE); d->redo = actionManager->createStandardAction(KStandardAction::Redo, this, SLOT(redo())); d->redo->setActivationFlags(KisAction::ACTIVE_IMAGE); d->undoActionsUpdateManager.reset(new KisUndoActionsUpdateManager(d->undo, d->redo)); d->undoActionsUpdateManager->setCurrentDocument(d->activeView ? d->activeView->document() : 0); // d->exportPdf = actionManager->createAction("file_export_pdf"); // connect(d->exportPdf, SIGNAL(triggered()), this, SLOT(exportToPdf())); d->importAnimation = actionManager->createAction("file_import_animation"); connect(d->importAnimation, SIGNAL(triggered()), this, SLOT(importAnimation())); d->closeAll = actionManager->createAction("file_close_all"); connect(d->closeAll, SIGNAL(triggered()), this, SLOT(slotFileCloseAll())); // d->reloadFile = actionManager->createAction("file_reload_file"); // d->reloadFile->setActivationFlags(KisAction::CURRENT_IMAGE_MODIFIED); // connect(d->reloadFile, SIGNAL(triggered(bool)), this, SLOT(slotReloadFile())); d->importFile = actionManager->createAction("file_import_file"); connect(d->importFile, SIGNAL(triggered(bool)), this, SLOT(slotImportFile())); d->exportFile = actionManager->createAction("file_export_file"); connect(d->exportFile, SIGNAL(triggered(bool)), this, SLOT(slotExportFile())); /* The following entry opens the document information dialog. Since the action is named so it intends to show data this entry should not have a trailing ellipses (...). */ d->showDocumentInfo = actionManager->createAction("file_documentinfo"); connect(d->showDocumentInfo, SIGNAL(triggered(bool)), this, SLOT(slotDocumentInfo())); d->themeManager->setThemeMenuAction(new KActionMenu(i18nc("@action:inmenu", "&Themes"), this)); d->themeManager->registerThemeActions(actionCollection()); connect(d->themeManager, SIGNAL(signalThemeChanged()), this, SLOT(slotThemeChanged())); connect(d->themeManager, SIGNAL(signalThemeChanged()), d->welcomePage, SLOT(slotUpdateThemeColors())); d->toggleDockers = actionManager->createAction("view_toggledockers"); KisConfig(true).showDockers(true); d->toggleDockers->setChecked(true); connect(d->toggleDockers, SIGNAL(toggled(bool)), SLOT(toggleDockersVisibility(bool))); d->toggleDetachCanvas = actionManager->createAction("view_detached_canvas"); d->toggleDetachCanvas->setChecked(false); connect(d->toggleDetachCanvas, SIGNAL(toggled(bool)), SLOT(setCanvasDetached(bool))); setCanvasDetached(false); actionCollection()->addAction("settings_dockers_menu", d->dockWidgetMenu); actionCollection()->addAction("window", d->windowMenu); d->mdiCascade = actionManager->createAction("windows_cascade"); connect(d->mdiCascade, SIGNAL(triggered()), d->mdiArea, SLOT(cascadeSubWindows())); d->mdiTile = actionManager->createAction("windows_tile"); connect(d->mdiTile, SIGNAL(triggered()), d->mdiArea, SLOT(tileSubWindows())); d->mdiNextWindow = actionManager->createAction("windows_next"); connect(d->mdiNextWindow, SIGNAL(triggered()), d->mdiArea, SLOT(activateNextSubWindow())); d->mdiPreviousWindow = actionManager->createAction("windows_previous"); connect(d->mdiPreviousWindow, SIGNAL(triggered()), d->mdiArea, SLOT(activatePreviousSubWindow())); d->newWindow = actionManager->createAction("view_newwindow"); connect(d->newWindow, SIGNAL(triggered(bool)), this, SLOT(newWindow())); d->close = actionManager->createStandardAction(KStandardAction::Close, this, SLOT(closeCurrentWindow())); d->showSessionManager = actionManager->createAction("file_sessions"); connect(d->showSessionManager, SIGNAL(triggered(bool)), this, SLOT(slotShowSessionManager())); actionManager->createStandardAction(KStandardAction::Preferences, this, SLOT(slotPreferences())); for (int i = 0; i < 2; i++) { d->expandingSpacers[i] = new KisAction(i18n("Expanding Spacer")); d->expandingSpacers[i]->setDefaultWidget(new QWidget(this)); d->expandingSpacers[i]->defaultWidget()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); actionManager->addAction(QString("expanding_spacer_%1").arg(i), d->expandingSpacers[i]); } } void KisMainWindow::applyToolBarLayout() { const bool isPlastiqueStyle = style()->objectName() == "plastique"; Q_FOREACH (KToolBar *toolBar, toolBars()) { toolBar->layout()->setSpacing(4); if (isPlastiqueStyle) { toolBar->setContentsMargins(0, 0, 0, 2); } //Hide text for buttons with an icon in the toolbar Q_FOREACH (QAction *ac, toolBar->actions()){ if (ac->icon().pixmap(QSize(1,1)).isNull() == false){ ac->setPriority(QAction::LowPriority); }else { ac->setIcon(QIcon()); } } } } void KisMainWindow::initializeGeometry() { // if the user didn's specify the geometry on the command line (does anyone do that still?), // we first figure out some good default size and restore the x,y position. See bug 285804Z. KConfigGroup cfg = d->windowStateConfig; QByteArray geom = QByteArray::fromBase64(cfg.readEntry("ko_geometry", QByteArray())); if (!restoreGeometry(geom)) { const int scnum = QApplication::desktop()->screenNumber(parentWidget()); QRect desk = QGuiApplication::screens().at(scnum)->availableVirtualGeometry(); quint32 x = desk.x(); quint32 y = desk.y(); quint32 w = 0; quint32 h = 0; // Default size -- maximize on small screens, something useful on big screens const int deskWidth = desk.width(); if (deskWidth > 1024) { // a nice width, and slightly less than total available // height to compensate for the window decs w = (deskWidth / 3) * 2; h = (desk.height() / 3) * 2; } else { w = desk.width(); h = desk.height(); } x += (desk.width() - w) / 2; y += (desk.height() - h) / 2; move(x,y); setGeometry(geometry().x(), geometry().y(), w, h); } d->fullScreenMode->setChecked(isFullScreen()); } void KisMainWindow::showManual() { QDesktopServices::openUrl(QUrl("https://docs.krita.org")); } void KisMainWindow::windowScreenChanged(QScreen *screen) { emit screenChanged(); d->screenConnectionsStore.clear(); d->screenConnectionsStore.addConnection(screen, SIGNAL(physicalDotsPerInchChanged(qreal)), this, SIGNAL(screenChanged())); } void KisMainWindow::slotXmlGuiMakingChanges(bool finished) { if (finished) { subWindowActivated(); } } #include diff --git a/libs/ui/KisMainWindow.h b/libs/ui/KisMainWindow.h index 1964b4d3f4..bcc858b29d 100644 --- a/libs/ui/KisMainWindow.h +++ b/libs/ui/KisMainWindow.h @@ -1,529 +1,530 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2004 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_MAIN_WINDOW_H #define KIS_MAIN_WINDOW_H #include "kritaui_export.h" #include #include #include #include #include #include #include #include "KisView.h" #include class QCloseEvent; class QMoveEvent; struct KoPageLayout; class KoCanvasResourceProvider; class KisDocument; class KisPrintJob; class KoDockFactoryBase; class QDockWidget; class KisView; class KisViewManager; class KoCanvasController; /** * @brief Main window for Krita * * This class is used to represent a main window within a Krita session. Each * main window contains a menubar and some toolbars, and potentially several * views of several canvases. * */ class KRITAUI_EXPORT KisMainWindow : public KXmlGuiWindow, public KoCanvasSupervisor { Q_OBJECT public: enum OpenFlag { None = 0, Import = 0x1, BatchMode = 0x2, RecoveryFile = 0x4 }; Q_DECLARE_FLAGS(OpenFlags, OpenFlag) public: /** * Constructor. * * Initializes a Calligra main window (with its basic GUI etc.). */ explicit KisMainWindow(QUuid id = QUuid()); /** * Destructor. */ ~KisMainWindow() override; QUuid id() const; /** * @brief showView shows the given view, in @p subWindow if not * null, in a new tab otherwise. */ virtual void showView(KisView *view, QMdiSubWindow *subWindow = 0); /** * @returns the currently active view */ KisView *activeView() const; /** * Sets the maximum number of recent documents entries. */ void setMaxRecentItems(uint _number); /** * The document opened a URL -> store into recent documents list. + * @param oldUrl if not empty, @p url will replace @p oldUrl if present */ - void addRecentURL(const QUrl &url); + void addRecentURL(const QUrl &url, const QUrl &oldUrl = QUrl()); /** * get list of URL strings for recent files */ QList recentFilesUrls(); /** * removes the given url from the list of recent files */ void removeRecentUrl(const QUrl &url); /** * Load the desired document and show it. * @param url the URL to open * * @return TRUE on success. */ bool openDocument(const QUrl &url, OpenFlags flags); /** * Activate a view containing the document in this window, creating one if needed. */ void showDocument(KisDocument *document); /** * Toggles between showing the welcome screen and the MDI area * * hack: There seems to be a bug that prevents events happening to the MDI area if it * isn't actively displayed (set in the widgetStack). This can cause things like the title bar * not to update correctly Before doing any actions related to opening or creating documents, * make sure to switch this first to make sure everything can communicate to the MDI area correctly */ void showWelcomeScreen(bool show); /** * Saves the document, asking for a filename if necessary. * * @param saveas if set to TRUE the user is always prompted for a filename * @param silent if set to TRUE rootDocument()->setTitleModified will not be called. * * @return TRUE on success, false on error or cancel * (don't display anything in this case, the error dialog box is also implemented here * but restore the original URL in slotFileSaveAs) */ bool saveDocument(KisDocument *document, bool saveas, bool isExporting); void setReadWrite(bool readwrite); /// Return the list of dock widgets belonging to this main window. QList dockWidgets() const; QDockWidget* dockWidget(const QString &id); QList canvasObservers() const override; KoCanvasResourceProvider *resourceManager() const; int viewCount() const; void saveWindowState(bool restoreNormalState =false); const KConfigGroup &windowStateConfig() const; /** * A wrapper around restoreState * @param state the saved state * @return TRUE on success */ bool restoreWorkspace(int workspaceId); bool restoreWorkspaceState(const QByteArray &state); static void swapWorkspaces(KisMainWindow *a, KisMainWindow *b); KisViewManager *viewManager() const; KisView *addViewAndNotifyLoadingCompleted(KisDocument *document, QMdiSubWindow *subWindow = 0); QStringList showOpenFileDialog(bool isImporting); /** * The top-level window used for a detached canvas. */ QWidget *canvasWindow() const; bool canvasDetached() const; /** * Shows if the main window is saving anything right now. If the * user presses Ctrl+W too fast, then the document can be close * before the saving is completed. I'm not sure if it is fixable * in any way without avoiding using porcessEvents() * everywhere (DK) * * Don't use it unless you have no option. */ bool hackIsSaving() const; /// Copy the given file into the bundle directory. bool installBundle(const QString &fileName) const; /** * @brief layoutThumbnail * @return image for the workspaces. */ QImage layoutThumbnail(); Q_SIGNALS: /** * This signal is emitted if the document has been saved successfully. */ void documentSaved(); /// This signal is emitted when this windows has finished loading of a /// document. The document may be opened in another window in the end. /// In this case, the signal means there is no link between the window /// and the document anymore. void loadCompleted(); /// This signal is emitted right after the docker states have been succefully restored from config void restoringDone(); /// This signal is emitted when the color theme changes void themeChanged(); /// This signal is emitted when the shortcut key configuration has changed void keyBindingsChanged(); void guiLoadingFinished(); /// emitted when the window is migrated among different screens void screenChanged(); public Q_SLOTS: /** * clears the list of the recent files */ void clearRecentFiles(); /** * Slot for opening a new document. * * If the current document is empty, the new document replaces it. * If not, a new mainwindow will be opened for showing the document. */ void slotFileNew(); /** * Slot for opening a saved file. * * If the current document is empty, the opened document replaces it. * If not a new mainwindow will be opened for showing the opened file. */ void slotFileOpen(bool isImporting = false); /** * Slot for opening a file among the recently opened files. * * If the current document is empty, the opened document replaces it. * If not a new mainwindow will be opened for showing the opened file. */ void slotFileOpenRecent(const QUrl &); /** * @brief slotPreferences open the preferences dialog */ void slotPreferences(); /** * Update caption from document info - call when document info * (title in the about page) changes. */ void updateCaption(); /** * Saves the current document with the current name. */ void slotFileSave(); void slotShowSessionManager(); // XXX: disabled KisPrintJob* exportToPdf(QString pdfFileName = QString()); /** * Update the option widgets to the argument ones, removing the currently set widgets. */ void newOptionWidgets(KoCanvasController *controller, const QList > & optionWidgetList); KisView *newView(QObject *document, QMdiSubWindow *subWindow = 0); void notifyChildViewDestroyed(KisView *view); /// Set the active view, this will update the undo/redo actions void setActiveView(KisView *view); void subWindowActivated(); void windowFocused(); /** * Reloads the recent documents list. */ void reloadRecentFileList(); /** * Detach canvas onto a separate window, or restore it back to to main window. */ void setCanvasDetached(bool detached); void slotFileSelected(QString path); void slotEmptyFilePath(); /** * Toggle full screen on/off. */ void viewFullscreen(bool fullScreen); private Q_SLOTS: /** * Save the list of recent files. */ void saveRecentFiles(); void slotLoadCompleted(); void slotLoadCanceled(const QString &); void slotSaveCompleted(); void slotSaveCanceled(const QString &); void forceDockTabFonts(); /** * @internal */ void slotDocumentTitleModified(); /** * Prints the actual document. */ void slotFilePrint(); /** * Saves the current document with a new name. */ void slotFileSaveAs(); void slotFilePrintPreview(); void importAnimation(); /** * Show a dialog with author and document information. */ void slotDocumentInfo(); /** * Closes all open documents. */ bool slotFileCloseAll(); /** * @brief showAboutApplication show the about box */ virtual void showAboutApplication(); /** * Closes the mainwindow. */ void slotFileQuit(); /** * Configure toolbars. */ void slotConfigureToolbars(); /** * Post toolbar config. * (Plug action lists back in, etc.) */ void slotNewToolbarConfig(); /** * Shows or hides a toolbar */ void slotToolbarToggled(bool toggle); /** * Reload file */ void slotReloadFile(); /** * File --> Import * * This will call slotFileOpen(). */ void slotImportFile(); /** * File --> Export * * This will call slotFileSaveAs(). */ void slotExportFile(); /** * Hide the dockers */ void toggleDockersVisibility(bool visible); /** * Handle theme changes from theme manager */ void slotThemeChanged(); void undo(); void redo(); void updateWindowMenu(); void updateSubwindowFlags(); void setActiveSubWindow(QWidget *window); void configChanged(); void newWindow(); void closeCurrentWindow(); void checkSanity(); /// Quits Krita with error message from m_errorMessage. void showErrorAndDie(); void initializeGeometry(); void showManual(); void switchTab(int index); void windowScreenChanged(QScreen *screen); void slotXmlGuiMakingChanges(bool finished); protected: void closeEvent(QCloseEvent * e) override; void resizeEvent(QResizeEvent * e) override; // QWidget overrides private: friend class KisWelcomePageWidget; void dragMove(QDragMoveEvent *event); void dragLeave(); private: /** * Add a the given view to the list of views of this mainwindow. * This is a private implementation. For public usage please use * newView() and addViewAndNotifyLoadingCompleted(). */ void addView(KisView *view, QMdiSubWindow *subWindow = 0); friend class KisPart; /** * Returns the dockwidget specified by the @p factory. If the dock widget doesn't exist yet it's created. * Add a "view_palette_action_menu" action to your view menu if you want to use closable dock widgets. * @param factory the factory used to create the dock widget if needed * @return the dock widget specified by @p factory (may be 0) */ QDockWidget* createDockWidget(KoDockFactoryBase* factory); bool openDocumentInternal(const QUrl &url, KisMainWindow::OpenFlags flags = 0); /** * Updates the window caption based on the document info and path. */ void updateCaption(const QString & caption, bool modified); void updateReloadFileAction(KisDocument *doc); void saveWindowSettings(); QPointer activeKisView(); void applyDefaultSettings(QPrinter &printer); void createActions(); void applyToolBarLayout(); QByteArray borrowWorkspace(KisMainWindow *borrower); private: /** * Struct used in the list created by createCustomDocumentWidgets() */ struct CustomDocumentWidgetItem { /// Pointer to the custom document widget QWidget *widget; /// title used in the sidebar. If left empty it will be displayed as "Custom Document" QString title; /// icon used in the sidebar. If left empty it will use the unknown icon QString icon; }; class Private; Private * const d; QString m_errorMessage; bool m_dieOnError; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KisMainWindow::OpenFlags) #endif diff --git a/libs/ui/KisNetworkAccessManager.cpp b/libs/ui/KisNetworkAccessManager.cpp index 09adb97dd4..e0a6e5f1f2 100644 --- a/libs/ui/KisNetworkAccessManager.cpp +++ b/libs/ui/KisNetworkAccessManager.cpp @@ -1,50 +1,51 @@ /* * Copyright (c) 2015 Boudewijn Rempt * * 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; either * version 2.1 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 library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "KisNetworkAccessManager.h" #include #include #include #include #include KisNetworkAccessManager::KisNetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent) { + setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); } void KisNetworkAccessManager::getUrl(const QUrl &url) { QNetworkRequest req; req.setUrl(url); get(req); } QNetworkReply* KisNetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { QString agentStr = QString::fromLatin1("%1/%2 (QNetworkAccessManager %3; %4; %5 bit)") .arg(qApp->applicationName()) .arg(qApp->applicationVersion()) .arg(QSysInfo::prettyProductName()) .arg(QLocale::system().name()) .arg(QSysInfo::WordSize); QNetworkRequest req(request); req.setRawHeader("User-Agent", agentStr.toLatin1()); return QNetworkAccessManager::createRequest(op, req, outgoingData); } diff --git a/libs/ui/KisPart.cpp b/libs/ui/KisPart.cpp index d8e61f9951..4e6ecc263f 100644 --- a/libs/ui/KisPart.cpp +++ b/libs/ui/KisPart.cpp @@ -1,590 +1,590 @@ /* This file is part of the KDE project * Copyright (C) 1998-1999 Torben Weis * Copyright (C) 2000-2005 David Faure * Copyright (C) 2007-2008 Thorsten Zachmann * Copyright (C) 2010-2012 Boudewijn Rempt * Copyright (C) 2011 Inge Wallin * Copyright (C) 2015 Michael Abrahams * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisPart.h" #include "KoProgressProxy.h" #include #include #include #include #include #include #include #include #include "KisApplication.h" #include "KisMainWindow.h" #include "KisDocument.h" #include "KisView.h" #include "KisViewManager.h" #include "KisImportExportManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "kis_shape_controller.h" #include "KisResourceServerProvider.h" #include "kis_animation_cache_populator.h" #include "kis_idle_watcher.h" #include "kis_image.h" #include "KisOpenPane.h" #include "kis_color_manager.h" #include "kis_action.h" #include "kis_action_registry.h" #include "KisSessionResource.h" #include "KisBusyWaitBroker.h" #include "dialogs/kis_delayed_save_dialog.h" Q_GLOBAL_STATIC(KisPart, s_instance) class Q_DECL_HIDDEN KisPart::Private { public: Private(KisPart *_part) : part(_part) , idleWatcher(2500) , animationCachePopulator(_part) { } ~Private() { } KisPart *part; QList > views; QList > mainWindows; QList > documents; KActionCollection *actionCollection{0}; KisIdleWatcher idleWatcher; KisAnimationCachePopulator animationCachePopulator; KisSessionResourceSP currentSession; bool closingSession{false}; QScopedPointer sessionManager; bool queryCloseDocument(KisDocument *document) { Q_FOREACH(auto view, views) { if (view && view->isVisible() && view->document() == document) { return view->queryClose(); } } return true; } }; KisPart* KisPart::instance() { return s_instance; } namespace { void busyWaitWithFeedback(KisImageSP image) { const int busyWaitDelay = 1000; KisDelayedSaveDialog dialog(image, KisDelayedSaveDialog::ForcedDialog, busyWaitDelay, KisPart::instance()->currentMainwindow()); dialog.blockIfImageIsBusy(); } } KisPart::KisPart() : d(new Private(this)) { // Preload all the resources in the background Q_UNUSED(KoResourceServerProvider::instance()); Q_UNUSED(KisResourceServerProvider::instance()); Q_UNUSED(KisColorManager::instance()); connect(this, SIGNAL(documentOpened(QString)), this, SLOT(updateIdleWatcherConnections())); connect(this, SIGNAL(documentClosed(QString)), this, SLOT(updateIdleWatcherConnections())); connect(KisActionRegistry::instance(), SIGNAL(shortcutsUpdated()), this, SLOT(updateShortcuts())); connect(&d->idleWatcher, SIGNAL(startedIdleMode()), &d->animationCachePopulator, SLOT(slotRequestRegeneration())); d->animationCachePopulator.slotRequestRegeneration(); KisBusyWaitBroker::instance()->setFeedbackCallback(&busyWaitWithFeedback); } KisPart::~KisPart() { while (!d->documents.isEmpty()) { delete d->documents.takeFirst(); } while (!d->views.isEmpty()) { delete d->views.takeFirst(); } while (!d->mainWindows.isEmpty()) { delete d->mainWindows.takeFirst(); } delete d; } void KisPart::updateIdleWatcherConnections() { QVector images; Q_FOREACH (QPointer document, documents()) { if (document->image()) { images << document->image(); } } d->idleWatcher.setTrackedImages(images); } void KisPart::addDocument(KisDocument *document) { //dbgUI << "Adding document to part list" << document; Q_ASSERT(document); if (!d->documents.contains(document)) { d->documents.append(document); emit documentOpened('/'+objectName()); emit sigDocumentAdded(document); connect(document, SIGNAL(sigSavingFinished()), SLOT(slotDocumentSaved())); } } QList > KisPart::documents() const { return d->documents; } KisDocument *KisPart::createDocument() const { KisDocument *doc = new KisDocument(); return doc; } KisDocument *KisPart::createTemporaryDocument() const { KisDocument *doc = new KisDocument(false); return doc; } int KisPart::documentCount() const { return d->documents.size(); } void KisPart::removeDocument(KisDocument *document) { d->documents.removeAll(document); emit documentClosed('/'+objectName()); emit sigDocumentRemoved(document->url().toLocalFile()); document->deleteLater(); } KisMainWindow *KisPart::createMainWindow(QUuid id) { KisMainWindow *mw = new KisMainWindow(id); dbgUI <<"mainWindow" << (void*)mw << "added to view" << this; d->mainWindows.append(mw); emit sigWindowAdded(mw); return mw; } KisView *KisPart::createView(KisDocument *document, KisViewManager *viewManager, QWidget *parent) { // If creating the canvas fails, record this and disable OpenGL next time KisConfig cfg(false); KConfigGroup grp( KSharedConfig::openConfig(), "crashprevention"); if (grp.readEntry("CreatingCanvas", false)) { cfg.disableOpenGL(); } if (cfg.canvasState() == "OPENGL_FAILED") { cfg.disableOpenGL(); } grp.writeEntry("CreatingCanvas", true); grp.sync(); QApplication::setOverrideCursor(Qt::WaitCursor); KisView *view = new KisView(document, viewManager, parent); QApplication::restoreOverrideCursor(); // Record successful canvas creation grp.writeEntry("CreatingCanvas", false); grp.sync(); addView(view); return view; } void KisPart::addView(KisView *view) { if (!view) return; if (!d->views.contains(view)) { d->views.append(view); } emit sigViewAdded(view); } void KisPart::removeView(KisView *view) { if (!view) return; /** * HACK ALERT: we check here explicitly if the document (or main * window), is saving the stuff. If we close the * document *before* the saving is completed, a crash * will happen. */ KIS_ASSERT_RECOVER_RETURN(!view->mainWindow()->hackIsSaving()); emit sigViewRemoved(view); QPointer doc = view->document(); d->views.removeAll(view); if (doc) { bool found = false; Q_FOREACH (QPointer view, d->views) { if (view && view->document() == doc) { found = true; break; } } if (!found) { removeDocument(doc); } } } QList > KisPart::views() const { return d->views; } int KisPart::viewCount(KisDocument *doc) const { if (!doc) { return d->views.count(); } else { int count = 0; Q_FOREACH (QPointer view, d->views) { if (view && view->isVisible() && view->document() == doc) { count++; } } return count; } } bool KisPart::closingSession() const { return d->closingSession; } bool KisPart::exists() { return s_instance.exists(); } bool KisPart::closeSession(bool keepWindows) { d->closingSession = true; Q_FOREACH(auto document, d->documents) { if (!d->queryCloseDocument(document.data())) { d->closingSession = false; return false; } } if (d->currentSession) { KisConfig kisCfg(false); if (kisCfg.saveSessionOnQuit(false)) { d->currentSession->storeCurrentWindows(); d->currentSession->save(); KConfigGroup cfg = KSharedConfig::openConfig()->group("session"); cfg.writeEntry("previousSession", d->currentSession->name()); } d->currentSession = nullptr; } if (!keepWindows) { Q_FOREACH (auto window, d->mainWindows) { window->close(); } if (d->sessionManager) { d->sessionManager->close(); } } d->closingSession = false; return true; } void KisPart::slotDocumentSaved() { KisDocument *doc = qobject_cast(sender()); emit sigDocumentSaved(doc->url().toLocalFile()); } void KisPart::removeMainWindow(KisMainWindow *mainWindow) { dbgUI <<"mainWindow" << (void*)mainWindow <<"removed from doc" << this; if (mainWindow) { d->mainWindows.removeAll(mainWindow); } } const QList > &KisPart::mainWindows() const { return d->mainWindows; } int KisPart::mainwindowCount() const { return d->mainWindows.count(); } KisMainWindow *KisPart::currentMainwindow() const { QWidget *widget = qApp->activeWindow(); KisMainWindow *mainWindow = qobject_cast(widget); while (!mainWindow && widget) { widget = widget->parentWidget(); mainWindow = qobject_cast(widget); } if (!mainWindow && mainWindows().size() > 0) { mainWindow = mainWindows().first(); } return mainWindow; } KisMainWindow * KisPart::windowById(QUuid id) const { Q_FOREACH(QPointer mainWindow, d->mainWindows) { if (mainWindow->id() == id) { return mainWindow; } } return nullptr; } KisIdleWatcher* KisPart::idleWatcher() const { return &d->idleWatcher; } KisAnimationCachePopulator* KisPart::cachePopulator() const { return &d->animationCachePopulator; } void KisPart::openExistingFile(const QUrl &url) { // TODO: refactor out this method! KisMainWindow *mw = currentMainwindow(); KIS_SAFE_ASSERT_RECOVER_RETURN(mw); mw->openDocument(url, KisMainWindow::None); } void KisPart::updateShortcuts() { // Update any non-UI actionCollections. That includes: // Now update the UI actions. Q_FOREACH (KisMainWindow *mainWindow, d->mainWindows) { KActionCollection *ac = mainWindow->actionCollection(); ac->updateShortcuts(); // Loop through mainWindow->actionCollections() to modify tooltips // so that they list shortcuts at the end in parentheses Q_FOREACH ( QAction* action, ac->actions()) { // Remove any existing suffixes from the tooltips. // Note this regexp starts with a space, e.g. " (Ctrl-a)" QString strippedTooltip = action->toolTip().remove(QRegExp("\\s\\(.*\\)")); // Now update the tooltips with the new shortcut info. if (action->shortcut() == QKeySequence(0)) action->setToolTip(strippedTooltip); else action->setToolTip( strippedTooltip + " (" + action->shortcut().toString() + ")"); } } } void KisPart::openTemplate(const QUrl &url) { qApp->setOverrideCursor(Qt::BusyCursor); KisDocument *document = createDocument(); bool ok = document->loadNativeFormat(url.toLocalFile()); document->setModified(false); document->undoStack()->clear(); if (ok) { QString mimeType = KisMimeDatabase::mimeTypeForFile(url.toLocalFile()); // in case this is a open document template remove the -template from the end mimeType.remove( QRegExp( "-template$" ) ); document->setMimeTypeAfterLoading(mimeType); document->resetURL(); document->setReadWrite(true); } else { if (document->errorMessage().isEmpty()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not create document from template\n%1", document->localFilePath())); } else { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not create document from template\n%1\nReason: %2", document->localFilePath(), document->errorMessage())); } delete document; return; } addDocument(document); KisMainWindow *mw = currentMainwindow(); mw->addViewAndNotifyLoadingCompleted(document); KisOpenPane *pane = qobject_cast(sender()); if (pane) { pane->hide(); pane->deleteLater(); } qApp->restoreOverrideCursor(); } -void KisPart::addRecentURLToAllMainWindows(QUrl url) +void KisPart::addRecentURLToAllMainWindows(QUrl url, QUrl oldUrl) { // Add to recent actions list in our mainWindows Q_FOREACH (KisMainWindow *mainWindow, d->mainWindows) { - mainWindow->addRecentURL(url); + mainWindow->addRecentURL(url, oldUrl); } } void KisPart::startCustomDocument(KisDocument* doc) { addDocument(doc); KisMainWindow *mw = currentMainwindow(); KisOpenPane *pane = qobject_cast(sender()); if (pane) { pane->hide(); pane->deleteLater(); } mw->addViewAndNotifyLoadingCompleted(doc); } KisInputManager* KisPart::currentInputManager() { KisMainWindow *mw = currentMainwindow(); KisViewManager *manager = mw ? mw->viewManager() : 0; return manager ? manager->inputManager() : 0; } void KisPart::showSessionManager() { if (d->sessionManager.isNull()) { d->sessionManager.reset(new KisSessionManagerDialog()); } d->sessionManager->show(); d->sessionManager->activateWindow(); } void KisPart::startBlankSession() { KisMainWindow *window = createMainWindow(); window->initializeGeometry(); window->show(); } bool KisPart::restoreSession(const QString &sessionName) { if (sessionName.isNull()) return false; KoResourceServer *rserver = KisResourceServerProvider::instance()->sessionServer(); KisSessionResourceSP session = rserver->resourceByName(sessionName); if (!session || !session->valid()) return false; return restoreSession(session); } bool KisPart::restoreSession(KisSessionResourceSP session) { session->restore(); d->currentSession = session; return true; } void KisPart::setCurrentSession(KisSessionResourceSP session) { d->currentSession = session; } diff --git a/libs/ui/KisPart.h b/libs/ui/KisPart.h index 46c3eaf09a..7759e81107 100644 --- a/libs/ui/KisPart.h +++ b/libs/ui/KisPart.h @@ -1,289 +1,289 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2005 David Faure Copyright (C) 2007 Thorsten Zachmann Copyright (C) 2010 Boudewijn Rempt Copyright (C) 2015 Michael Abrahams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KIS_PART_H #define KIS_PART_H #include #include #include #include #include #include "kritaui_export.h" #include #include #include namespace KIO { } class KisAction; class KisDocument; class KisView; class KisDocument; class KisIdleWatcher; class KisAnimationCachePopulator; /** * KisPart a singleton class which provides the main entry point to the application. * Krita supports multiple documents, multiple main windows, and multiple * components. KisPart manages these resources and provides them to the rest of * Krita. It manages lists of Actions and shortcuts as well. * * The terminology comes from KParts, which is a system allowing one KDE app * to be run from inside another, like pressing F4 inside dophin to run konsole. * * Needless to say, KisPart hasn't got much to do with KParts anymore. */ class KRITAUI_EXPORT KisPart : public QObject { Q_OBJECT public: static KisPart *instance(); /** * Constructor. * * @param parent may be another KisDocument, or anything else. * Usually passed by KPluginFactory::create. */ explicit KisPart(); /** * Destructor. * * The destructor does not delete any attached KisView objects and it does not * delete the attached widget as returned by widget(). */ ~KisPart() override; // ----------------- Document management ----------------- /** * create an empty document. The document is not automatically registered with the part. */ KisDocument *createDocument() const; /** * create a throwaway empty document. The document does not register a resource storage */ KisDocument *createTemporaryDocument() const; /** * Add the specified document to the list of documents this KisPart manages. */ void addDocument(KisDocument *document); /** * @return a list of all documents this part manages */ QList > documents() const; /** * @return number of documents this part manages. */ int documentCount() const; void removeDocument(KisDocument *document); // ----------------- MainWindow management ----------------- /** * Create a new main window. */ KisMainWindow *createMainWindow(QUuid id = QUuid()); /** * Removes a main window from the list of managed windows. * * This is called by the MainWindow after it finishes its shutdown routine. */ void removeMainWindow(KisMainWindow *mainWindow); /** * @return the list of main windows. */ const QList >& mainWindows() const; /** * @return the number of shells for the main window */ int mainwindowCount() const; - void addRecentURLToAllMainWindows(QUrl url); + void addRecentURLToAllMainWindows(QUrl url, QUrl oldUrl = QUrl()); /** * @return the currently active main window. */ KisMainWindow *currentMainwindow() const; KisMainWindow *windowById(QUuid id) const; /** * @return the application-wide KisIdleWatcher. */ KisIdleWatcher *idleWatcher() const; /** * @return the application-wide AnimationCachePopulator. */ KisAnimationCachePopulator *cachePopulator() const; public Q_SLOTS: /** * This slot loads an existing file. * @param url the file to load */ void openExistingFile(const QUrl &url); /** * This slot loads a template and deletes the sender. * @param url the template to load */ void openTemplate(const QUrl &url); /** * @brief startCustomDocument adds the given document to the document list and deletes the sender() * @param doc */ void startCustomDocument(KisDocument *doc); private Q_SLOTS: void updateIdleWatcherConnections(); void updateShortcuts(); Q_SIGNALS: /** * emitted when a new document is opened. (for the idle watcher) */ void documentOpened(const QString &ref); /** * emitted when an old document is closed. (for the idle watcher) */ void documentClosed(const QString &ref); // These signals are for libkis or sketch void sigViewAdded(KisView *view); void sigViewRemoved(KisView *view); void sigDocumentAdded(KisDocument *document); void sigDocumentSaved(const QString &url); void sigDocumentRemoved(const QString &filename); void sigWindowAdded(KisMainWindow *window); public: KisInputManager *currentInputManager(); //------------------ View management ------------------ /** * Create a new view for the document. The view is added to the list of * views, and if the document wasn't known yet, it's registered as well. */ KisView *createView(KisDocument *document, KisViewManager *viewManager, QWidget *parent); /** * Adds a view to the document. If the part doesn't know yet about * the document, it is registered. * * This calls KisView::updateReadWrite to tell the new view * whether the document is readonly or not. */ void addView(KisView *view); /** * Removes a view of the document. */ void removeView(KisView *view); /** * @return a list of views this document is displayed in */ QList > views() const; /** * @return number of views this document is displayed in */ int viewCount(KisDocument *doc) const; //------------------ Session management ------------------ void showSessionManager(); void startBlankSession(); /** * Restores a saved session by name */ bool restoreSession(const QString &sessionName); bool restoreSession(KisSessionResourceSP session); void setCurrentSession(KisSessionResourceSP session); /** * Attempts to save the session and close all windows. * This may involve asking the user to save open files. * @return false, if closing was cancelled by the user */ bool closeSession(bool keepWindows = false); /** * Are we in the process of closing the application through closeSession(). */ bool closingSession() const; /** * This function returns true if the instance has already been initialized, * false otherwise. This to prevent premature initialization that causes crash * on android see `1fbb25506a` * @see QGlobalStatic::exists() */ static bool exists(); private Q_SLOTS: void slotDocumentSaved(); private: Q_DISABLE_COPY(KisPart) class Private; Private *const d; }; #endif diff --git a/libs/ui/KisViewManager.cpp b/libs/ui/KisViewManager.cpp index 424fada03d..279093646f 100644 --- a/libs/ui/KisViewManager.cpp +++ b/libs/ui/KisViewManager.cpp @@ -1,1449 +1,1451 @@ /* * This file is part of KimageShop^WKrayon^WKrita * * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 1999 Carsten Pfeiffer * 2002 Patrick Julien * 2003-2011 Boudewijn Rempt * 2004 Clarence Dang * 2011 José Luis Vergara * 2017 L. E. Segovia * * 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 "KisViewManager.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 "input/kis_input_manager.h" #include "canvas/kis_canvas2.h" #include "canvas/kis_canvas_controller.h" #include "canvas/kis_grid_manager.h" #include "input/kis_input_profile_manager.h" #include "kis_action_manager.h" #include "kis_action.h" #include "kis_canvas_controls_manager.h" #include "kis_canvas_resource_provider.h" #include "kis_composite_progress_proxy.h" #include #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_control_frame.h" #include "kis_coordinates_converter.h" #include "KisDocument.h" #include "kis_favorite_resource_manager.h" #include "kis_filter_manager.h" #include "kis_group_layer.h" #include #include #include "kis_image_manager.h" #include #include "kis_mainwindow_observer.h" #include "kis_mask_manager.h" #include "kis_mimedata.h" #include "kis_mirror_manager.h" #include "kis_node_commands_adapter.h" #include "kis_node.h" #include "kis_node_manager.h" #include "KisDecorationsManager.h" #include #include "kis_paintop_box.h" #include #include "KisPart.h" #include "KisPrintJob.h" #include #include "KisResourceServerProvider.h" #include "kis_selection.h" #include "kis_selection_mask.h" #include "kis_selection_manager.h" #include "kis_shape_controller.h" #include "kis_shape_layer.h" #include #include "kis_statusbar.h" #include #include #include "kis_tooltip_manager.h" #include #include "KisView.h" #include "kis_zoom_manager.h" #include "widgets/kis_floating_message.h" #include "kis_signal_auto_connection.h" #include "kis_icon_utils.h" #include "kis_guides_manager.h" #include "kis_derived_resources.h" #include "dialogs/kis_delayed_save_dialog.h" #include #include "kis_signals_blocker.h" class BlockingUserInputEventFilter : public QObject { bool eventFilter(QObject *watched, QEvent *event) override { Q_UNUSED(watched); if(dynamic_cast(event) || dynamic_cast(event) || dynamic_cast(event)) { return true; } else { return false; } } }; class KisViewManager::KisViewManagerPrivate { public: KisViewManagerPrivate(KisViewManager *_q, KActionCollection *_actionCollection, QWidget *_q_parent) : filterManager(_q) , createTemplate(0) , saveIncremental(0) , saveIncrementalBackup(0) , openResourcesDirectory(0) , rotateCanvasRight(0) , rotateCanvasLeft(0) , resetCanvasRotation(0) , wrapAroundAction(0) , levelOfDetailAction(0) , showRulersAction(0) , rulersTrackMouseAction(0) , zoomTo100pct(0) , zoomIn(0) , zoomOut(0) , selectionManager(_q) , statusBar(_q) , controlFrame(_q, _q_parent) , nodeManager(_q) , imageManager(_q) , gridManager(_q) , canvasControlsManager(_q) , paintingAssistantsManager(_q) , actionManager(_q, _actionCollection) , mainWindow(0) , showFloatingMessage(true) , currentImageView(0) , canvasResourceProvider(_q) , canvasResourceManager() , guiUpdateCompressor(30, KisSignalCompressor::POSTPONE, _q) , actionCollection(_actionCollection) , mirrorManager(_q) , inputManager(_q) , actionAuthor(0) , showPixelGrid(0) { KisViewManager::initializeResourceManager(&canvasResourceManager); } public: KisFilterManager filterManager; KisAction *createTemplate; KisAction *createCopy; KisAction *saveIncremental; KisAction *saveIncrementalBackup; KisAction *openResourcesDirectory; KisAction *rotateCanvasRight; KisAction *rotateCanvasLeft; KisAction *resetCanvasRotation; KisAction *wrapAroundAction; KisAction *levelOfDetailAction; KisAction *showRulersAction; KisAction *rulersTrackMouseAction; KisAction *zoomTo100pct; KisAction *zoomIn; KisAction *zoomOut; KisAction *softProof; KisAction *gamutCheck; KisAction *toggleFgBg; KisAction *resetFgBg; KisSelectionManager selectionManager; KisGuidesManager guidesManager; KisStatusBar statusBar; QPointer persistentImageProgressUpdater; QScopedPointer persistentUnthreadedProgressUpdaterRouter; QPointer persistentUnthreadedProgressUpdater; KisControlFrame controlFrame; KisNodeManager nodeManager; KisImageManager imageManager; KisGridManager gridManager; KisCanvasControlsManager canvasControlsManager; KisDecorationsManager paintingAssistantsManager; BlockingUserInputEventFilter blockingEventFilter; KisActionManager actionManager; QMainWindow* mainWindow; QPointer savedFloatingMessage; bool showFloatingMessage; QPointer currentImageView; KisCanvasResourceProvider canvasResourceProvider; KoCanvasResourceProvider canvasResourceManager; KisSignalCompressor guiUpdateCompressor; KActionCollection *actionCollection; KisMirrorManager mirrorManager; KisInputManager inputManager; KisSignalAutoConnectionsStore viewConnections; KSelectAction *actionAuthor; // Select action for author profile. KisAction *showPixelGrid; QByteArray canvasState; #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) QFlags windowFlags; #endif bool blockUntilOperationsFinishedImpl(KisImageSP image, bool force); }; KisViewManager::KisViewManager(QWidget *parent, KActionCollection *_actionCollection) : d(new KisViewManagerPrivate(this, _actionCollection, parent)) { d->actionCollection = _actionCollection; d->mainWindow = dynamic_cast(parent); d->canvasResourceProvider.setResourceManager(&d->canvasResourceManager); connect(&d->guiUpdateCompressor, SIGNAL(timeout()), this, SLOT(guiUpdateTimeout())); createActions(); setupManagers(); // These initialization functions must wait until KisViewManager ctor is complete. d->statusBar.setup(); d->persistentImageProgressUpdater = d->statusBar.progressUpdater()->startSubtask(1, "", true); // reset state to "completed" d->persistentImageProgressUpdater->setRange(0,100); d->persistentImageProgressUpdater->setValue(100); d->persistentUnthreadedProgressUpdater = d->statusBar.progressUpdater()->startSubtask(1, "", true); // reset state to "completed" d->persistentUnthreadedProgressUpdater->setRange(0,100); d->persistentUnthreadedProgressUpdater->setValue(100); d->persistentUnthreadedProgressUpdaterRouter.reset( new KoProgressUpdater(d->persistentUnthreadedProgressUpdater, KoProgressUpdater::Unthreaded)); d->persistentUnthreadedProgressUpdaterRouter->setAutoNestNames(true); d->controlFrame.setup(parent); //Check to draw scrollbars after "Canvas only mode" toggle is created. this->showHideScrollbars(); QScopedPointer dummy(new KoDummyCanvasController(actionCollection())); KoToolManager::instance()->registerToolActions(actionCollection(), dummy.data()); QTimer::singleShot(0, this, SLOT(initializeStatusBarVisibility())); connect(KoToolManager::instance(), SIGNAL(inputDeviceChanged(KoInputDevice)), d->controlFrame.paintopBox(), SLOT(slotInputDeviceChanged(KoInputDevice))); connect(KoToolManager::instance(), SIGNAL(changedTool(KoCanvasController*,int)), d->controlFrame.paintopBox(), SLOT(slotToolChanged(KoCanvasController*,int))); connect(&d->nodeManager, SIGNAL(sigNodeActivated(KisNodeSP)), canvasResourceProvider(), SLOT(slotNodeActivated(KisNodeSP))); connect(KisPart::instance(), SIGNAL(sigViewAdded(KisView*)), SLOT(slotViewAdded(KisView*))); connect(KisPart::instance(), SIGNAL(sigViewRemoved(KisView*)), SLOT(slotViewRemoved(KisView*))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotUpdateAuthorProfileActions())); connect(KisConfigNotifier::instance(), SIGNAL(pixelGridModeChanged()), SLOT(slotUpdatePixelGridAction())); KisInputProfileManager::instance()->loadProfiles(); KisConfig cfg(true); d->showFloatingMessage = cfg.showCanvasMessages(); const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); KoColor foreground(Qt::black, cs); d->canvasResourceProvider.setFGColor(cfg.readKoColor("LastForeGroundColor",foreground)); KoColor background(Qt::white, cs); d->canvasResourceProvider.setBGColor(cfg.readKoColor("LastBackGroundColor",background)); } KisViewManager::~KisViewManager() { KisConfig cfg(false); if (canvasResourceProvider() && canvasResourceProvider()->currentPreset()) { cfg.writeKoColor("LastForeGroundColor",canvasResourceProvider()->fgColor()); cfg.writeKoColor("LastBackGroundColor",canvasResourceProvider()->bgColor()); } cfg.writeEntry("baseLength", KisResourceItemChooserSync::instance()->baseLength()); cfg.writeEntry("CanvasOnlyActive", false); // We never restart in CavnasOnlyMode delete d; } void KisViewManager::initializeResourceManager(KoCanvasResourceProvider *resourceManager) { resourceManager->addDerivedResourceConverter(toQShared(new KisCompositeOpResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisEffectiveCompositeOpResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisOpacityResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisFlowResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisSizeResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisLodAvailabilityResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisLodSizeThresholdResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisLodSizeThresholdSupportedResourceConverter)); resourceManager->addDerivedResourceConverter(toQShared(new KisEraserModeResourceConverter)); resourceManager->addResourceUpdateMediator(toQShared(new KisPresetUpdateMediator)); } KActionCollection *KisViewManager::actionCollection() const { return d->actionCollection; } void KisViewManager::slotViewAdded(KisView *view) { // WARNING: this slot is called even when a view from another main windows is added! // Don't expect \p view be a child of this view manager! Q_UNUSED(view); if (viewCount() == 0) { d->statusBar.showAllStatusBarItems(); } } void KisViewManager::slotViewRemoved(KisView *view) { // WARNING: this slot is called even when a view from another main windows is removed! // Don't expect \p view be a child of this view manager! Q_UNUSED(view); if (viewCount() == 0) { d->statusBar.hideAllStatusBarItems(); } KisConfig cfg(false); if (canvasResourceProvider() && canvasResourceProvider()->currentPreset()) { cfg.writeEntry("LastPreset", canvasResourceProvider()->currentPreset()->name()); } } void KisViewManager::setCurrentView(KisView *view) { bool first = true; if (d->currentImageView) { d->currentImageView->notifyCurrentStateChanged(false); d->currentImageView->canvasBase()->setCursor(QCursor(Qt::ArrowCursor)); first = false; KisDocument* doc = d->currentImageView->document(); if (doc) { doc->image()->compositeProgressProxy()->removeProxy(d->persistentImageProgressUpdater); doc->disconnect(this); } d->currentImageView->canvasController()->proxyObject->disconnect(&d->statusBar); d->viewConnections.clear(); } QPointer imageView = qobject_cast(view); d->currentImageView = imageView; if (imageView) { d->softProof->setChecked(imageView->softProofing()); d->gamutCheck->setChecked(imageView->gamutCheck()); // Wait for the async image to have loaded KisDocument* doc = imageView->document(); if (KisConfig(true).readEntry("EnablePositionLabel", false)) { connect(d->currentImageView->canvasController()->proxyObject, SIGNAL(documentMousePositionChanged(QPointF)), &d->statusBar, SLOT(documentMousePositionChanged(QPointF))); } // Restore the last used brush preset, color and background color. if (first) { KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); KisResourceModel *resourceModel = rserver->resourceModel(); QString defaultPresetName = "basic_tip_default"; for (int i = 0; i < resourceModel->rowCount(); i++) { QModelIndex idx = resourceModel->index(i, 0); QString resourceName = idx.data(Qt::UserRole + KisResourceModel::Name).toString().toLower(); QString fileName = idx.data(Qt::UserRole + KisResourceModel::Filename).toString().toLower(); if (resourceName.contains("basic_tip_default")) { defaultPresetName = resourceName; } else if (resourceName.contains("default") || fileName.contains("default")) { defaultPresetName = resourceName; } } KisConfig cfg(true); QString lastPreset = cfg.readEntry("LastPreset", defaultPresetName); KisPaintOpPresetSP preset = rserver->resourceByName(lastPreset); if (!preset) { preset = rserver->resourceByName(defaultPresetName); } if (!preset && rserver->resourceCount() > 0) { preset = rserver->firstResource(); } if (preset) { paintOpBox()->restoreResource(preset); canvasResourceProvider()->setCurrentCompositeOp(preset->settings()->paintOpCompositeOp()); } } KisCanvasController *canvasController = dynamic_cast(d->currentImageView->canvasController()); d->viewConnections.addUniqueConnection(&d->nodeManager, SIGNAL(sigNodeActivated(KisNodeSP)), doc->image(), SLOT(requestStrokeEndActiveNode())); d->viewConnections.addUniqueConnection(d->rotateCanvasRight, SIGNAL(triggered()), canvasController, SLOT(rotateCanvasRight15())); d->viewConnections.addUniqueConnection(d->rotateCanvasLeft, SIGNAL(triggered()),canvasController, SLOT(rotateCanvasLeft15())); d->viewConnections.addUniqueConnection(d->resetCanvasRotation, SIGNAL(triggered()),canvasController, SLOT(resetCanvasRotation())); d->viewConnections.addUniqueConnection(d->wrapAroundAction, SIGNAL(toggled(bool)), canvasController, SLOT(slotToggleWrapAroundMode(bool))); d->wrapAroundAction->setChecked(canvasController->wrapAroundMode()); d->viewConnections.addUniqueConnection(d->levelOfDetailAction, SIGNAL(toggled(bool)), canvasController, SLOT(slotToggleLevelOfDetailMode(bool))); d->levelOfDetailAction->setChecked(canvasController->levelOfDetailMode()); d->viewConnections.addUniqueConnection(d->currentImageView->image(), SIGNAL(sigColorSpaceChanged(const KoColorSpace*)), d->controlFrame.paintopBox(), SLOT(slotColorSpaceChanged(const KoColorSpace*))); d->viewConnections.addUniqueConnection(d->showRulersAction, SIGNAL(toggled(bool)), imageView->zoomManager(), SLOT(setShowRulers(bool))); d->viewConnections.addUniqueConnection(d->rulersTrackMouseAction, SIGNAL(toggled(bool)), imageView->zoomManager(), SLOT(setRulersTrackMouse(bool))); d->viewConnections.addUniqueConnection(d->zoomTo100pct, SIGNAL(triggered()), imageView->zoomManager(), SLOT(zoomTo100())); d->viewConnections.addUniqueConnection(d->zoomIn, SIGNAL(triggered()), imageView->zoomController()->zoomAction(), SLOT(zoomIn())); d->viewConnections.addUniqueConnection(d->zoomOut, SIGNAL(triggered()), imageView->zoomController()->zoomAction(), SLOT(zoomOut())); d->viewConnections.addUniqueConnection(d->softProof, SIGNAL(toggled(bool)), view, SLOT(slotSoftProofing(bool)) ); d->viewConnections.addUniqueConnection(d->gamutCheck, SIGNAL(toggled(bool)), view, SLOT(slotGamutCheck(bool)) ); // set up progrress reporting doc->image()->compositeProgressProxy()->addProxy(d->persistentImageProgressUpdater); d->viewConnections.addUniqueConnection(&d->statusBar, SIGNAL(sigCancellationRequested()), doc->image(), SLOT(requestStrokeCancellation())); d->viewConnections.addUniqueConnection(d->showPixelGrid, SIGNAL(toggled(bool)), canvasController, SLOT(slotTogglePixelGrid(bool))); imageView->zoomManager()->setShowRulers(d->showRulersAction->isChecked()); imageView->zoomManager()->setRulersTrackMouse(d->rulersTrackMouseAction->isChecked()); showHideScrollbars(); } d->filterManager.setView(imageView); d->selectionManager.setView(imageView); d->guidesManager.setView(imageView); d->nodeManager.setView(imageView); d->imageManager.setView(imageView); d->canvasControlsManager.setView(imageView); d->actionManager.setView(imageView); d->gridManager.setView(imageView); d->statusBar.setView(imageView); d->paintingAssistantsManager.setView(imageView); d->mirrorManager.setView(imageView); if (d->currentImageView) { d->currentImageView->notifyCurrentStateChanged(true); d->currentImageView->canvasController()->activate(); d->currentImageView->canvasController()->setFocus(); d->viewConnections.addUniqueConnection( image(), SIGNAL(sigSizeChanged(QPointF,QPointF)), canvasResourceProvider(), SLOT(slotImageSizeChanged())); d->viewConnections.addUniqueConnection( image(), SIGNAL(sigResolutionChanged(double,double)), canvasResourceProvider(), SLOT(slotOnScreenResolutionChanged())); d->viewConnections.addUniqueConnection( image(), SIGNAL(sigNodeChanged(KisNodeSP)), this, SLOT(updateGUI())); d->viewConnections.addUniqueConnection( d->currentImageView->zoomManager()->zoomController(), SIGNAL(zoomChanged(KoZoomMode::Mode,qreal)), canvasResourceProvider(), SLOT(slotOnScreenResolutionChanged())); } d->actionManager.updateGUI(); canvasResourceProvider()->slotImageSizeChanged(); canvasResourceProvider()->slotOnScreenResolutionChanged(); Q_EMIT viewChanged(); } KoZoomController *KisViewManager::zoomController() const { if (d->currentImageView) { return d->currentImageView->zoomController(); } return 0; } KisImageWSP KisViewManager::image() const { if (document()) { return document()->image(); } return 0; } KisCanvasResourceProvider * KisViewManager::canvasResourceProvider() { return &d->canvasResourceProvider; } KisCanvas2 * KisViewManager::canvasBase() const { if (d && d->currentImageView) { return d->currentImageView->canvasBase(); } return 0; } QWidget* KisViewManager::canvas() const { if (d && d->currentImageView && d->currentImageView->canvasBase()->canvasWidget()) { return d->currentImageView->canvasBase()->canvasWidget(); } return 0; } KisStatusBar * KisViewManager::statusBar() const { return &d->statusBar; } KisPaintopBox* KisViewManager::paintOpBox() const { return d->controlFrame.paintopBox(); } QPointer KisViewManager::createUnthreadedUpdater(const QString &name) { return d->persistentUnthreadedProgressUpdaterRouter->startSubtask(1, name, false); } QPointer KisViewManager::createThreadedUpdater(const QString &name) { return d->statusBar.progressUpdater()->startSubtask(1, name, false); } KisSelectionManager * KisViewManager::selectionManager() { return &d->selectionManager; } KisNodeSP KisViewManager::activeNode() { return d->nodeManager.activeNode(); } KisLayerSP KisViewManager::activeLayer() { return d->nodeManager.activeLayer(); } KisPaintDeviceSP KisViewManager::activeDevice() { return d->nodeManager.activePaintDevice(); } KisZoomManager * KisViewManager::zoomManager() { if (d->currentImageView) { return d->currentImageView->zoomManager(); } return 0; } KisFilterManager * KisViewManager::filterManager() { return &d->filterManager; } KisImageManager * KisViewManager::imageManager() { return &d->imageManager; } KisInputManager* KisViewManager::inputManager() const { return &d->inputManager; } KisSelectionSP KisViewManager::selection() { if (d->currentImageView) { return d->currentImageView->selection(); } return 0; } bool KisViewManager::selectionEditable() { KisLayerSP layer = activeLayer(); if (layer) { KisSelectionMaskSP mask = layer->selectionMask(); if (mask) { return mask->isEditable(); } } // global selection is always editable return true; } KisUndoAdapter * KisViewManager::undoAdapter() { if (!document()) return 0; KisImageWSP image = document()->image(); Q_ASSERT(image); return image->undoAdapter(); } void KisViewManager::createActions() { KisConfig cfg(true); d->saveIncremental = actionManager()->createAction("save_incremental_version"); connect(d->saveIncremental, SIGNAL(triggered()), this, SLOT(slotSaveIncremental())); d->saveIncrementalBackup = actionManager()->createAction("save_incremental_backup"); connect(d->saveIncrementalBackup, SIGNAL(triggered()), this, SLOT(slotSaveIncrementalBackup())); connect(mainWindow(), SIGNAL(documentSaved()), this, SLOT(slotDocumentSaved())); d->saveIncremental->setEnabled(false); d->saveIncrementalBackup->setEnabled(false); KisAction *tabletDebugger = actionManager()->createAction("tablet_debugger"); connect(tabletDebugger, SIGNAL(triggered()), this, SLOT(toggleTabletLogger())); d->createTemplate = actionManager()->createAction("create_template"); connect(d->createTemplate, SIGNAL(triggered()), this, SLOT(slotCreateTemplate())); d->createCopy = actionManager()->createAction("create_copy"); connect(d->createCopy, SIGNAL(triggered()), this, SLOT(slotCreateCopy())); d->openResourcesDirectory = actionManager()->createAction("open_resources_directory"); connect(d->openResourcesDirectory, SIGNAL(triggered()), SLOT(openResourcesDirectory())); d->rotateCanvasRight = actionManager()->createAction("rotate_canvas_right"); d->rotateCanvasLeft = actionManager()->createAction("rotate_canvas_left"); d->resetCanvasRotation = actionManager()->createAction("reset_canvas_rotation"); d->wrapAroundAction = actionManager()->createAction("wrap_around_mode"); d->levelOfDetailAction = actionManager()->createAction("level_of_detail_mode"); d->softProof = actionManager()->createAction("softProof"); d->gamutCheck = actionManager()->createAction("gamutCheck"); KisAction *tAction = actionManager()->createAction("showStatusBar"); tAction->setChecked(cfg.showStatusBar()); connect(tAction, SIGNAL(toggled(bool)), this, SLOT(showStatusBar(bool))); tAction = actionManager()->createAction("view_show_canvas_only"); tAction->setChecked(false); connect(tAction, SIGNAL(toggled(bool)), this, SLOT(switchCanvasOnly(bool))); //Workaround, by default has the same shortcut as mirrorCanvas KisAction *a = dynamic_cast(actionCollection()->action("format_italic")); if (a) { a->setDefaultShortcut(QKeySequence()); } actionManager()->createAction("ruler_pixel_multiple2"); d->showRulersAction = actionManager()->createAction("view_ruler"); d->showRulersAction->setChecked(cfg.showRulers()); connect(d->showRulersAction, SIGNAL(toggled(bool)), SLOT(slotSaveShowRulersState(bool))); d->rulersTrackMouseAction = actionManager()->createAction("rulers_track_mouse"); d->rulersTrackMouseAction->setChecked(cfg.rulersTrackMouse()); connect(d->rulersTrackMouseAction, SIGNAL(toggled(bool)), SLOT(slotSaveRulersTrackMouseState(bool))); d->zoomTo100pct = actionManager()->createAction("zoom_to_100pct"); d->zoomIn = actionManager()->createStandardAction(KStandardAction::ZoomIn, 0, ""); d->zoomOut = actionManager()->createStandardAction(KStandardAction::ZoomOut, 0, ""); d->actionAuthor = new KSelectAction(KisIconUtils::loadIcon("im-user"), i18n("Active Author Profile"), this); connect(d->actionAuthor, SIGNAL(triggered(QString)), this, SLOT(changeAuthorProfile(QString))); actionCollection()->addAction("settings_active_author", d->actionAuthor); slotUpdateAuthorProfileActions(); d->showPixelGrid = actionManager()->createAction("view_pixel_grid"); slotUpdatePixelGridAction(); d->toggleFgBg = actionManager()->createAction("toggle_fg_bg"); connect(d->toggleFgBg, SIGNAL(triggered(bool)), this, SLOT(slotToggleFgBg())); d->resetFgBg = actionManager()->createAction("reset_fg_bg"); connect(d->resetFgBg, SIGNAL(triggered(bool)), this, SLOT(slotResetFgBg())); } void KisViewManager::setupManagers() { // Create the managers for filters, selections, layers etc. // XXX: When the currentlayer changes, call updateGUI on all // managers d->filterManager.setup(actionCollection(), actionManager()); d->selectionManager.setup(actionManager()); d->guidesManager.setup(actionManager()); d->nodeManager.setup(actionCollection(), actionManager()); d->imageManager.setup(actionManager()); d->gridManager.setup(actionManager()); d->paintingAssistantsManager.setup(actionManager()); d->canvasControlsManager.setup(actionManager()); d->mirrorManager.setup(actionCollection()); } void KisViewManager::updateGUI() { d->guiUpdateCompressor.start(); } KisNodeManager * KisViewManager::nodeManager() const { return &d->nodeManager; } KisActionManager* KisViewManager::actionManager() const { return &d->actionManager; } KisGridManager * KisViewManager::gridManager() const { return &d->gridManager; } KisGuidesManager * KisViewManager::guidesManager() const { return &d->guidesManager; } KisDocument *KisViewManager::document() const { if (d->currentImageView && d->currentImageView->document()) { return d->currentImageView->document(); } return 0; } int KisViewManager::viewCount() const { KisMainWindow *mw = qobject_cast(d->mainWindow); if (mw) { return mw->viewCount(); } return 0; } bool KisViewManager::KisViewManagerPrivate::blockUntilOperationsFinishedImpl(KisImageSP image, bool force) { const int busyWaitDelay = 1000; KisDelayedSaveDialog dialog(image, !force ? KisDelayedSaveDialog::GeneralDialog : KisDelayedSaveDialog::ForcedDialog, busyWaitDelay, mainWindow); dialog.blockIfImageIsBusy(); return dialog.result() == QDialog::Accepted; } bool KisViewManager::blockUntilOperationsFinished(KisImageSP image) { return d->blockUntilOperationsFinishedImpl(image, false); } void KisViewManager::blockUntilOperationsFinishedForced(KisImageSP image) { d->blockUntilOperationsFinishedImpl(image, true); } void KisViewManager::slotCreateTemplate() { if (!document()) return; KisTemplateCreateDia::createTemplate( QStringLiteral("templates/"), ".kra", document(), mainWindow()); } void KisViewManager::slotCreateCopy() { KisDocument *srcDoc = document(); if (!srcDoc) return; if (!this->blockUntilOperationsFinished(srcDoc->image())) return; KisDocument *doc = 0; { KisImageBarrierLocker l(srcDoc->image()); doc = srcDoc->clone(); } KIS_SAFE_ASSERT_RECOVER_RETURN(doc); QString name = srcDoc->documentInfo()->aboutInfo("name"); if (name.isEmpty()) { name = document()->url().toLocalFile(); } name = i18n("%1 (Copy)", name); doc->documentInfo()->setAboutInfo("title", name); KisPart::instance()->addDocument(doc); KisMainWindow *mw = qobject_cast(d->mainWindow); mw->addViewAndNotifyLoadingCompleted(doc); } QMainWindow* KisViewManager::qtMainWindow() const { if (d->mainWindow) return d->mainWindow; //Fallback for when we have not yet set the main window. QMainWindow* w = qobject_cast(qApp->activeWindow()); if(w) return w; return mainWindow(); } void KisViewManager::setQtMainWindow(QMainWindow* newMainWindow) { d->mainWindow = newMainWindow; } void KisViewManager::slotDocumentSaved() { d->saveIncremental->setEnabled(true); d->saveIncrementalBackup->setEnabled(true); } void KisViewManager::slotSaveIncremental() { if (!document()) return; if (document()->url().isEmpty()) { KisMainWindow *mw = qobject_cast(d->mainWindow); mw->saveDocument(document(), true, false); return; } bool foundVersion; bool fileAlreadyExists; bool isBackup; QString version = "000"; QString newVersion; QString letter; QString fileName = document()->localFilePath(); // Find current version filenames // v v Regexp to find incremental versions in the filename, taking our backup scheme into account as well // Considering our incremental version and backup scheme, format is filename_001~001.ext QRegExp regex("_\\d{1,4}[.]|_\\d{1,4}[a-z][.]|_\\d{1,4}[~]|_\\d{1,4}[a-z][~]"); regex.indexIn(fileName); // Perform the search QStringList matches = regex.capturedTexts(); foundVersion = matches.at(0).isEmpty() ? false : true; // Ensure compatibility with Save Incremental Backup // If this regex is not kept separate, the entire algorithm needs modification; // It's simpler to just add this. QRegExp regexAux("_\\d{1,4}[~]|_\\d{1,4}[a-z][~]"); regexAux.indexIn(fileName); // Perform the search QStringList matchesAux = regexAux.capturedTexts(); isBackup = matchesAux.at(0).isEmpty() ? false : true; // If the filename has a version, prepare it for incrementation if (foundVersion) { version = matches.at(matches.count() - 1); // Look at the last index, we don't care about other matches if (version.contains(QRegExp("[a-z]"))) { version.chop(1); // Trim "." letter = version.right(1); // Save letter version.chop(1); // Trim letter } else { version.chop(1); // Trim "." } version.remove(0, 1); // Trim "_" } else { // TODO: this will not work with files extensions like jp2 // ...else, simply add a version to it so the next loop works QRegExp regex2("[.][a-z]{2,4}$"); // Heuristic to find file extension regex2.indexIn(fileName); QStringList matches2 = regex2.capturedTexts(); QString extensionPlusVersion = matches2.at(0); extensionPlusVersion.prepend(version); extensionPlusVersion.prepend("_"); fileName.replace(regex2, extensionPlusVersion); } // Prepare the base for new version filename int intVersion = version.toInt(0); ++intVersion; QString baseNewVersion = QString::number(intVersion); while (baseNewVersion.length() < version.length()) { baseNewVersion.prepend("0"); } // Check if the file exists under the new name and search until options are exhausted (test appending a to z) do { newVersion = baseNewVersion; newVersion.prepend("_"); if (!letter.isNull()) newVersion.append(letter); if (isBackup) { newVersion.append("~"); } else { newVersion.append("."); } fileName.replace(regex, newVersion); fileAlreadyExists = QFile(fileName).exists(); if (fileAlreadyExists) { if (!letter.isNull()) { char letterCh = letter.at(0).toLatin1(); ++letterCh; letter = QString(QChar(letterCh)); } else { letter = 'a'; } } } while (fileAlreadyExists && letter != "{"); // x, y, z, {... if (letter == "{") { QMessageBox::critical(mainWindow(), i18nc("@title:window", "Couldn't save incremental version"), i18n("Alternative names exhausted, try manually saving with a higher number")); return; } + QUrl newUrl = QUrl::fromUserInput(fileName); document()->setFileBatchMode(true); - document()->saveAs(QUrl::fromUserInput(fileName), document()->mimeType(), true); + document()->saveAs(newUrl, document()->mimeType(), true); document()->setFileBatchMode(false); + KisPart::instance()->addRecentURLToAllMainWindows(newUrl, document()->url()); if (mainWindow()) { mainWindow()->updateCaption(); } } void KisViewManager::slotSaveIncrementalBackup() { if (!document()) return; if (document()->url().isEmpty()) { KisMainWindow *mw = qobject_cast(d->mainWindow); mw->saveDocument(document(), true, false); return; } bool workingOnBackup; bool fileAlreadyExists; QString version = "000"; QString newVersion; QString letter; QString fileName = document()->localFilePath(); // First, discover if working on a backup file, or a normal file QRegExp regex("~\\d{1,4}[.]|~\\d{1,4}[a-z][.]"); regex.indexIn(fileName); // Perform the search QStringList matches = regex.capturedTexts(); workingOnBackup = matches.at(0).isEmpty() ? false : true; if (workingOnBackup) { // Try to save incremental version (of backup), use letter for alt versions version = matches.at(matches.count() - 1); // Look at the last index, we don't care about other matches if (version.contains(QRegExp("[a-z]"))) { version.chop(1); // Trim "." letter = version.right(1); // Save letter version.chop(1); // Trim letter } else { version.chop(1); // Trim "." } version.remove(0, 1); // Trim "~" // Prepare the base for new version filename int intVersion = version.toInt(0); ++intVersion; QString baseNewVersion = QString::number(intVersion); QString backupFileName = document()->localFilePath(); while (baseNewVersion.length() < version.length()) { baseNewVersion.prepend("0"); } // Check if the file exists under the new name and search until options are exhausted (test appending a to z) do { newVersion = baseNewVersion; newVersion.prepend("~"); if (!letter.isNull()) newVersion.append(letter); newVersion.append("."); backupFileName.replace(regex, newVersion); fileAlreadyExists = QFile(backupFileName).exists(); if (fileAlreadyExists) { if (!letter.isNull()) { char letterCh = letter.at(0).toLatin1(); ++letterCh; letter = QString(QChar(letterCh)); } else { letter = 'a'; } } } while (fileAlreadyExists && letter != "{"); // x, y, z, {... if (letter == "{") { QMessageBox::critical(mainWindow(), i18nc("@title:window", "Couldn't save incremental backup"), i18n("Alternative names exhausted, try manually saving with a higher number")); return; } QFile::copy(fileName, backupFileName); document()->saveAs(QUrl::fromUserInput(fileName), document()->mimeType(), true); if (mainWindow()) mainWindow()->updateCaption(); } else { // if NOT working on a backup... // Navigate directory searching for latest backup version, ignore letters const quint8 HARDCODED_DIGIT_COUNT = 3; QString baseNewVersion = "000"; QString backupFileName = document()->localFilePath(); QRegExp regex2("[.][a-z]{2,4}$"); // Heuristic to find file extension regex2.indexIn(backupFileName); QStringList matches2 = regex2.capturedTexts(); QString extensionPlusVersion = matches2.at(0); extensionPlusVersion.prepend(baseNewVersion); extensionPlusVersion.prepend("~"); backupFileName.replace(regex2, extensionPlusVersion); // Save version with 1 number higher than the highest version found ignoring letters do { newVersion = baseNewVersion; newVersion.prepend("~"); newVersion.append("."); backupFileName.replace(regex, newVersion); fileAlreadyExists = QFile(backupFileName).exists(); if (fileAlreadyExists) { // Prepare the base for new version filename, increment by 1 int intVersion = baseNewVersion.toInt(0); ++intVersion; baseNewVersion = QString::number(intVersion); while (baseNewVersion.length() < HARDCODED_DIGIT_COUNT) { baseNewVersion.prepend("0"); } } } while (fileAlreadyExists); // Save both as backup and on current file for interapplication workflow document()->setFileBatchMode(true); QFile::copy(fileName, backupFileName); document()->saveAs(QUrl::fromUserInput(fileName), document()->mimeType(), true); document()->setFileBatchMode(false); if (mainWindow()) mainWindow()->updateCaption(); } } void KisViewManager::disableControls() { // prevents possible crashes, if somebody changes the paintop during dragging by using the mousewheel // this is for Bug 250944 // the solution blocks all wheel, mouse and key event, while dragging with the freehand tool // see KisToolFreehand::initPaint() and endPaint() d->controlFrame.paintopBox()->installEventFilter(&d->blockingEventFilter); Q_FOREACH (QObject* child, d->controlFrame.paintopBox()->children()) { child->installEventFilter(&d->blockingEventFilter); } } void KisViewManager::enableControls() { d->controlFrame.paintopBox()->removeEventFilter(&d->blockingEventFilter); Q_FOREACH (QObject* child, d->controlFrame.paintopBox()->children()) { child->removeEventFilter(&d->blockingEventFilter); } } void KisViewManager::showStatusBar(bool toggled) { KisMainWindow *mw = mainWindow(); if(mw && mw->statusBar()) { mw->statusBar()->setVisible(toggled); KisConfig cfg(false); cfg.setShowStatusBar(toggled); } } void KisViewManager::switchCanvasOnly(bool toggled) { KisConfig cfg(false); KisMainWindow *main = mainWindow(); if(!main) { dbgUI << "Unable to switch to canvas-only mode, main window not found"; return; } cfg.writeEntry("CanvasOnlyActive", toggled); if (toggled) { d->canvasState = qtMainWindow()->saveState(); #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) d->windowFlags = main->windowState(); #endif } if (cfg.hideStatusbarFullscreen()) { if (main->statusBar()) { if (!toggled) { if (main->statusBar()->dynamicPropertyNames().contains("wasvisible")) { if (main->statusBar()->property("wasvisible").toBool()) { main->statusBar()->setVisible(true); } } } else { main->statusBar()->setProperty("wasvisible", main->statusBar()->isVisible()); main->statusBar()->setVisible(false); } } } if (cfg.hideDockersFullscreen()) { KisAction* action = qobject_cast(main->actionCollection()->action("view_toggledockers")); if (action) { action->setCheckable(true); if (toggled) { if (action->isChecked()) { cfg.setShowDockers(action->isChecked()); action->setChecked(false); } else { cfg.setShowDockers(false); } } else { action->setChecked(cfg.showDockers()); } } } // QT in windows does not return to maximized upon 4th tab in a row // https://bugreports.qt.io/browse/QTBUG-57882, https://bugreports.qt.io/browse/QTBUG-52555, https://codereview.qt-project.org/#/c/185016/ if (cfg.hideTitlebarFullscreen() && !cfg.fullscreenMode()) { if(toggled) { main->setWindowState( main->windowState() | Qt::WindowFullScreen); } else { main->setWindowState( main->windowState() & ~Qt::WindowFullScreen); #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) // If window was maximized prior to fullscreen, restore that if (d->windowFlags & Qt::WindowMaximized) { main->setWindowState( main->windowState() | Qt::WindowMaximized); } #endif } } if (cfg.hideMenuFullscreen()) { if (!toggled) { if (main->menuBar()->dynamicPropertyNames().contains("wasvisible")) { if (main->menuBar()->property("wasvisible").toBool()) { main->menuBar()->setVisible(true); } } } else { main->menuBar()->setProperty("wasvisible", main->menuBar()->isVisible()); main->menuBar()->setVisible(false); } } if (cfg.hideToolbarFullscreen()) { QList toolBars = main->findChildren(); Q_FOREACH (QToolBar* toolbar, toolBars) { if (!toggled) { if (toolbar->dynamicPropertyNames().contains("wasvisible")) { if (toolbar->property("wasvisible").toBool()) { toolbar->setVisible(true); } } } else { toolbar->setProperty("wasvisible", toolbar->isVisible()); toolbar->setVisible(false); } } } showHideScrollbars(); if (toggled) { // show a fading heads-up display about the shortcut to go back showFloatingMessage(i18n("Going into Canvas-Only mode.\nPress %1 to go back.", actionCollection()->action("view_show_canvas_only")->shortcut().toString()), QIcon()); } else { main->restoreState(d->canvasState); } } void KisViewManager::toggleTabletLogger() { d->inputManager.toggleTabletLogger(); } void KisViewManager::openResourcesDirectory() { QString dir = KoResourcePaths::locateLocal("data", ""); QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void KisViewManager::updateIcons() { if (mainWindow()) { QList dockers = mainWindow()->dockWidgets(); Q_FOREACH (QDockWidget* dock, dockers) { QObjectList objects; objects.append(dock); while (!objects.isEmpty()) { QObject* object = objects.takeFirst(); objects.append(object->children()); KisIconUtils::updateIconCommon(object); } } } } void KisViewManager::initializeStatusBarVisibility() { KisConfig cfg(true); d->mainWindow->statusBar()->setVisible(cfg.showStatusBar()); } void KisViewManager::guiUpdateTimeout() { d->nodeManager.updateGUI(); d->selectionManager.updateGUI(); d->filterManager.updateGUI(); if (zoomManager()) { zoomManager()->updateGUI(); } d->gridManager.updateGUI(); d->actionManager.updateGUI(); } void KisViewManager::showFloatingMessage(const QString &message, const QIcon& icon, int timeout, KisFloatingMessage::Priority priority, int alignment) { if (!d->currentImageView) return; d->currentImageView->showFloatingMessage(message, icon, timeout, priority, alignment); emit floatingMessageRequested(message, icon.name()); } KisMainWindow *KisViewManager::mainWindow() const { return qobject_cast(d->mainWindow); } void KisViewManager::showHideScrollbars() { if (!d->currentImageView) return; if (!d->currentImageView->canvasController()) return; KisConfig cfg(true); bool toggled = actionCollection()->action("view_show_canvas_only")->isChecked(); if ( (toggled && cfg.hideScrollbarsFullscreen()) || (!toggled && cfg.hideScrollbars()) ) { d->currentImageView->canvasController()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); d->currentImageView->canvasController()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } else { d->currentImageView->canvasController()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); d->currentImageView->canvasController()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); } } void KisViewManager::slotSaveShowRulersState(bool value) { KisConfig cfg(false); cfg.setShowRulers(value); } void KisViewManager::slotSaveRulersTrackMouseState(bool value) { KisConfig cfg(false); cfg.setRulersTrackMouse(value); } void KisViewManager::setShowFloatingMessage(bool show) { d->showFloatingMessage = show; } void KisViewManager::changeAuthorProfile(const QString &profileName) { KConfigGroup appAuthorGroup(KSharedConfig::openConfig(), "Author"); if (profileName.isEmpty() || profileName == i18nc("choice for author profile", "Anonymous")) { appAuthorGroup.writeEntry("active-profile", ""); } else { appAuthorGroup.writeEntry("active-profile", profileName); } appAuthorGroup.sync(); Q_FOREACH (KisDocument *doc, KisPart::instance()->documents()) { doc->documentInfo()->updateParameters(); } } void KisViewManager::slotUpdateAuthorProfileActions() { Q_ASSERT(d->actionAuthor); if (!d->actionAuthor) { return; } d->actionAuthor->clear(); d->actionAuthor->addAction(i18nc("choice for author profile", "Anonymous")); KConfigGroup authorGroup(KSharedConfig::openConfig(), "Author"); QStringList profiles = authorGroup.readEntry("profile-names", QStringList()); QString authorInfo = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/authorinfo/"; QStringList filters = QStringList() << "*.authorinfo"; QDir dir(authorInfo); Q_FOREACH(QString entry, dir.entryList(filters)) { int ln = QString(".authorinfo").size(); entry.chop(ln); if (!profiles.contains(entry)) { profiles.append(entry); } } Q_FOREACH (const QString &profile , profiles) { d->actionAuthor->addAction(profile); } KConfigGroup appAuthorGroup(KSharedConfig::openConfig(), "Author"); QString profileName = appAuthorGroup.readEntry("active-profile", ""); if (profileName == "anonymous" || profileName.isEmpty()) { d->actionAuthor->setCurrentItem(0); } else if (profiles.contains(profileName)) { d->actionAuthor->setCurrentAction(profileName); } } void KisViewManager::slotUpdatePixelGridAction() { KIS_SAFE_ASSERT_RECOVER_RETURN(d->showPixelGrid); KisSignalsBlocker b(d->showPixelGrid); KisConfig cfg(true); d->showPixelGrid->setChecked(cfg.pixelGridEnabled() && cfg.useOpenGL()); } void KisViewManager::slotActivateTransformTool() { if(KoToolManager::instance()->activeToolId() == "KisToolTransform") { KoToolBase* tool = KoToolManager::instance()->toolById(canvasBase(), "KisToolTransform"); QSet dummy; // Start a new stroke tool->deactivate(); tool->activate(KoToolBase::DefaultActivation, dummy); } KoToolManager::instance()->switchToolRequested("KisToolTransform"); } void KisViewManager::slotToggleFgBg() { KoColor newFg = d->canvasResourceManager.backgroundColor(); KoColor newBg = d->canvasResourceManager.foregroundColor(); /** * NOTE: Some of color selectors do not differentiate foreground * and background colors, so if one wants them to end up * being set up to foreground color, it should be set the * last. */ d->canvasResourceManager.setBackgroundColor(newBg); d->canvasResourceManager.setForegroundColor(newFg); } void KisViewManager::slotResetFgBg() { // see a comment in slotToggleFgBg() d->canvasResourceManager.setBackgroundColor(KoColor(Qt::white, KoColorSpaceRegistry::instance()->rgb8())); d->canvasResourceManager.setForegroundColor(KoColor(Qt::black, KoColorSpaceRegistry::instance()->rgb8())); } void KisViewManager::slotResetRotation() { KisCanvasController *canvasController = d->currentImageView->canvasController(); canvasController->resetCanvasRotation(); } void KisViewManager::slotToggleFullscreen() { KisConfig cfg(false); KisMainWindow *main = mainWindow(); main->viewFullscreen(!main->isFullScreen()); cfg.fullscreenMode(main->isFullScreen()); } diff --git a/libs/ui/widgets/KoFillConfigWidget.cpp b/libs/ui/widgets/KoFillConfigWidget.cpp index 43a444d34f..05af46ee0c 100644 --- a/libs/ui/widgets/KoFillConfigWidget.cpp +++ b/libs/ui/widgets/KoFillConfigWidget.cpp @@ -1,913 +1,919 @@ /* This file is part of the KDE project * Made by Tomislav Lukman (tomislav.lukman@ck.tel.hr) * Copyright (C) 2012 Jean-Nicolas Artaud * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoFillConfigWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoResourceServerProvider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoZoomHandler.h" #include "KoColorPopupButton.h" #include "ui_KoFillConfigWidget.h" #include #include #include #include #include "kis_canvas_resource_provider.h" #include #include #include #include "kis_global.h" #include "kis_debug.h" static const char* const buttonnone[]={ "16 16 3 1", "# c #000000", "e c #ff0000", "- c #ffffff", "################", "#--------------#", "#-e----------e-#", "#--e--------e--#", "#---e------e---#", "#----e----e----#", "#-----e--e-----#", "#------ee------#", "#------ee------#", "#-----e--e-----#", "#----e----e----#", "#---e------e---#", "#--e--------e--#", "#-e----------e-#", "#--------------#", "################"}; static const char* const buttonsolid[]={ "16 16 2 1", "# c #000000", ". c #969696", "################", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "#..............#", "################"}; // FIXME: Smoother gradient button. static const char* const buttongradient[]={ "16 16 15 1", "# c #000000", "n c #101010", "m c #202020", "l c #303030", "k c #404040", "j c #505050", "i c #606060", "h c #707070", "g c #808080", "f c #909090", "e c #a0a0a0", "d c #b0b0b0", "c c #c0c0c0", "b c #d0d0d0", "a c #e0e0e0", "################", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "#abcdefghijklmn#", "################"}; static const char* const buttonpattern[]={ "16 16 4 1", ". c #0a0a0a", "# c #333333", "a c #a0a0a0", "b c #ffffffff", "################", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#bbbbbaaaabbbbb#", "#bbbbbaaaabbbbb#", "#bbbbbaaaabbbbb#", "#bbbbbaaaabbbbb#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "#aaaaabbbbaaaaa#", "################"}; class Q_DECL_HIDDEN KoFillConfigWidget::Private { public: Private(KoFlake::FillVariant _fillVariant) : canvas(0), colorChangedCompressor(100, KisSignalCompressor::FIRST_ACTIVE), gradientChangedCompressor(100, KisSignalCompressor::FIRST_ACTIVE), shapeChangedCompressor(200,KisSignalCompressor::FIRST_ACTIVE), fillVariant(_fillVariant), noSelectionTrackingMode(false) { } KoColorPopupAction *colorAction; KoResourcePopupAction *gradientAction; KoResourcePopupAction *patternAction; QButtonGroup *group; KoCanvasBase *canvas; KisSignalCompressor colorChangedCompressor; KisAcyclicSignalConnector shapeChangedAcyclicConnector; KisAcyclicSignalConnector resourceManagerAcyclicConnector; KoFillConfigWidget::StyleButton selectedFillIndex {KoFillConfigWidget::None}; KoStopGradientSP activeGradient; KisSignalCompressor gradientChangedCompressor; KisSignalCompressor shapeChangedCompressor; KoFlake::FillVariant fillVariant; bool noSelectionTrackingMode; Ui_KoFillConfigWidget *ui; std::vector deactivationLocks; boost::optional overriddenColorFromProvider; }; KoFillConfigWidget::KoFillConfigWidget(KoCanvasBase *canvas, KoFlake::FillVariant fillVariant, bool trackShapeSelection, QWidget *parent) : QWidget(parent) , d(new Private(fillVariant)) { d->canvas = canvas; if (trackShapeSelection) { d->shapeChangedAcyclicConnector.connectBackwardVoid( d->canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), &d->shapeChangedCompressor, SLOT(start())); d->shapeChangedAcyclicConnector.connectBackwardVoid( d->canvas->selectedShapesProxy(), SIGNAL(selectionContentChanged()), &d->shapeChangedCompressor, SLOT(start())); connect(&d->shapeChangedCompressor, SIGNAL(timeout()), this, SLOT(shapeChanged())); } d->resourceManagerAcyclicConnector.connectBackwardResourcePair( d->canvas->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)), this, SLOT(slotCanvasResourceChanged(int,QVariant))); d->resourceManagerAcyclicConnector.connectForwardVoid( this, SIGNAL(sigInternalRequestColorToResourceManager()), this, SLOT(slotProposeCurrentColorToResourceManager())); KisAcyclicSignalConnector *resetConnector = d->resourceManagerAcyclicConnector.createCoordinatedConnector(); resetConnector->connectForwardVoid( this, SIGNAL(sigInternalRecoverColorInResourceManager()), this, SLOT(slotRecoverColorInResourceManager())); // configure GUI d->ui = new Ui_KoFillConfigWidget(); d->ui->setupUi(this); d->group = new QButtonGroup(this); d->group->setExclusive(true); d->ui->btnNoFill->setIcon(QPixmap((const char **) buttonnone)); d->group->addButton(d->ui->btnNoFill, None); d->ui->btnSolidFill->setIcon(QPixmap((const char **) buttonsolid)); d->group->addButton(d->ui->btnSolidFill, Solid); d->ui->btnGradientFill->setIcon(QPixmap((const char **) buttongradient)); d->group->addButton(d->ui->btnGradientFill, Gradient); d->ui->btnPatternFill->setIcon(QPixmap((const char **) buttonpattern)); d->group->addButton(d->ui->btnPatternFill, Pattern); d->ui->btnPatternFill->setVisible(false); d->colorAction = new KoColorPopupAction(d->ui->btnChooseSolidColor); d->colorAction->setToolTip(i18n("Change the filling color")); d->colorAction->setCurrentColor(Qt::white); d->ui->btnChooseSolidColor->setDefaultAction(d->colorAction); d->ui->btnChooseSolidColor->setPopupMode(QToolButton::InstantPopup); d->ui->btnSolidColorPick->setIcon(KisIconUtils::loadIcon("krita_tool_color_picker")); // TODO: for now the color picking button is disabled! d->ui->btnSolidColorPick->setEnabled(false); d->ui->btnSolidColorPick->setVisible(false); connect(d->colorAction, SIGNAL(colorChanged(KoColor)), &d->colorChangedCompressor, SLOT(start())); connect(&d->colorChangedCompressor, SIGNAL(timeout()), SLOT(colorChanged())); connect(d->ui->btnChooseSolidColor, SIGNAL(iconSizeChanged()), d->colorAction, SLOT(updateIcon())); connect(d->group, SIGNAL(buttonClicked(int)), SLOT(styleButtonPressed(int))); connect(d->group, SIGNAL(buttonClicked(int)), SLOT(slotUpdateFillTitle())); slotUpdateFillTitle(); styleButtonPressed(d->group->checkedId()); // Gradient selector d->ui->wdgGradientEditor->setCompactMode(true); connect(d->ui->wdgGradientEditor, SIGNAL(sigGradientChanged()), &d->gradientChangedCompressor, SLOT(start())); connect(&d->gradientChangedCompressor, SIGNAL(timeout()), SLOT(activeGradientChanged())); d->gradientAction = new KoResourcePopupAction(ResourceType::Gradients, d->ui->btnChoosePredefinedGradient); d->gradientAction->setToolTip(i18n("Change filling gradient")); d->ui->btnChoosePredefinedGradient->setDefaultAction(d->gradientAction); d->ui->btnChoosePredefinedGradient->setPopupMode(QToolButton::InstantPopup); connect(d->gradientAction, SIGNAL(resourceSelected(QSharedPointer)), SLOT(gradientResourceChanged())); connect(d->ui->btnChoosePredefinedGradient, SIGNAL(iconSizeChanged()), d->gradientAction, SLOT(updateIcon())); d->ui->btnSaveGradient->setIcon(KisIconUtils::loadIcon("document-save")); connect(d->ui->btnSaveGradient, SIGNAL(clicked()), SLOT(slotSavePredefinedGradientClicked())); connect(d->ui->cmbGradientRepeat, SIGNAL(currentIndexChanged(int)), SLOT(slotGradientRepeatChanged())); connect(d->ui->cmbGradientType, SIGNAL(currentIndexChanged(int)), SLOT(slotGradientTypeChanged())); - deactivate(); + // initialize deactivation locks + d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->shapeChangedAcyclicConnector)); + d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->resourceManagerAcyclicConnector)); + /* // Pattern selector d->patternAction = new KoResourcePopupAction(ResourceType::Patterns, d->colorButton); d->patternAction->setToolTip(i18n("Change the filling pattern")); connect(d->patternAction, SIGNAL(resourceSelected(QSharedPointer)), this, SLOT(patternChanged(QSharedPointer))); connect(d->colorButton, SIGNAL(iconSizeChanged()), d->patternAction, SLOT(updateIcon())); */ } KoFillConfigWidget::~KoFillConfigWidget() { delete d; } void KoFillConfigWidget::activate() { + KIS_SAFE_ASSERT_RECOVER_NOOP(!d->deactivationLocks.empty()); d->deactivationLocks.clear(); + if (!d->noSelectionTrackingMode) { d->shapeChangedCompressor.start(); } else { loadCurrentFillFromResourceServer(); } updateWidgetComponentVisbility(); } void KoFillConfigWidget::deactivate() { emit sigInternalRecoverColorInResourceManager(); - d->deactivationLocks.clear(); + + KIS_SAFE_ASSERT_RECOVER_NOOP(d->deactivationLocks.empty()); d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->shapeChangedAcyclicConnector)); d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->resourceManagerAcyclicConnector)); } void KoFillConfigWidget::forceUpdateOnSelectionChanged() { d->shapeChangedCompressor.start(); } void KoFillConfigWidget::setNoSelectionTrackingMode(bool value) { d->noSelectionTrackingMode = value; if (!d->noSelectionTrackingMode) { d->shapeChangedCompressor.start(); } } void KoFillConfigWidget::slotUpdateFillTitle() { QString text = d->group->checkedButton() ? d->group->checkedButton()->text() : QString(); text.replace('&', QString()); d->ui->lblFillTitle->setText(text); } void KoFillConfigWidget::slotCanvasResourceChanged(int key, const QVariant &value) { if ((key == KoCanvasResourceProvider::ForegroundColor && d->fillVariant == KoFlake::Fill) || (key == KoCanvasResourceProvider::BackgroundColor && d->fillVariant == KoFlake::StrokeFill && !d->noSelectionTrackingMode) || (key == KoCanvasResourceProvider::ForegroundColor && d->noSelectionTrackingMode)) { KoColor color = value.value(); const int checkedId = d->group->checkedId(); if ((checkedId < 0 || checkedId == None || checkedId == Solid) && !(checkedId == Solid && d->colorAction->currentKoColor() == color)) { d->group->button(Solid)->setChecked(true); d->selectedFillIndex = Solid; d->colorAction->setCurrentColor(color); d->colorChangedCompressor.start(); } else if (checkedId == Gradient && key == KoCanvasResourceProvider::ForegroundColor) { d->ui->wdgGradientEditor->notifyGlobalColorChanged(color); } } else if (key == KisCanvasResourceProvider::CurrentGradient) { KoResourceSP gradient = value.value(); const int checkedId = d->group->checkedId(); if (gradient && (checkedId < 0 || checkedId == None || checkedId == Gradient)) { d->group->button(Gradient)->setChecked(true); d->gradientAction->setCurrentResource(gradient); } } } QList KoFillConfigWidget::currentShapes() { return d->canvas->selectedShapesProxy()->selection()->selectedEditableShapes(); } int KoFillConfigWidget::selectedFillIndex() { return d->selectedFillIndex; } void KoFillConfigWidget::styleButtonPressed(int buttonId) { QList shapes = currentShapes(); switch (buttonId) { case KoFillConfigWidget::None: noColorSelected(); break; case KoFillConfigWidget::Solid: colorChanged(); break; case KoFillConfigWidget::Gradient: if (d->activeGradient) { setNewGradientBackgroundToShape(); updateGradientSaveButtonAvailability(); } else { gradientResourceChanged(); } break; case KoFillConfigWidget::Pattern: // Only select mode in the widget, don't set actual pattern :/ //d->colorButton->setDefaultAction(d->patternAction); //patternChanged(d->patternAction->currentBackground()); break; } // update tool option fields with first selected object if (shapes.isEmpty() == false) { KoShape *firstShape = shapes.first(); updateFillIndexFromShape(firstShape); updateFillColorFromShape(firstShape); } updateWidgetComponentVisbility(); } KoShapeStrokeSP KoFillConfigWidget::createShapeStroke() { KoShapeStrokeSP stroke(new KoShapeStroke()); KIS_ASSERT_RECOVER_RETURN_VALUE(d->fillVariant == KoFlake::StrokeFill, stroke); switch (d->group->checkedId()) { case KoFillConfigWidget::None: stroke->setColor(Qt::transparent); break; case KoFillConfigWidget::Solid: stroke->setColor(d->colorAction->currentColor()); break; case KoFillConfigWidget::Gradient: { QScopedPointer g(d->activeGradient->toQGradient()); QBrush newBrush = *g; stroke->setLineBrush(newBrush); stroke->setColor(Qt::transparent); break; } case KoFillConfigWidget::Pattern: break; } return stroke; } void KoFillConfigWidget::noColorSelected() { KisAcyclicSignalConnector::Blocker b(d->shapeChangedAcyclicConnector); QList selectedShapes = currentShapes(); if (selectedShapes.isEmpty()) { emit sigFillChanged(); return; } KoShapeFillWrapper wrapper(selectedShapes, d->fillVariant); KUndo2Command *command = wrapper.setColor(QColor()); if (command) { d->canvas->addCommand(command); } if (d->fillVariant == KoFlake::StrokeFill) { KUndo2Command *lineCommand = wrapper.setLineWidth(0.0); if (lineCommand) { d->canvas->addCommand(lineCommand); } } emit sigFillChanged(); } void KoFillConfigWidget::colorChanged() { KisAcyclicSignalConnector::Blocker b(d->shapeChangedAcyclicConnector); QList selectedShapes = currentShapes(); if (selectedShapes.isEmpty()) { emit sigInternalRequestColorToResourceManager(); emit sigFillChanged(); return; } d->overriddenColorFromProvider = boost::none; KoShapeFillWrapper wrapper(selectedShapes, d->fillVariant); KUndo2Command *command = wrapper.setColor(d->colorAction->currentColor()); if (command) { d->canvas->addCommand(command); } // only returns true if it is a stroke object that has a 0 for line width if (wrapper.hasZeroLineWidth() ) { KUndo2Command *lineCommand = wrapper.setLineWidth(1.0); if (lineCommand) { d->canvas->addCommand(lineCommand); } // * line to test out QColor solidColor = d->colorAction->currentColor(); solidColor.setAlpha(255); command = wrapper.setColor(solidColor); if (command) { d->canvas->addCommand(command); } } d->colorAction->setCurrentColor(wrapper.color()); emit sigFillChanged(); emit sigInternalRequestColorToResourceManager(); } void KoFillConfigWidget::slotProposeCurrentColorToResourceManager() { const int checkedId = d->group->checkedId(); bool hasColor = false; KoColor color; KoCanvasResourceProvider::CanvasResource colorSlot = KoCanvasResourceProvider::ForegroundColor; if (checkedId == Solid) { if (d->fillVariant == KoFlake::StrokeFill) { colorSlot = KoCanvasResourceProvider::BackgroundColor; } color = d->colorAction->currentKoColor(); hasColor = true; } else if (checkedId == Gradient) { if (boost::optional gradientColor = d->ui->wdgGradientEditor->currentActiveStopColor()) { color = *gradientColor; hasColor = true; } } if (hasColor) { if (!d->overriddenColorFromProvider) { d->overriddenColorFromProvider = d->canvas->resourceManager()->resource(colorSlot).value(); } /** * Don't let opacity leak to our resource manager system * * NOTE: theoretically, we could guarantee it on a level of the * resource manager itself, */ color.setOpacity(OPACITY_OPAQUE_U8); d->canvas->resourceManager()->setResource(colorSlot, QVariant::fromValue(color)); } } void KoFillConfigWidget::slotRecoverColorInResourceManager() { if (d->overriddenColorFromProvider) { KoCanvasResourceProvider::CanvasResource colorSlot = KoCanvasResourceProvider::ForegroundColor; if (d->fillVariant == KoFlake::StrokeFill) { colorSlot = KoCanvasResourceProvider::BackgroundColor; } d->canvas->resourceManager()->setResource(colorSlot, QVariant::fromValue(*d->overriddenColorFromProvider)); d->overriddenColorFromProvider = boost::none; } } void KoFillConfigWidget::slotSavePredefinedGradientClicked() { KoResourceServerProvider *serverProvider = KoResourceServerProvider::instance(); auto server = serverProvider->gradientServer(); const QString defaultGradientNamePrefix = i18nc("default prefix for the saved gradient", "gradient"); const QString saveLocation = server->saveLocation(); QString name = d->activeGradient->name().isEmpty() ? defaultGradientNamePrefix : d->activeGradient->name(); QFileInfo fileInfo(saveLocation + name.split(" ").join("_") + d->activeGradient->defaultFileExtension()); bool fileOverWriteAccepted = false; while(!fileOverWriteAccepted) { name = QInputDialog::getText(this, i18nc("@title:window", "Save Gradient"), i18n("Enter gradient name:"), QLineEdit::Normal, name); if (name.isNull() || name.isEmpty()) { return; } else { fileInfo = QFileInfo(saveLocation + name.split(" ").join("_") + d->activeGradient->defaultFileExtension()); if (fileInfo.exists()) { int res = QMessageBox::warning(this, i18nc("@title:window", "Name Already Exists") , i18n("The name '%1' already exists, do you wish to overwrite it?", name) , QMessageBox::Yes | QMessageBox::No); if (res == QMessageBox::Yes) fileOverWriteAccepted = true; } else { fileOverWriteAccepted = true; } } } d->activeGradient->setName(name); d->activeGradient->setFilename(name.split(" ").join("_") + d->activeGradient->defaultFileExtension()); KoAbstractGradientSP newGradient = d->activeGradient->clone().dynamicCast(); server->addResource(newGradient); d->gradientAction->setCurrentResource(newGradient); } void KoFillConfigWidget::activeGradientChanged() { setNewGradientBackgroundToShape(); updateGradientSaveButtonAvailability(); emit sigInternalRequestColorToResourceManager(); } void KoFillConfigWidget::gradientResourceChanged() { QSharedPointer bg = qSharedPointerDynamicCast( d->gradientAction->currentBackground()); uploadNewGradientBackground(bg->gradient()); setNewGradientBackgroundToShape(); updateGradientSaveButtonAvailability(); } void KoFillConfigWidget::slotGradientTypeChanged() { QGradient::Type type = d->ui->cmbGradientType->currentIndex() == 0 ? QGradient::LinearGradient : QGradient::RadialGradient; d->activeGradient->setType(type); activeGradientChanged(); } void KoFillConfigWidget::slotGradientRepeatChanged() { QGradient::Spread spread = QGradient::Spread(d->ui->cmbGradientRepeat->currentIndex()); d->activeGradient->setSpread(spread); activeGradientChanged(); } void KoFillConfigWidget::uploadNewGradientBackground(const QGradient *gradient) { KisSignalsBlocker b1(d->ui->wdgGradientEditor, d->ui->cmbGradientType, d->ui->cmbGradientRepeat); d->ui->wdgGradientEditor->setGradient(0); d->activeGradient = KoStopGradient::fromQGradient(gradient); d->ui->wdgGradientEditor->setGradient(d->activeGradient); d->ui->cmbGradientType->setCurrentIndex(d->activeGradient->type() != QGradient::LinearGradient); d->ui->cmbGradientRepeat->setCurrentIndex(int(d->activeGradient->spread())); } void KoFillConfigWidget::setNewGradientBackgroundToShape() { QList selectedShapes = currentShapes(); if (selectedShapes.isEmpty()) { emit sigFillChanged(); return; } KisAcyclicSignalConnector::Blocker b(d->shapeChangedAcyclicConnector); KoShapeFillWrapper wrapper(selectedShapes, d->fillVariant); QScopedPointer srcQGradient(d->activeGradient->toQGradient()); KUndo2Command *command = wrapper.applyGradientStopsOnly(srcQGradient.data()); if (command) { d->canvas->addCommand(command); } emit sigFillChanged(); } void KoFillConfigWidget::updateGradientSaveButtonAvailability() { bool savingEnabled = false; QScopedPointer currentGradient(d->activeGradient->toQGradient()); QSharedPointer bg = d->gradientAction->currentBackground(); if (bg) { QSharedPointer resourceBackground = qSharedPointerDynamicCast(bg); savingEnabled = resourceBackground->gradient()->stops() != currentGradient->stops(); savingEnabled |= resourceBackground->gradient()->type() != currentGradient->type(); savingEnabled |= resourceBackground->gradient()->spread() != currentGradient->spread(); } d->ui->btnSaveGradient->setEnabled(savingEnabled); } void KoFillConfigWidget::patternChanged(QSharedPointer background) { Q_UNUSED(background); #if 0 QSharedPointer patternBackground = qSharedPointerDynamicCast(background); if (! patternBackground) { return; } QList selectedShapes = currentShapes(); if (selectedShapes.isEmpty()) { return; } KoImageCollection *imageCollection = d->canvas->shapeController()->resourceManager()->imageCollection(); if (imageCollection) { QSharedPointer fill(new KoPatternBackground(imageCollection)); fill->setPattern(patternBackground->pattern()); d->canvas->addCommand(new KoShapeBackgroundCommand(selectedShapes, fill)); } #endif } void KoFillConfigWidget::loadCurrentFillFromResourceServer() { { KoColor color = d->canvas->resourceManager()->backgroundColor(); slotCanvasResourceChanged(KoCanvasResourceProvider::BackgroundColor, QVariant::fromValue(color)); } { KoColor color = d->canvas->resourceManager()->foregroundColor(); slotCanvasResourceChanged(KoCanvasResourceProvider::ForegroundColor, QVariant::fromValue(color)); } Q_FOREACH (QAbstractButton *button, d->group->buttons()) { button->setEnabled(true); } emit sigFillChanged(); } void KoFillConfigWidget::shapeChanged() { if (d->noSelectionTrackingMode) return; QList shapes = currentShapes(); bool shouldUploadColorToResourceManager = false; if (shapes.isEmpty() || (shapes.size() > 1 && KoShapeFillWrapper(shapes, d->fillVariant).isMixedFill())) { Q_FOREACH (QAbstractButton *button, d->group->buttons()) { button->setEnabled(!shapes.isEmpty()); } } else { // only one vector object selected Q_FOREACH (QAbstractButton *button, d->group->buttons()) { button->setEnabled(true); } // update active index of shape KoShape *shape = shapes.first(); updateFillIndexFromShape(shape); updateFillColorFromShape(shape); // updates tool options fields shouldUploadColorToResourceManager = true; } // updates the UI d->group->button(d->selectedFillIndex)->setChecked(true); updateWidgetComponentVisbility(); slotUpdateFillTitle(); if (shouldUploadColorToResourceManager) { emit sigInternalRequestColorToResourceManager(); } else { emit sigInternalRecoverColorInResourceManager(); } } void KoFillConfigWidget::updateFillIndexFromShape(KoShape *shape) { KIS_SAFE_ASSERT_RECOVER_RETURN(shape); KoShapeFillWrapper wrapper(shape, d->fillVariant); switch (wrapper.type()) { case KoFlake::None: d->selectedFillIndex = KoFillConfigWidget::None; break; case KoFlake::Solid: d->selectedFillIndex = KoFillConfigWidget::Solid; break; case KoFlake::Gradient: d->selectedFillIndex = KoFillConfigWidget::Gradient; break; case KoFlake::Pattern: d->selectedFillIndex = KoFillConfigWidget::Pattern; break; } } void KoFillConfigWidget::updateFillColorFromShape(KoShape *shape) { KIS_SAFE_ASSERT_RECOVER_RETURN(shape); KoShapeFillWrapper wrapper(shape, d->fillVariant); switch (wrapper.type()) { case KoFlake::None: break; case KoFlake::Solid: { QColor color = wrapper.color(); if (color.alpha() > 0) { d->colorAction->setCurrentColor(wrapper.color()); } break; } case KoFlake::Gradient: uploadNewGradientBackground(wrapper.gradient()); updateGradientSaveButtonAvailability(); break; case KoFlake::Pattern: break; } } void KoFillConfigWidget::updateWidgetComponentVisbility() { // The UI is showing/hiding things like this because the 'stacked widget' isn't very flexible // and makes it difficult to put anything underneath it without a lot empty space // hide everything first d->ui->wdgGradientEditor->setVisible(false); d->ui->btnChoosePredefinedGradient->setVisible(false); d->ui->btnChooseSolidColor->setVisible(false); d->ui->typeLabel->setVisible(false); d->ui->repeatLabel->setVisible(false); d->ui->cmbGradientRepeat->setVisible(false); d->ui->cmbGradientType->setVisible(false); d->ui->btnSolidColorPick->setVisible(false); d->ui->btnSaveGradient->setVisible(false); d->ui->gradientTypeLine->setVisible(false); d->ui->soldStrokeColorLabel->setVisible(false); d->ui->presetLabel->setVisible(false); // keep options hidden if no vector shapes are selected if(currentShapes().isEmpty()) { return; } switch (d->selectedFillIndex) { case KoFillConfigWidget::None: break; case KoFillConfigWidget::Solid: d->ui->btnChooseSolidColor->setVisible(true); d->ui->btnSolidColorPick->setVisible(false); d->ui->soldStrokeColorLabel->setVisible(true); break; case KoFillConfigWidget::Gradient: d->ui->wdgGradientEditor->setVisible(true); d->ui->btnChoosePredefinedGradient->setVisible(true); d->ui->typeLabel->setVisible(true); d->ui->repeatLabel->setVisible(true); d->ui->cmbGradientRepeat->setVisible(true); d->ui->cmbGradientType->setVisible(true); d->ui->btnSaveGradient->setVisible(true); d->ui->gradientTypeLine->setVisible(true); d->ui->presetLabel->setVisible(true); break; case KoFillConfigWidget::Pattern: break; } } diff --git a/libs/ui/widgets/KoStrokeConfigWidget.cpp b/libs/ui/widgets/KoStrokeConfigWidget.cpp index cbdc24ca28..3895d5fda2 100644 --- a/libs/ui/widgets/KoStrokeConfigWidget.cpp +++ b/libs/ui/widgets/KoStrokeConfigWidget.cpp @@ -1,800 +1,804 @@ /* This file is part of the KDE project * Made by Tomislav Lukman (tomislav.lukman@ck.tel.hr) * Copyright (C) 2002 Tomislav Lukman * Copyright (C) 2002-2003 Rob Buis * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005-2007 Thomas Zander * Copyright (C) 2005-2006, 2011 Inge Wallin * Copyright (C) 2005-2008 Jan Hambrecht * Copyright (C) 2006 C. Boemann * Copyright (C) 2006 Peter Simonsson * Copyright (C) 2006 Laurent Montel * Copyright (C) 2007,2011 Thorsten Zachmann * Copyright (C) 2011 Jean-Nicolas Artaud * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // Own #include "KoStrokeConfigWidget.h" // Qt #include #include #include #include #include #include #include // KDE #include // Calligra #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_KoStrokeConfigWidget.h" #include #include #include "kis_canvas_resource_provider.h" #include "kis_acyclic_signal_connector.h" #include // Krita #include "kis_double_parse_unit_spin_box.h" class CapNJoinMenu : public QMenu { public: CapNJoinMenu(QWidget *parent = 0); QSize sizeHint() const override; KisDoubleParseUnitSpinBox *miterLimit {0}; QButtonGroup *capGroup {0}; QButtonGroup *joinGroup {0}; }; CapNJoinMenu::CapNJoinMenu(QWidget *parent) : QMenu(parent) { QGridLayout *mainLayout = new QGridLayout(); mainLayout->setMargin(2); // The cap group capGroup = new QButtonGroup(this); capGroup->setExclusive(true); QToolButton *button = 0; button = new QToolButton(this); button->setIcon(koIcon("stroke-cap-butt")); button->setCheckable(true); button->setToolTip(i18n("Butt cap")); capGroup->addButton(button, Qt::FlatCap); mainLayout->addWidget(button, 2, 0); button = new QToolButton(this); button->setIcon(koIcon("stroke-cap-round")); button->setCheckable(true); button->setToolTip(i18n("Round cap")); capGroup->addButton(button, Qt::RoundCap); mainLayout->addWidget(button, 2, 1); button = new QToolButton(this); button->setIcon(koIcon("stroke-cap-square")); button->setCheckable(true); button->setToolTip(i18n("Square cap")); capGroup->addButton(button, Qt::SquareCap); mainLayout->addWidget(button, 2, 2, Qt::AlignLeft); // The join group joinGroup = new QButtonGroup(this); joinGroup->setExclusive(true); button = new QToolButton(this); button->setIcon(koIcon("stroke-join-miter")); button->setCheckable(true); button->setToolTip(i18n("Miter join")); joinGroup->addButton(button, Qt::MiterJoin); mainLayout->addWidget(button, 3, 0); button = new QToolButton(this); button->setIcon(koIcon("stroke-join-round")); button->setCheckable(true); button->setToolTip(i18n("Round join")); joinGroup->addButton(button, Qt::RoundJoin); mainLayout->addWidget(button, 3, 1); button = new QToolButton(this); button->setIcon(koIcon("stroke-join-bevel")); button->setCheckable(true); button->setToolTip(i18n("Bevel join")); joinGroup->addButton(button, Qt::BevelJoin); mainLayout->addWidget(button, 3, 2, Qt::AlignLeft); // Miter limit // set min/max/step and value in points, then set actual unit miterLimit = new KisDoubleParseUnitSpinBox(this); miterLimit->setMinMaxStep(0.0, 1000.0, 0.5); miterLimit->setDecimals(2); miterLimit->setUnit(KoUnit(KoUnit::Point)); miterLimit->setToolTip(i18n("Miter limit")); mainLayout->addWidget(miterLimit, 4, 0, 1, 3); mainLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); setLayout(mainLayout); } QSize CapNJoinMenu::sizeHint() const { return layout()->sizeHint(); } class Q_DECL_HIDDEN KoStrokeConfigWidget::Private { public: Private() : selectionChangedCompressor(200, KisSignalCompressor::FIRST_ACTIVE) { } KoLineStyleSelector *lineStyle {0}; KisDoubleParseUnitSpinBox *lineWidth {0}; KoMarkerSelector *startMarkerSelector {0}; KoMarkerSelector *midMarkerSelector {0}; KoMarkerSelector *endMarkerSelector {0}; CapNJoinMenu *capNJoinMenu {0}; QWidget*spacer {0}; KoCanvasBase *canvas {0}; bool active {true}; bool allowLocalUnitManagement {false}; KoFillConfigWidget *fillConfigWidget {0}; bool noSelectionTrackingMode {false}; KisAcyclicSignalConnector shapeChangedAcyclicConnector; KisAcyclicSignalConnector resourceManagerAcyclicConnector; KisSignalCompressor selectionChangedCompressor; std::vector deactivationLocks; Ui_KoStrokeConfigWidget *ui; }; KoStrokeConfigWidget::KoStrokeConfigWidget(KoCanvasBase *canvas, QWidget * parent) : QWidget(parent) , d(new Private()) { // configure GUI d->ui = new Ui_KoStrokeConfigWidget(); d->ui->setupUi(this); setObjectName("Stroke widget"); { // connect the canvas d->shapeChangedAcyclicConnector.connectBackwardVoid( canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), &d->selectionChangedCompressor, SLOT(start())); d->shapeChangedAcyclicConnector.connectBackwardVoid( canvas->selectedShapesProxy(), SIGNAL(selectionContentChanged()), &d->selectionChangedCompressor, SLOT(start())); connect(&d->selectionChangedCompressor, SIGNAL(timeout()), this, SLOT(selectionChanged())); d->resourceManagerAcyclicConnector.connectBackwardResourcePair( canvas->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)), this, SLOT(canvasResourceChanged(int,QVariant))); d->canvas = canvas; } { d->fillConfigWidget = new KoFillConfigWidget(canvas, KoFlake::StrokeFill, true, this); d->fillConfigWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); d->ui->fillConfigWidgetLayout->addWidget(d->fillConfigWidget); connect(d->fillConfigWidget, SIGNAL(sigFillChanged()), SIGNAL(sigStrokeChanged())); } d->ui->thicknessLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); d->ui->thicknessLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // set min/max/step and value in points, then set actual unit d->ui->lineWidth->setMinMaxStep(0.5, 1000.0, 0.5); // if someone wants 0, just set to "none" on UI d->ui->lineWidth->setDecimals(2); d->ui->lineWidth->setUnit(KoUnit(KoUnit::Point)); d->ui->lineWidth->setToolTip(i18n("Set line width of actual selection")); d->ui->capNJoinButton->setMinimumHeight(25); d->capNJoinMenu = new CapNJoinMenu(this); d->ui->capNJoinButton->setMenu(d->capNJoinMenu); d->ui->capNJoinButton->setText("..."); d->ui->capNJoinButton->setPopupMode(QToolButton::InstantPopup); { // Line style d->ui->strokeStyleLabel->setText(i18n("Line Style:")); d->ui->strokeStyleLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); d->ui->lineStyle->setToolTip(i18nc("@info:tooltip", "Line style")); d->ui->lineStyle->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); d->ui->lineStyle->setLineStyle(Qt::SolidLine, QVector()); } { QList emptyMarkers; d->startMarkerSelector = new KoMarkerSelector(KoFlake::StartMarker, this); d->startMarkerSelector->setToolTip(i18nc("@info:tooltip", "Start marker")); d->startMarkerSelector->updateMarkers(emptyMarkers); d->startMarkerSelector->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred ); d->ui->markerLayout->addWidget(d->startMarkerSelector); d->midMarkerSelector = new KoMarkerSelector(KoFlake::MidMarker, this); d->midMarkerSelector->setToolTip(i18nc("@info:tooltip", "Node marker")); d->midMarkerSelector->updateMarkers(emptyMarkers); d->midMarkerSelector->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred ); d->ui->markerLayout->addWidget(d->midMarkerSelector); d->endMarkerSelector = new KoMarkerSelector(KoFlake::EndMarker, this); d->endMarkerSelector->setToolTip(i18nc("@info:tooltip", "End marker")); d->endMarkerSelector->updateMarkers(emptyMarkers); d->endMarkerSelector->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred ); d->ui->markerLayout->addWidget(d->endMarkerSelector); } // Spacer d->spacer = new QWidget(); d->spacer->setObjectName("SpecialSpacer"); d->ui->markerLayout->addWidget(d->spacer); connect(d->ui->lineStyle, SIGNAL(currentIndexChanged(int)), this, SLOT(applyDashStyleChanges())); connect(d->ui->lineWidth, SIGNAL(valueChangedPt(qreal)), this, SLOT(applyLineWidthChanges())); connect(d->capNJoinMenu->capGroup, SIGNAL(buttonClicked(int)), this, SLOT(applyJoinCapChanges())); connect(d->capNJoinMenu->joinGroup, SIGNAL(buttonClicked(int)), this, SLOT(applyJoinCapChanges())); connect(d->capNJoinMenu->miterLimit, SIGNAL(valueChangedPt(qreal)), this, SLOT(applyJoinCapChanges())); { // Map the marker signals correctly KisSignalMapper *mapper = new KisSignalMapper(this); connect(mapper, SIGNAL(mapped(int)), SLOT(applyMarkerChanges(int))); connect(d->startMarkerSelector, SIGNAL(currentIndexChanged(int)), mapper, SLOT(map())); connect(d->midMarkerSelector, SIGNAL(currentIndexChanged(int)), mapper, SLOT(map())); connect(d->endMarkerSelector, SIGNAL(currentIndexChanged(int)), mapper, SLOT(map())); mapper->setMapping(d->startMarkerSelector, KoFlake::StartMarker); mapper->setMapping(d->midMarkerSelector, KoFlake::MidMarker); mapper->setMapping(d->endMarkerSelector, KoFlake::EndMarker); } KoDocumentResourceManager *resourceManager = canvas->shapeController()->resourceManager(); if (resourceManager) { KoMarkerCollection *collection = resourceManager->resource(KoDocumentResourceManager::MarkerCollection).value(); if (collection) { updateMarkers(collection->markers()); } } d->selectionChangedCompressor.start(); - d->fillConfigWidget->activate(); - deactivate(); + // initialize deactivation locks + d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->shapeChangedAcyclicConnector)); + d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->resourceManagerAcyclicConnector)); } KoStrokeConfigWidget::~KoStrokeConfigWidget() { delete d; } void KoStrokeConfigWidget::setNoSelectionTrackingMode(bool value) { d->fillConfigWidget->setNoSelectionTrackingMode(value); d->noSelectionTrackingMode = value; if (!d->noSelectionTrackingMode) { d->selectionChangedCompressor.start(); } } // ---------------------------------------------------------------- // getters and setters Qt::PenStyle KoStrokeConfigWidget::lineStyle() const { return d->ui->lineStyle->lineStyle(); } QVector KoStrokeConfigWidget::lineDashes() const { return d->ui->lineStyle->lineDashes(); } qreal KoStrokeConfigWidget::lineWidth() const { return d->ui->lineWidth->value(); } qreal KoStrokeConfigWidget::miterLimit() const { return d->capNJoinMenu->miterLimit->value(); } KoMarker *KoStrokeConfigWidget::startMarker() const { return d->startMarkerSelector->marker(); } KoMarker *KoStrokeConfigWidget::endMarker() const { return d->endMarkerSelector->marker(); } Qt::PenCapStyle KoStrokeConfigWidget::capStyle() const { return static_cast(d->capNJoinMenu->capGroup->checkedId()); } Qt::PenJoinStyle KoStrokeConfigWidget::joinStyle() const { return static_cast(d->capNJoinMenu->joinGroup->checkedId()); } KoShapeStrokeSP KoStrokeConfigWidget::createShapeStroke() { KoShapeStrokeSP stroke(d->fillConfigWidget->createShapeStroke()); stroke->setLineWidth(lineWidth()); stroke->setCapStyle(capStyle()); stroke->setJoinStyle(joinStyle()); stroke->setMiterLimit(miterLimit()); stroke->setLineStyle(lineStyle(), lineDashes()); return stroke; } // ---------------------------------------------------------------- // Other public functions void KoStrokeConfigWidget::updateStyleControlsAvailability(bool enabled) { d->ui->lineWidth->setEnabled(enabled); d->capNJoinMenu->setEnabled(enabled); d->ui->lineStyle->setEnabled(enabled); d->startMarkerSelector->setEnabled(enabled); d->midMarkerSelector->setEnabled(enabled); d->endMarkerSelector->setEnabled(enabled); } void KoStrokeConfigWidget::setUnit(const KoUnit &unit, KoShape *representativeShape) { if (!d->allowLocalUnitManagement) { return; //the unit management is completely transferred to the unitManagers. } blockChildSignals(true); /** * KoStrokeShape knows nothing about the transformations applied * to the shape, which doesn't prevent the shape to apply them and * display the stroke differently. So just take that into account * and show the user correct values using the multiplier in KoUnit. */ KoUnit newUnit(unit); if (representativeShape) { newUnit.adjustByPixelTransform(representativeShape->absoluteTransformation()); } d->ui->lineWidth->setUnit(newUnit); d->capNJoinMenu->miterLimit->setUnit(newUnit); d->ui->lineWidth->setLineStep(1.0); d->capNJoinMenu->miterLimit->setLineStep(1.0); blockChildSignals(false); } void KoStrokeConfigWidget::setUnitManagers(KisSpinBoxUnitManager* managerLineWidth, KisSpinBoxUnitManager *managerMitterLimit) { blockChildSignals(true); d->allowLocalUnitManagement = false; d->ui->lineWidth->setUnitManager(managerLineWidth); d->capNJoinMenu->miterLimit->setUnitManager(managerMitterLimit); blockChildSignals(false); } void KoStrokeConfigWidget::updateMarkers(const QList &markers) { d->startMarkerSelector->updateMarkers(markers); d->midMarkerSelector->updateMarkers(markers); d->endMarkerSelector->updateMarkers(markers); } void KoStrokeConfigWidget::activate() { + KIS_SAFE_ASSERT_RECOVER_NOOP(!d->deactivationLocks.empty()); d->deactivationLocks.clear(); d->fillConfigWidget->activate(); + if (!d->noSelectionTrackingMode) { d->selectionChangedCompressor.start(); } else { loadCurrentStrokeFillFromResourceServer(); } } void KoStrokeConfigWidget::deactivate() { - d->deactivationLocks.clear(); + KIS_SAFE_ASSERT_RECOVER_NOOP(d->deactivationLocks.empty()); + d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->shapeChangedAcyclicConnector)); d->deactivationLocks.push_back(KisAcyclicSignalConnector::Blocker(d->resourceManagerAcyclicConnector)); d->fillConfigWidget->deactivate(); } void KoStrokeConfigWidget::blockChildSignals(bool block) { d->ui->lineWidth->blockSignals(block); d->capNJoinMenu->capGroup->blockSignals(block); d->capNJoinMenu->joinGroup->blockSignals(block); d->capNJoinMenu->miterLimit->blockSignals(block); d->ui->lineStyle->blockSignals(block); d->startMarkerSelector->blockSignals(block); d->midMarkerSelector->blockSignals(block); d->endMarkerSelector->blockSignals(block); } void KoStrokeConfigWidget::setActive(bool active) { d->active = active; } //------------------------ template auto applyChangeToStrokes(KoCanvasBase *canvas, ModifyFunction modifyFunction) -> decltype(modifyFunction(KoShapeStrokeSP()), void()) { KoSelection *selection = canvas->selectedShapesProxy()->selection(); if (!selection) return; QList shapes = selection->selectedEditableShapes(); KUndo2Command *command = KoFlake::modifyShapesStrokes(shapes, modifyFunction); if (command) { canvas->addCommand(command); } } void KoStrokeConfigWidget::applyDashStyleChanges() { applyChangeToStrokes( d->canvas, [this] (KoShapeStrokeSP stroke) { stroke->setLineStyle(lineStyle(), lineDashes()); }); emit sigStrokeChanged(); } void KoStrokeConfigWidget::applyLineWidthChanges() { applyChangeToStrokes( d->canvas, [this] (KoShapeStrokeSP stroke) { stroke->setLineWidth(lineWidth()); }); emit sigStrokeChanged(); } void KoStrokeConfigWidget::applyJoinCapChanges() { applyChangeToStrokes( d->canvas, [this] (KoShapeStrokeSP stroke) { stroke->setCapStyle(static_cast(d->capNJoinMenu->capGroup->checkedId())); stroke->setJoinStyle(static_cast(d->capNJoinMenu->joinGroup->checkedId())); stroke->setMiterLimit(miterLimit()); }); emit sigStrokeChanged(); } void KoStrokeConfigWidget::applyMarkerChanges(int rawPosition) { KoSelection *selection = d->canvas->selectedShapesProxy()->selection(); if (!selection) { emit sigStrokeChanged(); return; } QList shapes = selection->selectedEditableShapes(); QList pathShapes; Q_FOREACH (KoShape *shape, shapes) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape) { pathShapes << pathShape; } } if (pathShapes.isEmpty()) { emit sigStrokeChanged(); return; } KoFlake::MarkerPosition position = KoFlake::MarkerPosition(rawPosition); QScopedPointer marker; switch (position) { case KoFlake::StartMarker: if (d->startMarkerSelector->marker()) { marker.reset(new KoMarker(*d->startMarkerSelector->marker())); } break; case KoFlake::MidMarker: if (d->midMarkerSelector->marker()) { marker.reset(new KoMarker(*d->midMarkerSelector->marker())); } break; case KoFlake::EndMarker: if (d->endMarkerSelector->marker()) { marker.reset(new KoMarker(*d->endMarkerSelector->marker())); } break; } KUndo2Command* command = new KoPathShapeMarkerCommand(pathShapes, marker.take(), position); d->canvas->addCommand(command); emit sigStrokeChanged(); } // ---------------------------------------------------------------- struct CheckShapeStrokeStyleBasePolicy { typedef KoShapeStrokeSP PointerType; static PointerType getProperty(KoShape *shape) { return qSharedPointerDynamicCast(shape->stroke()); } }; struct CheckShapeStrokeDashesPolicy : public CheckShapeStrokeStyleBasePolicy { static bool compareTo(PointerType p1, PointerType p2) { return p1->lineStyle() == p2->lineStyle() && p1->lineDashes() == p2->lineDashes() && p1->dashOffset() == p2->dashOffset(); } }; struct CheckShapeStrokeCapJoinPolicy : public CheckShapeStrokeStyleBasePolicy { static bool compareTo(PointerType p1, PointerType p2) { return p1->capStyle() == p2->capStyle() && p1->joinStyle() == p2->joinStyle() && p1->miterLimit() == p2->miterLimit(); } }; struct CheckShapeStrokeWidthPolicy : public CheckShapeStrokeStyleBasePolicy { static bool compareTo(PointerType p1, PointerType p2) { return p1->lineWidth() == p2->lineWidth(); } }; struct CheckShapeMarkerPolicy { CheckShapeMarkerPolicy(KoFlake::MarkerPosition position) : m_position(position) { } typedef KoMarker* PointerType; PointerType getProperty(KoShape *shape) const { KoPathShape *pathShape = dynamic_cast(shape); return pathShape ? pathShape->marker(m_position) : 0; } bool compareTo(PointerType p1, PointerType p2) const { if ((!p1 || !p2) && p1 != p2) return false; if (!p1 && p1 == p2) return true; return p1 == p2 || *p1 == *p2; } KoFlake::MarkerPosition m_position; }; void KoStrokeConfigWidget::selectionChanged() { if (d->noSelectionTrackingMode) return; KoSelection *selection = d->canvas->selectedShapesProxy()->selection(); if (!selection) return; // we need to linearize update order, and force the child widget to update // before we start doing it QList shapes = selection->selectedEditableShapes(); d->fillConfigWidget->forceUpdateOnSelectionChanged(); // calls shapeChanged() logic KoShape *shape = !shapes.isEmpty() ? shapes.first() : 0; const KoShapeStrokeSP stroke = shape ? qSharedPointerDynamicCast(shape->stroke()) : KoShapeStrokeSP(); // setUnit uses blockChildSignals() so take care not to use it inside the block setUnit(d->canvas->unit(), shape); blockChildSignals(true); // line width if (stroke && KoFlake::compareShapePropertiesEqual(shapes)) { d->ui->lineWidth->changeValue(stroke->lineWidth()); } else { d->ui->lineWidth->changeValue(0); } // caps & joins if (stroke && KoFlake::compareShapePropertiesEqual(shapes)) { Qt::PenCapStyle capStyle = stroke->capStyle() >= 0 ? stroke->capStyle() : Qt::FlatCap; Qt::PenJoinStyle joinStyle = stroke->joinStyle() >= 0 ? stroke->joinStyle() : Qt::MiterJoin; { QAbstractButton *button = d->capNJoinMenu->capGroup->button(capStyle); KIS_SAFE_ASSERT_RECOVER_RETURN(button); button->setChecked(true); } { QAbstractButton *button = d->capNJoinMenu->joinGroup->button(joinStyle); KIS_SAFE_ASSERT_RECOVER_RETURN(button); button->setChecked(true); } d->capNJoinMenu->miterLimit->changeValue(stroke->miterLimit()); d->capNJoinMenu->miterLimit->setEnabled(joinStyle == Qt::MiterJoin); } else { d->capNJoinMenu->capGroup->button(Qt::FlatCap)->setChecked(true); d->capNJoinMenu->joinGroup->button(Qt::MiterJoin)->setChecked(true); d->capNJoinMenu->miterLimit->changeValue(0.0); d->capNJoinMenu->miterLimit->setEnabled(true); } // dashes style if (stroke && KoFlake::compareShapePropertiesEqual(shapes)) { d->ui->lineStyle->setLineStyle(stroke->lineStyle(), stroke->lineDashes()); } else { d->ui->lineStyle->setLineStyle(Qt::SolidLine, QVector()); } // markers KoPathShape *pathShape = dynamic_cast(shape); if (pathShape) { if (KoFlake::compareShapePropertiesEqual(shapes, CheckShapeMarkerPolicy(KoFlake::StartMarker))) { d->startMarkerSelector->setMarker(pathShape->marker(KoFlake::StartMarker)); } if (KoFlake::compareShapePropertiesEqual(shapes, CheckShapeMarkerPolicy(KoFlake::MidMarker))) { d->midMarkerSelector->setMarker(pathShape->marker(KoFlake::MidMarker)); } if (KoFlake::compareShapePropertiesEqual(shapes, CheckShapeMarkerPolicy(KoFlake::EndMarker))) { d->endMarkerSelector->setMarker(pathShape->marker(KoFlake::EndMarker)); } } const bool lineOptionsVisible = (d->fillConfigWidget->selectedFillIndex() != 0); // This switch statement is to help the tab widget "pages" to be closer to the correct size // if we don't do this the internal widgets get rendered, then the tab page has to get resized to // fill up the space, then the internal widgets have to resize yet again...causing flicker switch(d->fillConfigWidget->selectedFillIndex()) { case 0: // no fill this->setMinimumHeight(130); break; case 1: // solid fill this->setMinimumHeight(200); break; case 2: // gradient fill this->setMinimumHeight(350); case 3: // pattern fill break; } d->ui->thicknessLineBreak->setVisible(lineOptionsVisible); d->ui->lineWidth->setVisible(lineOptionsVisible); d->ui->capNJoinButton->setVisible(lineOptionsVisible); d->ui->lineStyle->setVisible(lineOptionsVisible); d->startMarkerSelector->setVisible(lineOptionsVisible); d->midMarkerSelector->setVisible(lineOptionsVisible); d->endMarkerSelector->setVisible(lineOptionsVisible); d->ui->thicknessLabel->setVisible(lineOptionsVisible); d->ui->strokeStyleLabel->setVisible(lineOptionsVisible); blockChildSignals(false); updateStyleControlsAvailability(!shapes.isEmpty()); } void KoStrokeConfigWidget::canvasResourceChanged(int key, const QVariant &value) { switch (key) { case KoCanvasResourceProvider::Unit: // we request the whole selection to reload because the // unit of the stroke width depends on the selected shape d->selectionChangedCompressor.start(); break; case KisCanvasResourceProvider::Size: if (d->noSelectionTrackingMode) { d->ui->lineWidth->changeValue(d->canvas->unit().fromUserValue(value.toReal())); } break; } } void KoStrokeConfigWidget::loadCurrentStrokeFillFromResourceServer() { if (d->canvas) { const QVariant value = d->canvas->resourceManager()->resource(KisCanvasResourceProvider::Size); canvasResourceChanged(KisCanvasResourceProvider::Size, value); updateStyleControlsAvailability(true); emit sigStrokeChanged(); } } diff --git a/libs/ui/widgets/kis_floating_message.cpp b/libs/ui/widgets/kis_floating_message.cpp index 84ad5218b8..3896b98bc1 100644 --- a/libs/ui/widgets/kis_floating_message.cpp +++ b/libs/ui/widgets/kis_floating_message.cpp @@ -1,354 +1,354 @@ /* * This file is part of KimageShop^WKrayon^WKrita * * Copyright (c) 2012 Boudewijn Rempt * Copyright (c) 2004 Christian Muehlhaeuser * Copyright (c) 2004-2006 Seb Ruiz * Copyright (c) 2004,2005 Max Howell * Copyright (c) 2005 Gabor Lehel * Copyright (c) 2008,2009 Mark Kretschmann * * 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_floating_message.h" #include #include #include #include #include #include +#include #include #include #include "kis_global.h" - /* Code copied from kshadowengine.cpp * * Copyright (C) 2003 Laur Ivan * * Many thanks to: * - Bernardo Hung for the enhanced shadow * algorithm (currently used) * - Tim Jansen for the API updates and fixes. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ namespace ShadowEngine { // Not sure, doesn't work above 10 static const int MULTIPLICATION_FACTOR = 3; // Multiplication factor for pixels directly above, under, or next to the text static const double AXIS_FACTOR = 2.0; // Multiplication factor for pixels diagonal to the text static const double DIAGONAL_FACTOR = 0.1; // Self explanatory static const int MAX_OPACITY = 200; double decay( QImage&, int, int ); QImage makeShadow( const QPixmap& textPixmap, const QColor &bgColor ) { const int w = textPixmap.width(); const int h = textPixmap.height(); const int bgr = bgColor.red(); const int bgg = bgColor.green(); const int bgb = bgColor.blue(); int alphaShadow; // This is the source pixmap QImage img = textPixmap.toImage(); QImage result( w, h, QImage::Format_ARGB32 ); result.fill( 0 ); // fill with black static const int M = 5; for( int i = M; i < w - M; i++) { for( int j = M; j < h - M; j++ ) { alphaShadow = (int) decay( img, i, j ); result.setPixel( i,j, qRgba( bgr, bgg , bgb, qMin( MAX_OPACITY, alphaShadow ) ) ); } } return result; } double decay( QImage& source, int i, int j ) { double alphaShadow; alphaShadow =(qGray(source.pixel(i-1,j-1)) * DIAGONAL_FACTOR + qGray(source.pixel(i-1,j )) * AXIS_FACTOR + qGray(source.pixel(i-1,j+1)) * DIAGONAL_FACTOR + qGray(source.pixel(i ,j-1)) * AXIS_FACTOR + 0 + qGray(source.pixel(i ,j+1)) * AXIS_FACTOR + qGray(source.pixel(i+1,j-1)) * DIAGONAL_FACTOR + qGray(source.pixel(i+1,j )) * AXIS_FACTOR + qGray(source.pixel(i+1,j+1)) * DIAGONAL_FACTOR) / MULTIPLICATION_FACTOR; return alphaShadow; } } #define OSD_WINDOW_OPACITY 0.74 KisFloatingMessage::KisFloatingMessage(const QString &message, QWidget *parent, bool showOverParent, int timeout, Priority priority, int alignment) : QWidget(parent) , m_message(message) , m_showOverParent(showOverParent) , m_timeout(timeout) , m_priority(priority) , m_alignment(alignment) , widgetQueuedForDeletion(false) { m_icon = KisIconUtils::loadIcon("krita").pixmap(256, 256).toImage(); setWindowFlags(Qt::FramelessWindowHint | Qt::ToolTip); setFocusPolicy(Qt::NoFocus); setAttribute(Qt::WA_ShowWithoutActivating); setFont(QFont("sans-serif")); m_timer.setSingleShot( true ); connect(&m_timer, SIGNAL(timeout()), SLOT(startFade())); connect(this, SIGNAL(destroyed()), SLOT(widgetDeleted())); } void KisFloatingMessage::tryOverrideMessage(const QString message, const QIcon& icon, int timeout, KisFloatingMessage::Priority priority, int alignment) { if ((int)priority > (int)m_priority) return; m_message = message; setIcon(icon); m_timeout = timeout; m_priority = priority; m_alignment = alignment; showMessage(); update(); } void KisFloatingMessage::showMessage() { if (widgetQueuedForDeletion) return; #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) setGeometry(determineMetrics(fontMetrics().horizontalAdvance('x'))); #else setGeometry(determineMetrics(fontMetrics().width('x'))); #endif setWindowOpacity(OSD_WINDOW_OPACITY); QWidget::setVisible(true); m_timer.start(m_timeout); } void KisFloatingMessage::setShowOverParent(bool show) { m_showOverParent = show; } void KisFloatingMessage::setIcon(const QIcon& icon) { m_icon = icon.pixmap(256, 256).toImage(); } const int MARGIN = 20; QRect KisFloatingMessage::determineMetrics( const int M ) { m_m = M; const QSize minImageSize = m_icon.size().boundedTo(QSize(100, 100)); // determine a sensible maximum size, don't cover the whole desktop or cross the screen const QSize margin( (M + MARGIN) * 2, (M + MARGIN) * 2); //margins const QSize image = m_icon.isNull() ? QSize(0, 0) : minImageSize; #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QScreen *s = qApp->screenAt(parentWidget()->geometry().topLeft()); const QSize max = s->availableGeometry().size() - margin; #else const QSize max = QApplication::desktop()->availableGeometry(parentWidget()).size() - margin; #endif // If we don't do that, the boundingRect() might not be suitable for drawText() (Qt issue N67674) m_message.replace(QRegExp( " +\n"), "\n"); // remove consecutive line breaks m_message.replace(QRegExp( "\n+"), "\n"); // The osd cannot be larger than the screen QRect rect = fontMetrics().boundingRect(0, 0, max.width() - image.width(), max.height(), m_alignment, m_message); if (!m_icon.isNull()) { const int availableWidth = max.width() - rect.width() - M; //WILL be >= (minImageSize.width() - M) m_scaledIcon = QPixmap::fromImage(m_icon.scaled(qMin(availableWidth, m_icon.width()), qMin( rect.height(), m_icon.height()), Qt::KeepAspectRatio, Qt::SmoothTransformation)); const int widthIncludingImage = rect.width() + m_scaledIcon.width() + M; //margin between text + image rect.setWidth( widthIncludingImage ); } // expand in all directions by 2*M // // take care with this rect, because it must be *bigger* // than the rect we paint the message in rect = kisGrowRect(rect, 2 * M); const QSize newSize = rect.size(); #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QRect screen = s->availableGeometry(); #else QRect screen = QApplication::desktop()->screenGeometry(parentWidget()); #endif QPoint newPos(MARGIN, MARGIN); if (parentWidget() && m_showOverParent) { screen = parentWidget()->geometry(); screen.setTopLeft(parentWidget()->mapToGlobal(QPoint(MARGIN, MARGIN + 50))); newPos = screen.topLeft(); } else { // move to the right newPos.rx() = screen.width() - MARGIN - newSize.width(); //ensure we don't dip below the screen if (newPos.y() + newSize.height() > screen.height() - MARGIN) { newPos.ry() = screen.height() - MARGIN - newSize.height(); } // correct for screen position newPos += screen.topLeft(); if (parentWidget()) { // Move a bit to the left as there could be a scrollbar newPos.setX(newPos.x() - MARGIN); } } QRect rc(newPos, rect.size()); return rc; } void KisFloatingMessage::paintEvent( QPaintEvent *e ) { const int& M = m_m; QPoint point; QRect rect(point, size()); rect.adjust(0, 0, -1, -1); QPainter p(this); p.setRenderHints( QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing ); p.setClipRect(e->rect()); //QPixmap background = The::svgHandler()->renderSvgWithDividers( "service_list_item", width(), height(), "service_list_item" ); //p.drawPixmap( 0, 0, background ); p.setPen(Qt::white); rect.adjust(M, M, -M, -M); if (!m_icon.isNull()) { QRect r(rect); r.setTop((size().height() - m_scaledIcon.height() ) / 2); r.setSize(m_scaledIcon.size()); p.drawPixmap(r.topLeft(), m_scaledIcon); rect.setLeft(rect.left() + m_scaledIcon.width() + M); } int graphicsHeight = 0; rect.setBottom(rect.bottom() - graphicsHeight); // Draw "shadow" text effect (black outline) QPixmap pixmap( rect.size() + QSize( 10, 10 ) ); pixmap.fill( Qt::black ); QPainter p2(&pixmap); p2.setFont(font()); p2.setPen(Qt::white); p2.setBrush(Qt::white); p2.drawText(QRect( QPoint( 5, 5 ), rect.size() ), m_alignment, m_message); p2.end(); QColor shadowColor; { int h, s, v; palette().color( QPalette::Normal, QPalette::Foreground ).getHsv( &h, &s, &v ); shadowColor = v > 128 ? Qt::black : Qt::white; } p.drawImage(rect.topLeft() - QPoint(5, 5), ShadowEngine::makeShadow(pixmap, shadowColor)); p.setPen( palette().color(QPalette::Active, QPalette::WindowText )); p.drawText(rect, m_alignment, m_message); } void KisFloatingMessage::startFade() { m_fadeTimeLine.setDuration(250); m_fadeTimeLine.setCurveShape(QTimeLine::EaseInCurve); m_fadeTimeLine.setLoopCount(1); m_fadeTimeLine.setFrameRange(0, 0); m_fadeTimeLine.setFrameRange(0, 10); connect(&m_fadeTimeLine, SIGNAL(finished()), SLOT(removeMessage())); connect(&m_fadeTimeLine, SIGNAL(frameChanged(int)), SLOT(updateOpacity(int))); m_fadeTimeLine.start(); } void KisFloatingMessage::removeMessage() { m_timer.stop(); m_fadeTimeLine.stop(); widgetQueuedForDeletion = true; hide(); deleteLater(); } void KisFloatingMessage::updateOpacity(int /*value*/) { setWindowOpacity(OSD_WINDOW_OPACITY - 0.1); } void KisFloatingMessage::widgetDeleted() { widgetQueuedForDeletion = false; } diff --git a/packaging/linux/appimage/build-image.sh b/packaging/linux/appimage/build-image.sh index 9c2f943981..363e342cbd 100755 --- a/packaging/linux/appimage/build-image.sh +++ b/packaging/linux/appimage/build-image.sh @@ -1,110 +1,110 @@ #!/bin/bash # Halt on errors and be verbose about what we are doing set -e set -x # Read in our parameters export BUILD_PREFIX=$1 export KRITA_SOURCES=$2 # Save some frequently referenced locations in variables for ease of use / updating export APPDIR=$BUILD_PREFIX/krita.appdir export PLUGINS=$APPDIR/usr/lib/kritaplugins/ # qjsonparser, used to add metadata to the plugins needs to work in a en_US.UTF-8 environment. # That's not always the case, so make sure it is export LC_ALL=en_US.UTF-8 export LANG=en_us.UTF-8 # We want to use $prefix/deps/usr/ for all our dependencies export DEPS_INSTALL_PREFIX=$BUILD_PREFIX/deps/usr/ export DOWNLOADS_DIR=$BUILD_PREFIX/downloads/ # Setup variables needed to help everything find what we built export LD_LIBRARY_PATH=$DEPS_INSTALL_PREFIX/lib/:$DEPS_INSTALL_PREFIX/lib/x86_64-linux-gnu/:$APPDIR/usr/lib/:$LD_LIBRARY_PATH export PATH=$DEPS_INSTALL_PREFIX/bin/:$PATH export PKG_CONFIG_PATH=$DEPS_INSTALL_PREFIX/share/pkgconfig/:$DEPS_INSTALL_PREFIX/lib/pkgconfig/:/usr/lib/pkgconfig/:$PKG_CONFIG_PATH export CMAKE_PREFIX_PATH=$DEPS_INSTALL_PREFIX:$CMAKE_PREFIX_PATH -export PYTHONPATH=$DEPS_INSTALL_PREFIX/sip/:$DEPS_INSTALL_PREFIX/lib/python3.5/site-packages/:$DEPS_INSTALL_PREFIX/lib/python3.5/ +export PYTHONPATH=$DEPS_INSTALL_PREFIX/sip/:$DEPS_INSTALL_PREFIX/lib/python3.8/site-packages/:$DEPS_INSTALL_PREFIX/lib/python3.8/ export PYTHONHOME=$DEPS_INSTALL_PREFIX # Switch over to our build prefix cd $BUILD_PREFIX # # Now we can get the process started! # # Step 0: place the translations where ki18n and Qt look for them if [ -d $APPDIR/usr/share/locale ] ; then mv $APPDIR/usr/share/locale $APPDIR/usr/share/krita fi # Step 1: Copy over all the resources provided by dependencies that we need cp -r $DEPS_INSTALL_PREFIX/share/locale $APPDIR/usr/share/krita cp -r $DEPS_INSTALL_PREFIX/share/kf5 $APPDIR/usr/share cp -r $DEPS_INSTALL_PREFIX/share/mime $APPDIR/usr/share -cp -r $DEPS_INSTALL_PREFIX/lib/python3.5 $APPDIR/usr/lib +cp -r $DEPS_INSTALL_PREFIX/lib/python3.8 $APPDIR/usr/lib cp -r $DEPS_INSTALL_PREFIX/share/sip $APPDIR/usr/share cp -r $DEPS_INSTALL_PREFIX/translations $APPDIR/usr/ # Step 2: Relocate x64 binaries from the architecture specific directory as required for Appimages mv $APPDIR/usr/lib/x86_64-linux-gnu/* $APPDIR/usr/lib rm -rf $APPDIR/usr/lib/x86_64-linux-gnu/ # Step 3: Update the rpath in the various plugins we have to make sure they'll be loadable in an Appimage context for lib in $PLUGINS/*.so*; do patchelf --set-rpath '$ORIGIN/..' $lib; done -for lib in $APPDIR/usr/lib/python3.5/site-packages/PyQt5/*.so*; do +for lib in $APPDIR/usr/lib/python3.8/site-packages/PyQt5/*.so*; do patchelf --set-rpath '$ORIGIN/../..' $lib; done -for lib in $APPDIR/usr/lib/python3.5/lib-dynload/*.so*; do +for lib in $APPDIR/usr/lib/python3.8/lib-dynload/*.so*; do patchelf --set-rpath '$ORIGIN/../..' $lib; done patchelf --set-rpath '$ORIGIN/../../../..' $APPDIR/usr/lib/qml/org/krita/draganddrop/libdraganddropplugin.so patchelf --set-rpath '$ORIGIN/../../../..' $APPDIR/usr/lib/qml/org/krita/sketch/libkritasketchplugin.so patchelf --set-rpath '$ORIGIN/../..' $APPDIR/usr/lib/krita-python-libs/PyKrita/krita.so -patchelf --set-rpath '$ORIGIN/../..' $APPDIR/usr/lib/python3.5/site-packages/PyQt5/sip.so +patchelf --set-rpath '$ORIGIN/../..' $APPDIR/usr/lib/python3.8/site-packages/PyQt5/sip.so # Step 5: Find out what version of Krita we built and give the Appimage a proper name cd $BUILD_PREFIX/krita-build KRITA_VERSION=$(grep "#define KRITA_VERSION_STRING" libs/version/kritaversion.h | cut -d '"' -f 2) # Also find out the revision of Git we built # Then use that to generate a combined name we'll distribute cd $KRITA_SOURCES if [[ -d .git ]]; then GIT_REVISION=$(git rev-parse --short HEAD) VERSION=$KRITA_VERSION-$GIT_REVISION else VERSION=$KRITA_VERSION fi # Return to our build root cd $BUILD_PREFIX # place the icon where linuxdeployqt seems to expect it find $APPDIR -name krita.png cp /home/appimage//appimage-workspace/krita.appdir/usr/share/icons/hicolor/256x256/apps/krita.png $APPDIR ls $APPDIR # Step 4: Build the image!!! linuxdeployqt $APPDIR/usr/share/applications/org.kde.krita.desktop \ -executable=$APPDIR/usr/bin/krita \ -qmldir=$DEPS_INSTALL_PREFIX/qml \ -verbose=2 \ -bundle-non-qt-libs \ -extra-plugins=$PLUGINS,$APPDIR/usr/lib/krita-python-libs/PyKrita/krita.so,$APPDIR/usr/lib//qml/org/krita/sketch/libkritasketchplugin.so,$APPDIR/usr/lib/qml/org/krita/draganddrop/libdraganddropplugin.so \ -appimage # Generate a new name for the Appimage file and rename it accordingly APPIMAGE=krita-"$VERSION"-x86_64.appimage mv Krita*x86_64.AppImage $APPIMAGE diff --git a/packaging/linux/appimage/build-krita.sh b/packaging/linux/appimage/build-krita.sh index 84288455e0..f69b53b3ac 100755 --- a/packaging/linux/appimage/build-krita.sh +++ b/packaging/linux/appimage/build-krita.sh @@ -1,52 +1,52 @@ #!/bin/bash # Halt on errors and be verbose about what we are doing set -e set -x # Read in our parameters export BUILD_PREFIX=$1 export KRITA_SOURCES=$2 # qjsonparser, used to add metadata to the plugins needs to work in a en_US.UTF-8 environment. # That's not always the case, so make sure it is export LC_ALL=en_US.UTF-8 export LANG=en_us.UTF-8 # We want to use $prefix/deps/usr/ for all our dependencies export DEPS_INSTALL_PREFIX=$BUILD_PREFIX/deps/usr/ export DOWNLOADS_DIR=$BUILD_PREFIX/downloads/ # Setup variables needed to help everything find what we build export LD_LIBRARY_PATH=$DEPS_INSTALL_PREFIX/lib:$LD_LIBRARY_PATH export PATH=$DEPS_INSTALL_PREFIX/bin:$PATH export PKG_CONFIG_PATH=$DEPS_INSTALL_PREFIX/share/pkgconfig:$DEPS_INSTALL_PREFIX/lib/pkgconfig:/usr/lib/pkgconfig:$PKG_CONFIG_PATH export CMAKE_PREFIX_PATH=$DEPS_INSTALL_PREFIX:$CMAKE_PREFIX_PATH -export PYTHONPATH=$DEPS_INSTALL_PREFIX/sip:$DEPS_INSTALL_PREFIX/lib/python3.5/site-packages:$DEPS_INSTALL_PREFIX/lib/python3.5 +export PYTHONPATH=$DEPS_INSTALL_PREFIX/sip:$DEPS_INSTALL_PREFIX/lib/python3.8/site-packages:$DEPS_INSTALL_PREFIX/lib/python3.8 export PYTHONHOME=$DEPS_INSTALL_PREFIX # Make sure our build directory exists if [ ! -d $BUILD_PREFIX/krita-build/ ] ; then mkdir -p $BUILD_PREFIX/krita-build/ fi # Now switch to it cd $BUILD_PREFIX/krita-build/ # Determine how many CPUs we have CPU_COUNT=`grep processor /proc/cpuinfo | wc -l` # Configure Krita cmake $KRITA_SOURCES \ -DCMAKE_INSTALL_PREFIX:PATH=$BUILD_PREFIX/krita.appdir/usr \ -DDEFINE_NO_DEPRECATED=1 \ -DCMAKE_BUILD_TYPE=Release \ -DFOUNDATION_BUILD=1 \ -DHIDE_SAFE_ASSERTS=ON \ -DBUILD_TESTING=FALSE \ -DPYQT_SIP_DIR_OVERRIDE=$DEPS_INSTALL_PREFIX/share/sip/ \ -DHAVE_MEMORY_LEAK_TRACKER=FALSE # Build and Install Krita (ready for the next phase) make -j$CPU_COUNT install diff --git a/packaging/macos/osxbuild.sh b/packaging/macos/osxbuild.sh index 8b4d97f5f0..66f1e3b68f 100755 --- a/packaging/macos/osxbuild.sh +++ b/packaging/macos/osxbuild.sh @@ -1,536 +1,536 @@ #!/usr/bin/env bash # osxbuild.sh automates building and installing of krita and krita dependencies # for OSX, the script only needs you to set BUILDROOT environment to work # properly. # # Run with no args for a short help about each command. # builddeps: Attempts to build krita dependencies in the necessary order, # intermediate steps for creating symlinks and fixing rpath of some # packages midway is also managed. Order goes from top to bottom, to add # new steps just place them in the proper place. # rebuilddeps: This re-runs all make and make install of dependencies 3rdparty # this was needed as deleting the entire install directory an rerunning build # step for dependencies does not install if they are already built. This step # forces installation. Have not tested it lately so it might not be needed anymore # build: Runs cmake build and make step for krita sources. It always run cmake step, so # it might take a bit longer than a pure on the source tree. The script tries # to set the make flag -jN to a proper N. # install: Runs install step for krita sources. # fixboost: Search for all libraries using boost and sets a proper @rpath for boost as by # default it fails to set a proper @rpath # buildinstall: Runs build, install and fixboost steps.# if test -z $BUILDROOT; then echo "ERROR: BUILDROOT env not set, exiting!" echo "\t Must point to the root of the buildfiles as stated in 3rdparty Readme" exit fi echo "BUILDROOT set to ${BUILDROOT}" export KIS_SRC_DIR=${BUILDROOT}/krita export KIS_TBUILD_DIR=${BUILDROOT}/depbuild export KIS_TDEPINSTALL_DIR=${BUILDROOT}/depinstall export KIS_DOWN_DIR=${BUILDROOT}/down export KIS_BUILD_DIR=${BUILDROOT}/kisbuild export KIS_INSTALL_DIR=${BUILDROOT}/i # flags for OSX environment # Qt only supports from 10.12 up, and https://doc.qt.io/qt-5/macos.html#target-platforms warns against setting it lower export MACOSX_DEPLOYMENT_TARGET=10.12 export QMAKE_MACOSX_DEPLOYMENT_TARGET=10.12 # Build time variables if test -z $(which cmake); then echo "ERROR: cmake not found, exiting!" exit fi export PATH=${KIS_INSTALL_DIR}/bin:$PATH export C_INCLUDE_PATH=${KIS_INSTALL_DIR}/include:/usr/include:${C_INCLUDE_PATH} export CPLUS_INCLUDE_PATH=${KIS_INSTALL_DIR}/include:/usr/include:${CPLUS_INCLUDE_PATH} export LIBRARY_PATH=${KIS_INSTALL_DIR}/lib:/usr/lib:${LIBRARY_PATH} # export CPPFLAGS=-I${KIS_INSTALL_DIR}/include # export LDFLAGS=-L${KIS_INSTALL_DIR}/lib export FRAMEWORK_PATH=${KIS_INSTALL_DIR}/lib/ # export PYTHONHOME=${KIS_INSTALL_DIR} -# export PYTHONPATH=${KIS_INSTALL_DIR}/sip:${KIS_INSTALL_DIR}/lib/python3.5/site-packages:${KIS_INSTALL_DIR}/lib/python3.5 +# export PYTHONPATH=${KIS_INSTALL_DIR}/sip:${KIS_INSTALL_DIR}/lib/python3.8/site-packages:${KIS_INSTALL_DIR}/lib/python3.8 # This will make the debug output prettier export KDE_COLOR_DEBUG=1 export QTEST_COLORED=1 export OUPUT_LOG="${BUILDROOT}/osxbuild.log" export ERROR_LOG="${BUILDROOT}/osxbuild-error.log" printf "" > "${OUPUT_LOG}" printf "" > "${ERROR_LOG}" # configure max core for make compile ((MAKE_THREADS=1)) if test ${OSTYPE} == "darwin*"; then ((MAKE_THREADS = $(sysctl -n hw.ncpu) - 1)) fi # Prints stderr and stdout to log files # >(tee) works but breaks sigint log_cmd () { if [[ "${VERBOSE}" ]]; then "$@" 1>> ${OUPUT_LOG} 2>> ${ERROR_LOG} else "$@" 2>> ${ERROR_LOG} | tee -a ${OUPUT_LOG} > /dev/null fi } # Log messages to logfile log () { if [[ "${VERBOSE}" ]]; then printf "%s\n" "${@}" | tee -a ${OUPUT_LOG} else printf "%s\n" "${@}" | tee -a ${OUPUT_LOG} > /dev/null fi } # if previous command gives error # print msg print_if_error() { error_stat="${?}" if [ ${error_stat} -ne 0 ]; then printf "\e[31m%s %s\e[0m\n" "Error:" "${1}" 2>> ${ERROR_LOG} printf "%s\r" "${error_stat}" fi } # print status messages print_msg() { printf "\e[32m%s\e[0m\n" "${1}" printf "%s\n" "${1}" >> ${OUPUT_LOG} } check_dir_path () { printf "%s" "Checking if ${1} exists and is dir... " if test -d ${1}; then echo -e "OK" elif test -e ${1}; then echo -e "\n\tERROR: file ${1} exists but is not a directory!" >&2 return 1 else echo -e "Creating ${1}" mkdir ${1} fi return 0 } # builds dependencies for the first time cmake_3rdparty () { cd ${KIS_TBUILD_DIR} local build_pkgs=("${@}") # convert to array if [[ ${2} = "1" ]]; then local nofix="true" local build_pkgs=(${build_pkgs[@]:0:1}) fi for package in ${build_pkgs[@]} ; do print_msg "Building ${package}" log_cmd cmake --build . --config RelWithDebInfo --target ${package} if [[ ! $(print_if_error "Failed build ${package}") ]]; then print_msg "Build Success! ${package}" fi # fixes does not depend on failure if [[ ! ${nofix} ]]; then build_3rdparty_fixes ${package} fi done } build_3rdparty_fixes(){ pkg=${1} if [[ "${pkg}" = "ext_qt" && -e "${KIS_INSTALL_DIR}/bin/qmake" ]]; then ln -sf qmake "${KIS_INSTALL_DIR}/bin/qmake-qt5" elif [[ "${pkg}" = "ext_openexr" ]]; then # open exr will fail the first time is called # rpath needs to be fixed an build rerun log "Fixing rpath on openexr file: b44ExpLogTable" log "Fixing rpath on openexr file: dwaLookups" log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib $(find ${KIS_TBUILD_DIR}/ext_openexr/ext_openexr-prefix/src/ext_openexr-build -name b44ExpLogTable) log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib $(find ${KIS_TBUILD_DIR}/ext_openexr/ext_openexr-prefix/src/ext_openexr-build -name dwaLookups) # we must rerun build! cmake_3rdparty ext_openexr "1" elif [[ "${pkg}" = "ext_fontconfig" ]]; then log "fixing rpath on fc-cache" log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib ${KIS_TBUILD_DIR}/ext_fontconfig/ext_fontconfig-prefix/src/ext_fontconfig-build/fc-cache/.libs/fc-cache # rerun rebuild cmake_3rdparty ext_fontconfig "1" fi } build_3rdparty () { print_msg "building in ${KIS_TBUILD_DIR}" log "$(check_dir_path ${KIS_TBUILD_DIR})" log "$(check_dir_path ${KIS_DOWN_DIR})" log "$(check_dir_path ${KIS_INSTALL_DIR})" cd ${KIS_TBUILD_DIR} log_cmd cmake ${KIS_SRC_DIR}/3rdparty/ \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.12 \ -DCMAKE_INSTALL_PREFIX=${KIS_INSTALL_DIR} \ -DEXTERNALS_DOWNLOAD_DIR=${KIS_DOWN_DIR} \ -DINSTALL_ROOT=${KIS_INSTALL_DIR} # -DCPPFLAGS=-I${KIS_INSTALL_DIR}/include \ # -DLDFLAGS=-L${KIS_INSTALL_DIR}/lib print_msg "finished 3rdparty build setup" if [[ -n ${1} ]]; then cmake_3rdparty "${@}" exit fi # build 3rdparty tools # The order must not be changed! cmake_3rdparty \ ext_pkgconfig \ ext_gettext \ ext_openssl \ ext_qt \ ext_zlib \ ext_boost \ ext_eigen3 \ ext_exiv2 \ ext_fftw3 \ ext_ilmbase \ ext_jpeg \ ext_lcms2 \ ext_ocio \ ext_openexr cmake_3rdparty \ ext_png \ ext_tiff \ ext_gsl \ ext_vc \ ext_libraw \ ext_giflib \ ext_freetype \ ext_fontconfig \ ext_poppler # Stop if qmake link was not created # this meant qt build fail and further builds will # also fail. log_cmd test -L "${KIS_INSTALL_DIR}/bin/qmake-qt5" if [[ $(print_if_error "qmake link missing!") ]]; then printf " link: ${KIS_INSTALL_DIR}/bin/qmake-qt5 missing! It probably means ext_qt failed!! check, fix and rerun!\n" exit fi # for python cmake_3rdparty \ ext_python \ ext_sip \ ext_pyqt cmake_3rdparty ext_libheif cmake_3rdparty \ ext_extra_cmake_modules \ ext_kconfig \ ext_kwidgetsaddons \ ext_kcompletion \ ext_kcoreaddons \ ext_kguiaddons \ ext_ki18n \ ext_kitemmodels \ ext_kitemviews \ ext_kimageformats \ ext_kwindowsystem \ ext_quazip } # Recall cmake for all 3rd party packages # make is only on target first run # subsequent runs only call make install rebuild_3rdparty () { print_msg "starting rebuild of 3rdparty packages" build_install_ext() { for pkg in ${@:1:${#@}}; do { cd ${KIS_TBUILD_DIR}/${pkg}/${pkg}-prefix/src/${pkg}-stamp } || { cd ${KIS_TBUILD_DIR}/ext_frameworks/${pkg}-prefix/src/${pkg}-stamp } || { cd ${KIS_TBUILD_DIR}/ext_heif/${pkg}-prefix/src/${pkg}-stamp } log "Installing ${pkg} files..." rm ${pkg}-configure ${pkg}-build ${pkg}-install cmake_3rdparty ${pkg} cd ${KIS_TBUILD_DIR} done } # Do not process complete list only pkgs given. if ! test -z ${1}; then build_install_ext ${@} exit fi build_install_ext \ ext_pkgconfig \ ext_iconv \ ext_gettext \ ext_openssl \ ext_qt \ ext_zlib \ ext_boost \ ext_eigen3 \ ext_expat \ ext_exiv2 \ ext_fftw3 \ ext_ilmbase \ ext_jpeg \ ext_patch \ ext_lcms2 \ ext_ocio \ ext_ilmbase \ ext_openexr \ ext_png \ ext_tiff \ ext_gsl \ ext_vc \ ext_libraw \ ext_giflib \ ext_fontconfig \ ext_freetype \ ext_poppler \ ext_python \ ext_sip \ ext_pyqt \ build_install_ext \ ext_yasm \ ext_nasm \ ext_libx265 \ ext_libde265 \ ext_libheif \ # Build kde_frameworks build_install_ext \ ext_extra_cmake_modules \ ext_kconfig \ ext_kwidgetsaddons \ ext_kcompletion \ ext_kcoreaddons \ ext_kguiaddons \ ext_ki18n \ ext_kitemmodels \ ext_kitemviews \ ext_kimageformats \ ext_kwindowsystem \ ext_quazip } #not tested set_krita_dirs() { if [[ -n ${1} ]]; then KIS_BUILD_DIR=${BUILDROOT}/b_${1} KIS_INSTALL_DIR=${BUILDROOT}/i_${1} KIS_SRC_DIR=${BUILDROOT}/src_${1} fi } # build_krita # run cmake krita build_krita () { export DYLD_FRAMEWORK_PATH=${FRAMEWORK_PATH} echo ${KIS_BUILD_DIR} echo ${KIS_INSTALL_DIR} log_cmd check_dir_path ${KIS_BUILD_DIR} cd ${KIS_BUILD_DIR} cmake ${KIS_SRC_DIR} \ -DFOUNDATION_BUILD=ON \ -DBoost_INCLUDE_DIR=${KIS_INSTALL_DIR}/include \ -DCMAKE_INSTALL_PREFIX=${KIS_INSTALL_DIR} \ -DDEFINE_NO_DEPRECATED=1 \ -DBUILD_TESTING=OFF \ -DHIDE_SAFE_ASSERTS=ON \ -DKDE_INSTALL_BUNDLEDIR=${KIS_INSTALL_DIR}/bin \ -DPYQT_SIP_DIR_OVERRIDE=${KIS_INSTALL_DIR}/share/sip/ \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.12 \ -DPYTHON_INCLUDE_DIR=${KIS_INSTALL_DIR}/lib/Python.framework/Headers # copiling phase make -j${MAKE_THREADS} # compile integrations if test ${OSTYPE} == "darwin*"; then cd ${KIS_BUILD_DIR}/krita/integration/kritaquicklook make -j${MAKE_THREADS} fi } build_krita_tarball () { filename="$(basename ${1})" KIS_CUSTOM_BUILD="${BUILDROOT}/releases/${filename%.tar.gz}" print_msg "Tarball BUILDROOT is ${KIS_CUSTOM_BUILD}" filename_dir=$(dirname "${1}") cd "${filename_dir}" file_abspath="$(pwd)/${1##*/}" mkdir "${KIS_CUSTOM_BUILD}" 2> /dev/null cd "${KIS_CUSTOM_BUILD}" mkdir "src" "build" 2> /dev/null tar -xzf "${file_abspath}" --strip-components=1 --directory "src" if [[ $(print_if_error "Untar ${file_abspath} failed!") ]]; then exit fi KIS_BUILD_DIR="${KIS_CUSTOM_BUILD}/build" KIS_SRC_DIR="${KIS_CUSTOM_BUILD}/src" build_krita print_msg "Build done!" print_msg "to install run osxbuild.sh install ${KIS_BUILD_DIR}" } install_krita () { # custom install provided if [[ -n "${1}" ]]; then KIS_BUILD_DIR="${1}" fi print_msg "Install krita from ${KIS_BUILD_DIR}" log_cmd check_dir_path ${KIS_BUILD_DIR} cd ${KIS_BUILD_DIR} if [[ $(print_if_error "could not cd to ${KIS_BUILD_DIR}") ]]; then exit fi make install # compile integrations if test ${OSTYPE} == "darwin*"; then cd ${KIS_BUILD_DIR}/krita/integration/kritaquicklook make install fi } # Runs all fixes for path and packages. # Historically only fixed boost @rpath fix_boost_rpath () { # helpers to define function only once fixboost_find () { for FILE in "${@}"; do if [[ -n "$(otool -L $FILE | grep boost)" ]]; then log "Fixing -- $FILE" log_cmd install_name_tool -change libboost_system.dylib @rpath/libboost_system.dylib $FILE fi done } batch_fixboost() { xargs -P4 -I FILE bash -c 'fixboost_find "FILE"' } export -f fixboost_find export -f log export -f log_cmd print_msg "Fixing boost in... ${KIS_INSTALL_DIR}" # install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib $BUILDROOT/$KRITA_INSTALL/bin/krita.app/Contents/MacOS/gmic_krita_qt log_cmd install_name_tool -add_rpath ${KIS_INSTALL_DIR}/lib ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita # echo "Added rpath ${KIS_INSTALL_DIR}/lib to krita bin" # install_name_tool -add_rpath ${BUILDROOT}/deps/lib ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita log_cmd install_name_tool -change libboost_system.dylib @rpath/libboost_system.dylib ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita find -L "${KIS_INSTALL_DIR}" -name '*so' -o -name '*dylib' | batch_fixboost log "Fixing boost done!" } print_usage () { printf "USAGE: osxbuild.sh [pkg|file]\n" printf "BUILDSTEPS:\t\t" printf "\n builddeps \t\t Run cmake step for 3rd party dependencies, optionally takes a [pkg] arg" printf "\n rebuilddeps \t\t Rerun make and make install step for 3rd party deps, optionally takes a [pkg] arg \t\t\t useful for cleaning install directory and quickly reinstall all deps." printf "\n fixboost \t\t Fixes broken boost \@rpath on OSX" printf "\n build \t\t\t Builds krita" printf "\n buildtarball \t\t\t Builds krita from provided [file] tarball" printf "\n install \t\t Installs krita. Optionally accepts a [build dir] as argument \t\t\t this will install krita from given directory" printf "\n buildinstall \t\t Build and Installs krita, running fixboost after installing" printf "\n" } if [[ ${#} -eq 0 ]]; then echo "ERROR: No option given!" print_usage exit 1 fi if [[ ${1} = "builddeps" ]]; then build_3rdparty "${@:2}" elif [[ ${1} = "rebuilddeps" ]]; then rebuild_3rdparty "${@:2}" elif [[ ${1} = "fixboost" ]]; then if [[ -d ${1} ]]; then KIS_BUILD_DIR="${1}" fi fix_boost_rpath elif [[ ${1} = "build" ]]; then build_krita ${2} elif [[ ${1} = "buildtarball" ]]; then # uncomment line to optionally change # install directory providing a third argument # This is not on by default as build success requires all # deps installed in the given dir beforehand. # KIS_INSTALL_DIR=${3} build_krita_tarball ${2} elif [[ ${1} = "install" ]]; then install_krita ${2} fix_boost_rpath elif [[ ${1} = "buildinstall" ]]; then build_krita ${2} install_krita ${2} fix_boost_rpath ${2} elif [[ ${1} = "test" ]]; then ${KIS_INSTALL_DIR}/bin/krita.app/Contents/MacOS/krita else echo "Option ${1} not supported" print_usage exit 1 fi # after finishig sometimes it complains about missing matching quotes. diff --git a/plugins/dockers/advancedcolorselector/kis_shade_selector_line_combo_box.cpp b/plugins/dockers/advancedcolorselector/kis_shade_selector_line_combo_box.cpp index 4332469cf7..f045cf11fc 100644 --- a/plugins/dockers/advancedcolorselector/kis_shade_selector_line_combo_box.cpp +++ b/plugins/dockers/advancedcolorselector/kis_shade_selector_line_combo_box.cpp @@ -1,179 +1,180 @@ /* * Copyright (c) 2010 Adam Celarek * * 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 "kis_shade_selector_line_combo_box.h" #include #include +#include #include #include #include #include "kis_shade_selector_line.h" #include "kis_shade_selector_line_combo_box_popup.h" #include "kis_color_selector_base_proxy.h" #include "kis_global.h" KisShadeSelectorLineComboBox::KisShadeSelectorLineComboBox(QWidget *parent) : QComboBox(parent), m_popup(new KisShadeSelectorLineComboBoxPopup(this)), m_parentProxy(new KisColorSelectorBaseProxyNoop()), m_currentLine(new KisShadeSelectorLine(0,0,0, m_parentProxy.data(), this)) { QGridLayout* l = new QGridLayout(this); l->addWidget(m_currentLine); m_currentLine->setEnabled(false); KoColor color; color.fromQColor(QColor(190, 50, 50)); m_currentLine->setColor(color); updateSettings(); } KisShadeSelectorLineComboBox::~KisShadeSelectorLineComboBox() { } void KisShadeSelectorLineComboBox::hidePopup() { QComboBox::hidePopup(); m_popup->hide(); } void KisShadeSelectorLineComboBox::showPopup() { QComboBox::showPopup(); m_popup->show(); const int widgetMargin = 20; #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QScreen *screen = qApp->screenAt(this->geometry().topLeft()); const QRect fitRect = kisGrowRect(screen->availableGeometry(), -widgetMargin); #else const QRect fitRect = kisGrowRect(QApplication::desktop()->screenGeometry(), -widgetMargin); #endif QRect popupRect = m_popup->rect(); popupRect.moveTo(mapToGlobal(QPoint())); popupRect = kisEnsureInRect(popupRect, fitRect); m_popup->move(popupRect.topLeft()); m_popup->setConfiguration(m_currentLine->toString()); } void KisShadeSelectorLineComboBox::setConfiguration(const QString &stri) { m_currentLine->fromString(stri); update(); } QString KisShadeSelectorLineComboBox::configuration() const { return m_currentLine->toString(); } void KisShadeSelectorLineComboBox::setLineNumber(int n) { m_currentLine->setLineNumber(n); for(int i=0; ilayout()->count(); i++) { KisShadeSelectorLine* item = dynamic_cast(m_popup->layout()->itemAt(i)->widget()); if(item!=0) { item->setLineNumber(n); } } } void KisShadeSelectorLineComboBox::resizeEvent(QResizeEvent *e) { Q_UNUSED(e); m_currentLine->setMaximumWidth(width()-30-m_popup->spacing); m_popup->setMinimumWidth(qMax(280, width())); m_popup->setMaximumWidth(qMax(280, width())); } void KisShadeSelectorLineComboBox::updateSettings() { m_currentLine->updateSettings(); for(int i=0; ilayout()->count(); i++) { KisShadeSelectorLine* item = dynamic_cast(m_popup->layout()->itemAt(i)->widget()); if(item!=0) { item->updateSettings(); item->m_lineHeight=30; item->setMaximumHeight(30); item->setMinimumHeight(30); } } setLineHeight(m_currentLine->m_lineHeight); } void KisShadeSelectorLineComboBox::setGradient(bool b) { m_currentLine->m_gradient=b; for(int i=0; ilayout()->count(); i++) { KisShadeSelectorLine* item = dynamic_cast(m_popup->layout()->itemAt(i)->widget()); if(item!=0) { item->m_gradient=b; } } update(); } void KisShadeSelectorLineComboBox::setPatches(bool b) { m_currentLine->m_gradient=!b; for(int i=0; ilayout()->count(); i++) { KisShadeSelectorLine* item = dynamic_cast(m_popup->layout()->itemAt(i)->widget()); if(item!=0) { item->m_gradient=!b; } } update(); } void KisShadeSelectorLineComboBox::setPatchCount(int count) { m_currentLine->m_patchCount=count; for(int i=0; ilayout()->count(); i++) { KisShadeSelectorLine* item = dynamic_cast(m_popup->layout()->itemAt(i)->widget()); if(item!=0) { item->m_patchCount=count; } } update(); } void KisShadeSelectorLineComboBox::setLineHeight(int height) { m_currentLine->m_lineHeight=height; m_currentLine->setMinimumHeight(height); m_currentLine->setMaximumHeight(height); setMinimumHeight(height+m_popup->spacing); setMaximumHeight(height+m_popup->spacing); update(); } diff --git a/plugins/dockers/historydocker/HistoryDock.cpp b/plugins/dockers/historydocker/HistoryDock.cpp index 13ffa6230e..14470dfbb4 100644 --- a/plugins/dockers/historydocker/HistoryDock.cpp +++ b/plugins/dockers/historydocker/HistoryDock.cpp @@ -1,85 +1,84 @@ /* This file is part of the KDE project * Copyright (C) 2010 Matus Talcik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "HistoryDock.h" #include #include #include #include #include #include #include #include #include HistoryDock::HistoryDock() : QDockWidget() , m_historyCanvas(0) { QWidget *page = new QWidget(this); QVBoxLayout *vl = new QVBoxLayout(page); m_undoView = new KisUndoView(this); vl->addWidget(m_undoView); QHBoxLayout *hl = new QHBoxLayout(); hl->addSpacerItem(new QSpacerItem(10, 1, QSizePolicy::Expanding, QSizePolicy::Fixed)); m_bnConfigure = new QToolButton(page); m_bnConfigure->setIcon(KisIconUtils::loadIcon("configure")); connect(m_bnConfigure, SIGNAL(clicked(bool)), SLOT(configure())); hl->addWidget(m_bnConfigure); - vl->addItem(hl); vl->addLayout(hl); setWidget(page); setWindowTitle(i18n("Undo History")); } void HistoryDock::setCanvas(KoCanvasBase *canvas) { setEnabled(canvas != 0); QPointer myCanvas = dynamic_cast(canvas); if (myCanvas && myCanvas->shapeController() && myCanvas->shapeController()->resourceManager() && myCanvas->shapeController()->resourceManager()->undoStack()) { KUndo2Stack* undoStack = myCanvas->shapeController()->resourceManager()->undoStack(); m_undoView->setStack(undoStack); KisConfig cfg(true); m_undoView->stack()->setUseCumulativeUndoRedo(cfg.useCumulativeUndoRedo()); m_undoView->stack()->setTimeT1(cfg.stackT1()); m_undoView->stack()->setTimeT2(cfg.stackT2()); m_undoView->stack()->setStrokesN(cfg.stackN()); } m_undoView->setCanvas( myCanvas ); } void HistoryDock::configure() { DlgConfigureHistoryDock dlg(m_undoView, m_undoView->stack(), this); dlg.exec(); } void HistoryDock::unsetCanvas() { m_historyCanvas = 0; setEnabled(false); m_undoView->setStack(0); } diff --git a/plugins/extensions/pykrita/plugin/utilities.cpp b/plugins/extensions/pykrita/plugin/utilities.cpp index 15f896eb12..c4f099fe17 100644 --- a/plugins/extensions/pykrita/plugin/utilities.cpp +++ b/plugins/extensions/pykrita/plugin/utilities.cpp @@ -1,706 +1,710 @@ // This file is part of PyKrita, Krita' Python scripting plugin. // // Copyright (C) 2006 Paul Giannaros // Copyright (C) 2012, 2013 Shaheed Haque // // 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; either // version 2.1 of the License, or (at your option) version 3, or any // later version accepted by the membership of KDE e.V. (or its // successor approved by the membership of KDE e.V.), which shall // act as a proxy defined in Section 6 of version 3 of the license. // // 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 library. If not, see . // // config.h defines PYKRITA_PYTHON_LIBRARY, the path to libpython.so // on the build system #include "config.h" #include "utilities.h" #include "PythonPluginManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "PykritaModule.h" #define THREADED 1 namespace PyKrita { static InitResult initStatus = INIT_UNINITIALIZED; static QScopedPointer pluginManagerInstance; InitResult initialize() { // Already initialized? if (initStatus == INIT_OK) return INIT_OK; dbgScript << "Initializing Python plugin for Python" << PY_MAJOR_VERSION << "," << PY_MINOR_VERSION; if (!Python::libraryLoad()) { return INIT_CANNOT_LOAD_PYTHON_LIBRARY; } // Update PYTHONPATH // 0) custom plugin directories (prefer local dir over systems') // 1) shipped krita module's dir QStringList pluginDirectories = KoResourcePaths::findDirs("pythonscripts"); dbgScript << "Plugin Directories: " << pluginDirectories; if (!Python::setPath(pluginDirectories)) { initStatus = INIT_CANNOT_SET_PYTHON_PATHS; return initStatus; } #if defined(IS_PY3K) if (0 != PyImport_AppendInittab(Python::PYKRITA_ENGINE, PyInit_pykrita)) { #else if (0 != PyImport_AppendInittab(Python::PYKRITA_ENGINE, initpykrita)) { #endif initStatus = INIT_CANNOT_LOAD_PYKRITA_MODULE; return initStatus; } Python::ensureInitialized(); Python py = Python(); // NOTE: This code is not needed on Python 3. // This might also fail if private sip module for PyQt5 is used, // as required by newer PyQt5. It's fine since any exceptions // in this script will be ignored. PyRun_SimpleString( "import sip\n" "sip.setapi('QDate', 2)\n" "sip.setapi('QTime', 2)\n" "sip.setapi('QDateTime', 2)\n" "sip.setapi('QUrl', 2)\n" "sip.setapi('QTextStream', 2)\n" "sip.setapi('QString', 2)\n" "sip.setapi('QVariant', 2)\n" ); // Initialize 'plugins' dict of module 'pykrita' PyObject* plugins = PyDict_New(); py.itemStringSet("plugins", plugins); pluginManagerInstance.reset(new PythonPluginManager()); #if defined(IS_PY3K) // Initialize our built-in module. auto pykritaModule = PyInit_pykrita(); if (!pykritaModule) { initStatus = INIT_CANNOT_LOAD_PYKRITA_MODULE; return initStatus; //return i18nc("@info:tooltip ", "No pykrita built-in module"); } #else initpykrita(); #endif initStatus = INIT_OK; return initStatus; } PythonPluginManager *pluginManager() { auto pluginManager = pluginManagerInstance.data(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(pluginManager, nullptr); return pluginManager; } void finalize() { dbgScript << "Going to destroy the Python engine"; if (pluginManagerInstance) { pluginManagerInstance->unloadAllModules(); PyKrita::Python::maybeFinalize(); PyKrita::Python::libraryUnload(); pluginManagerInstance.reset(); initStatus = INIT_UNINITIALIZED; } } namespace { #ifndef Q_OS_WIN QLibrary* s_pythonLibrary = 0; #endif PyThreadState* s_pythonThreadState = 0; bool isPythonPathSet = false; } // anonymous namespace const char* Python::PYKRITA_ENGINE = "pykrita"; Python::Python() { #if THREADED m_state = PyGILState_Ensure(); #endif } Python::~Python() { #if THREADED PyGILState_Release(m_state); #endif } bool Python::prependStringToList(PyObject* const list, const QString& value) { PyObject* const u = unicode(value); bool result = !PyList_Insert(list, 0, u); Py_DECREF(u); if (!result) traceback(QString("Failed to prepend %1").arg(value)); return result; } bool Python::functionCall(const char* const functionName, const char* const moduleName) { PyObject* const result = functionCall(functionName, moduleName, PyTuple_New(0)); if (result) Py_DECREF(result); return bool(result); } PyObject* Python::functionCall( const char* const functionName , const char* const moduleName , PyObject* const arguments ) { if (!arguments) { errScript << "Missing arguments for" << moduleName << functionName; return 0; } PyObject* const func = itemString(functionName, moduleName); if (!func) { errScript << "Failed to resolve" << moduleName << functionName; return 0; } if (!PyCallable_Check(func)) { traceback(QString("Not callable %1.%2").arg(moduleName).arg(functionName)); return 0; } PyObject* const result = PyObject_CallObject(func, arguments); Py_DECREF(arguments); if (!result) traceback(QString("No result from %1.%2").arg(moduleName).arg(functionName)); return result; } bool Python::itemStringDel(const char* const item, const char* const moduleName) { PyObject* const dict = moduleDict(moduleName); const bool result = dict && PyDict_DelItemString(dict, item); if (!result) traceback(QString("Could not delete item string %1.%2").arg(moduleName).arg(item)); return result; } PyObject* Python::itemString(const char* const item, const char* const moduleName) { if (PyObject* const value = itemString(item, moduleDict(moduleName))) return value; errScript << "Could not get item string" << moduleName << item; return 0; } PyObject* Python::itemString(const char* item, PyObject* dict) { if (dict) if (PyObject* const value = PyDict_GetItemString(dict, item)) return value; traceback(QString("Could not get item string %1").arg(item)); return 0; } bool Python::itemStringSet(const char* const item, PyObject* const value, const char* const moduleName) { PyObject* const dict = moduleDict(moduleName); const bool result = dict && !PyDict_SetItemString(dict, item, value); if (!result) traceback(QString("Could not set item string %1.%2").arg(moduleName).arg(item)); return result; } PyObject* Python::kritaHandler(const char* const moduleName, const char* const handler) { if (PyObject* const module = moduleImport(moduleName)) return functionCall(handler, "krita", Py_BuildValue("(O)", module)); return 0; } QString Python::lastTraceback() const { QString result; result.swap(m_traceback); return result; } bool Python::libraryLoad() { // no-op on Windows #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) if (!s_pythonLibrary) { QFileInfo fi(PYKRITA_PYTHON_LIBRARY); // get the filename of the configured Python library, without the .so suffix const QString libraryName = fi.completeBaseName(); // 1.0 is the SONAME of the shared Python library s_pythonLibrary = new QLibrary(libraryName, "1.0"); s_pythonLibrary->setLoadHints(QLibrary::ExportExternalSymbolsHint); if (!s_pythonLibrary->load()) { dbgScript << QString("Could not load %1 -- Reason: %2").arg(s_pythonLibrary->fileName()).arg(s_pythonLibrary->errorString()); delete s_pythonLibrary; s_pythonLibrary = 0; return false; } dbgScript << QString("Loaded %1").arg(s_pythonLibrary->fileName()); } #endif return true; } namespace { QString findKritaPythonLibsPath(const QString &libdir) { QDir rootDir(KoResourcePaths::getApplicationRoot()); - QFileInfoList candidates = rootDir.entryInfoList(QStringList() << "lib*", QDir::Dirs | QDir::NoDotAndDotDot) + rootDir.entryInfoList(QStringList() << "Frameworks", QDir::Dirs | QDir::NoDotAndDotDot); + + QFileInfoList candidates = + rootDir.entryInfoList(QStringList() << "lib*", QDir::Dirs | QDir::NoDotAndDotDot) + + rootDir.entryInfoList(QStringList() << "Frameworks", QDir::Dirs | QDir::NoDotAndDotDot) + + rootDir.entryInfoList(QStringList() << "share", QDir::Dirs | QDir::NoDotAndDotDot); Q_FOREACH (const QFileInfo &entry, candidates) { QDir libDir(entry.absoluteFilePath()); if (libDir.cd(libdir)) { return libDir.absolutePath(); } else { // Handle cases like Linux where libs are placed in a sub-dir // with the ABI name Q_FOREACH (const QFileInfo &subEntry, libDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { QDir subDir(subEntry.absoluteFilePath()); if (subDir.cd(libdir)) { return subDir.absolutePath(); } } } } return QString(); } } // namespace bool Python::setPath(const QStringList& scriptPaths) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!Py_IsInitialized(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!isPythonPathSet, false); // qDebug() << ">>>>>>>>>>>" << qgetenv("APPDIR") // << KoResourcePaths::getApplicationRoot() // << (!qgetenv("APPDIR").isNull() && KoResourcePaths::getApplicationRoot().contains(qgetenv("APPDIR"))); bool runningInBundle = ((!qgetenv("APPDIR").isNull() && KoResourcePaths::getApplicationRoot().contains(qgetenv("APPDIR"))) || KoResourcePaths::getApplicationRoot().toLower().contains("krita.app")); dbgScript << "Python::setPath. Script paths:" << scriptPaths << runningInBundle; #ifdef Q_OS_WIN constexpr char pathSeparator = ';'; #else constexpr char pathSeparator = ':'; #endif QString originalPath; // Start with the script paths QStringList paths(scriptPaths); // Append the Krita libraries path QString pythonLibsPath = findKritaPythonLibsPath("krita-python-libs"); dbgScript << "pythonLibsPath (krita-python-libs)" << pythonLibsPath; if (pythonLibsPath.isEmpty()) { dbgScript << "Cannot find krita-python-libs"; return false; } dbgScript << "Found krita-python-libs at" << pythonLibsPath; paths.append(pythonLibsPath); #ifndef Q_OS_WIN // Append the sip libraries path pythonLibsPath = findKritaPythonLibsPath("sip"); dbgScript << "pythonLibsPath (sip)" << pythonLibsPath; if (!pythonLibsPath.isEmpty()) { dbgScript << "Found sip at" << pythonLibsPath; paths.append(pythonLibsPath); } #endif #ifdef Q_OS_WIN // Find embeddable Python at /python QDir pythonDir(KoResourcePaths::getApplicationRoot()); if (pythonDir.cd("python")) { dbgScript << "Found bundled Python at" << pythonDir.absolutePath(); // The default paths for Windows embeddable Python is ./python38.zip;./ // HACK: Assuming bundled Python is version 3.8.* // FIXME: Should we read python36._pth for the paths or use Py_GetPath? paths.append(pythonDir.absoluteFilePath("python38.zip")); paths.append(pythonDir.absolutePath()); } else { errScript << "Bundled Python not found, cannot set Python library paths"; return false; } #else // If using a system Python install, respect the current PYTHONPATH if (runningInBundle) { // We're running from an appimage, so we need our local python QString p = QFileInfo(PYKRITA_PYTHON_LIBRARY).fileName(); #ifdef Q_OS_MAC - QString p2 = p.remove("lib").remove("m.dy"); + QString p2 = p.remove("lib").remove("m.dy").remove(".dy"); #else - QString p2 = p.remove("lib").remove("m.so"); + QString p2 = p.remove("lib").remove("m.so").remove(".so"); #endif dbgScript << "\t" << p << p2; originalPath = findKritaPythonLibsPath(p); #ifdef Q_OS_MAC // Are we running with a system Python library instead? if (originalPath.isEmpty()) { // Keep the original Python search path. originalPath = QString::fromWCharArray(Py_GetPath()); QString d = QFileInfo(PYKRITA_PYTHON_LIBRARY).absolutePath(); paths.append(d + "/" + p2 + "/site-packages"); paths.append(d + "/" + p2 + "/site-packages/PyQt5"); } else { #endif paths.append(originalPath + "/lib-dynload"); paths.append(originalPath + "/site-packages"); paths.append(originalPath + "/site-packages/PyQt5"); #ifdef Q_OS_MAC } #endif } else { // Use the system path originalPath = QString::fromLocal8Bit(qgetenv("PYTHONPATH")); } #endif QString joinedPaths = paths.join(pathSeparator); if (!originalPath.isEmpty()) { joinedPaths = joinedPaths + pathSeparator + originalPath; } dbgScript << "Setting python paths:" << joinedPaths; #ifdef Q_OS_WIN QVector joinedPathsWChars(joinedPaths.size() + 1, 0); joinedPaths.toWCharArray(joinedPathsWChars.data()); Py_SetPath(joinedPathsWChars.data()); #else if (runningInBundle) { QVector joinedPathsWChars(joinedPaths.size() + 1, 0); joinedPaths.toWCharArray(joinedPathsWChars.data()); Py_SetPath(joinedPathsWChars.data()); } else { qputenv("PYTHONPATH", joinedPaths.toLocal8Bit()); } #endif isPythonPathSet = true; return true; } void Python::ensureInitialized() { if (Py_IsInitialized()) { warnScript << "Python interpreter is already initialized, not initializing again"; } else { dbgScript << "Initializing Python interpreter"; Py_InitializeEx(0); if (!Py_IsInitialized()) { errScript << "Could not initialize Python interpreter"; } #if THREADED PyEval_InitThreads(); s_pythonThreadState = PyGILState_GetThisThreadState(); PyEval_ReleaseThread(s_pythonThreadState); #endif } } void Python::maybeFinalize() { if (!Py_IsInitialized()) { warnScript << "Python interpreter not initialized, no need to finalize"; } else { #if THREADED PyEval_AcquireThread(s_pythonThreadState); #endif Py_Finalize(); } } void Python::libraryUnload() { // no-op on Windows #ifndef Q_OS_WIN if (s_pythonLibrary) { // Shut the interpreter down if it has been started. if (s_pythonLibrary->isLoaded()) { s_pythonLibrary->unload(); } delete s_pythonLibrary; s_pythonLibrary = 0; } #endif } PyObject* Python::moduleActions(const char* moduleName) { return kritaHandler(moduleName, "moduleGetActions"); } PyObject* Python::moduleConfigPages(const char* const moduleName) { return kritaHandler(moduleName, "moduleGetConfigPages"); } QString Python::moduleHelp(const char* moduleName) { QString r; PyObject* const result = kritaHandler(moduleName, "moduleGetHelp"); if (result) { r = unicode(result); Py_DECREF(result); } return r; } PyObject* Python::moduleDict(const char* const moduleName) { PyObject* const module = moduleImport(moduleName); if (module) if (PyObject* const dictionary = PyModule_GetDict(module)) return dictionary; traceback(QString("Could not get dict %1").arg(moduleName)); return 0; } PyObject* Python::moduleImport(const char* const moduleName) { PyObject* const module = PyImport_ImportModule(moduleName); if (module) return module; traceback(QString("Could not import %1").arg(moduleName)); return 0; } // Inspired by https://lists.gt.net/python/python/150924. void Python::traceback(const QString& description) { m_traceback.clear(); if (!PyErr_Occurred()) // Return an empty string on no error. // NOTE "Return a string?" really?? return; PyObject* exc_typ; PyObject* exc_val; PyObject* exc_tb; PyErr_Fetch(&exc_typ, &exc_val, &exc_tb); PyErr_NormalizeException(&exc_typ, &exc_val, &exc_tb); // Include the traceback. if (exc_tb) { m_traceback = "Traceback (most recent call last):\n"; PyObject* const arguments = PyTuple_New(1); PyTuple_SetItem(arguments, 0, exc_tb); PyObject* const result = functionCall("format_tb", "traceback", arguments); if (result) { for (int i = 0, j = PyList_Size(result); i < j; i++) { PyObject* const tt = PyList_GetItem(result, i); PyObject* const t = Py_BuildValue("(O)", tt); char* buffer; if (!PyArg_ParseTuple(t, "s", &buffer)) break; m_traceback += buffer; } Py_DECREF(result); } Py_DECREF(exc_tb); } // Include the exception type and value. if (exc_typ) { PyObject* const temp = PyObject_GetAttrString(exc_typ, "__name__"); if (temp) { m_traceback += unicode(temp); m_traceback += ": "; } Py_DECREF(exc_typ); } if (exc_val) { PyObject* const temp = PyObject_Str(exc_val); if (temp) { m_traceback += unicode(temp); m_traceback += "\n"; } Py_DECREF(exc_val); } m_traceback += description; QStringList l = m_traceback.split("\n"); Q_FOREACH(const QString &s, l) { errScript << s; } /// \todo How about to show it somewhere else than "console output"? } PyObject* Python::unicode(const QString& string) { #if PY_MAJOR_VERSION < 3 /* Python 2.x. https://docs.python.org/2/c-api/unicode.html */ PyObject* s = PyString_FromString(PQ(string)); PyObject* u = PyUnicode_FromEncodedObject(s, "utf-8", "strict"); Py_DECREF(s); return u; #elif PY_MINOR_VERSION < 3 /* Python 3.2 or less. https://docs.python.org/3.2/c-api/unicode.html#unicode-objects */ # ifdef Py_UNICODE_WIDE return PyUnicode_DecodeUTF16((const char*)string.constData(), string.length() * 2, 0, 0); # else return PyUnicode_FromUnicode(string.constData(), string.length()); # endif #else /* Python 3.3 or greater. https://docs.python.org/3.3/c-api/unicode.html#unicode-objects */ return PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND, string.constData(), string.length()); #endif } QString Python::unicode(PyObject* const string) { #if PY_MAJOR_VERSION < 3 /* Python 2.x. https://docs.python.org/2/c-api/unicode.html */ if (PyString_Check(string)) return QString(PyString_AsString(string)); else if (PyUnicode_Check(string)) { const int unichars = PyUnicode_GetSize(string); # ifdef HAVE_USABLE_WCHAR_T return QString::fromWCharArray(PyUnicode_AsUnicode(string), unichars); # else # ifdef Py_UNICODE_WIDE return QString::fromUcs4((const unsigned int*)PyUnicode_AsUnicode(string), unichars); # else return QString::fromUtf16(PyUnicode_AsUnicode(string), unichars); # endif # endif } else return QString(); #elif PY_MINOR_VERSION < 3 /* Python 3.2 or less. https://docs.python.org/3.2/c-api/unicode.html#unicode-objects */ if (!PyUnicode_Check(string)) return QString(); const int unichars = PyUnicode_GetSize(string); # ifdef HAVE_USABLE_WCHAR_T return QString::fromWCharArray(PyUnicode_AsUnicode(string), unichars); # else # ifdef Py_UNICODE_WIDE return QString::fromUcs4(PyUnicode_AsUnicode(string), unichars); # else return QString::fromUtf16(PyUnicode_AsUnicode(string), unichars); # endif # endif #else /* Python 3.3 or greater. https://docs.python.org/3.3/c-api/unicode.html#unicode-objects */ if (!PyUnicode_Check(string)) return QString(); const int unichars = PyUnicode_GetLength(string); if (0 != PyUnicode_READY(string)) return QString(); switch (PyUnicode_KIND(string)) { case PyUnicode_1BYTE_KIND: return QString::fromLatin1((const char*)PyUnicode_1BYTE_DATA(string), unichars); case PyUnicode_2BYTE_KIND: return QString::fromUtf16(PyUnicode_2BYTE_DATA(string), unichars); case PyUnicode_4BYTE_KIND: return QString::fromUcs4(PyUnicode_4BYTE_DATA(string), unichars); default: break; } return QString(); #endif } bool Python::isUnicode(PyObject* const string) { #if PY_MAJOR_VERSION < 3 return PyString_Check(string) || PyUnicode_Check(string); #else return PyUnicode_Check(string); #endif } bool Python::prependPythonPaths(const QString& path) { PyObject* sys_path = itemString("path", "sys"); return bool(sys_path) && prependPythonPaths(path, sys_path); } bool Python::prependPythonPaths(const QStringList& paths) { PyObject* sys_path = itemString("path", "sys"); if (!sys_path) return false; /// \todo Heh, boosts' range adaptors would be good here! QStringList reversed_paths; std::reverse_copy( paths.begin() , paths.end() , std::back_inserter(reversed_paths) ); Q_FOREACH(const QString & path, reversed_paths) if (!prependPythonPaths(path, sys_path)) return false; return true; } bool Python::prependPythonPaths(const QString& path, PyObject* sys_path) { Q_ASSERT("Dir entry expected to be valid" && sys_path); return bool(prependStringToList(sys_path, path)); } } // namespace PyKrita // krita: indent-width 4; diff --git a/plugins/impex/raw/3rdparty/libkdcraw/src/rwidgetutils.cpp b/plugins/impex/raw/3rdparty/libkdcraw/src/rwidgetutils.cpp index 53a05b166d..6d6d561bc7 100644 --- a/plugins/impex/raw/3rdparty/libkdcraw/src/rwidgetutils.cpp +++ b/plugins/impex/raw/3rdparty/libkdcraw/src/rwidgetutils.cpp @@ -1,631 +1,632 @@ /** =========================================================== * @file * * This file is a part of digiKam project * https://www.digikam.org * * @date 2014-09-12 * @brief Simple helper widgets collection * * @author Copyright (C) 2014-2015 by Gilles Caulier * caulier dot gilles at gmail dot com * * 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, 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. * * ============================================================ */ #include "rwidgetutils.h" // Qt includes #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include // KDE includes #include // Local includes #include "libkdcraw_debug.h" namespace KDcrawIface { RActiveLabel::RActiveLabel(const QUrl& url, const QString& imgPath, QWidget* const parent) : QLabel(parent) { setMargin(0); setScaledContents(false); setOpenExternalLinks(true); setTextFormat(Qt::RichText); setFocusPolicy(Qt::NoFocus); setTextInteractionFlags(Qt::LinksAccessibleByMouse); setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); QImage img = QImage(imgPath); updateData(url, img); } RActiveLabel::~RActiveLabel() { } void RActiveLabel::updateData(const QUrl& url, const QImage& img) { QByteArray byteArray; QBuffer buffer(&byteArray); img.save(&buffer, "PNG"); setText(QString::fromLatin1("%2") .arg(url.url()) .arg(QString::fromLatin1("") .arg(QString::fromLatin1(byteArray.toBase64().data())))); } // ------------------------------------------------------------------------------------ RLineWidget::RLineWidget(Qt::Orientation orientation, QWidget* const parent) : QFrame(parent) { setLineWidth(1); setMidLineWidth(0); if (orientation == Qt::Vertical) { setFrameShape(QFrame::VLine); setFrameShadow(QFrame::Sunken); setMinimumSize(2, 0); } else { setFrameShape(QFrame::HLine); setFrameShadow(QFrame::Sunken); setMinimumSize(0, 2); } updateGeometry(); } RLineWidget::~RLineWidget() { } // ------------------------------------------------------------------------------------ RHBox::RHBox(QWidget* const parent) : QFrame(parent) { QHBoxLayout* const layout = new QHBoxLayout(this); layout->setSpacing(0); layout->setMargin(0); setLayout(layout); } RHBox::RHBox(bool /*vertical*/, QWidget* const parent) : QFrame(parent) { QVBoxLayout* const layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setMargin(0); setLayout(layout); } RHBox::~RHBox() { } void RHBox::childEvent(QChildEvent* e) { switch (e->type()) { case QEvent::ChildAdded: { QChildEvent* const ce = static_cast(e); if (ce->child()->isWidgetType()) { QWidget* const w = static_cast(ce->child()); static_cast(layout())->addWidget(w); } break; } case QEvent::ChildRemoved: { QChildEvent* const ce = static_cast(e); if (ce->child()->isWidgetType()) { QWidget* const w = static_cast(ce->child()); static_cast(layout())->removeWidget(w); } break; } default: break; } QFrame::childEvent(e); } QSize RHBox::sizeHint() const { RHBox* const b = const_cast(this); QApplication::sendPostedEvents(b, QEvent::ChildAdded); return QFrame::sizeHint(); } QSize RHBox::minimumSizeHint() const { RHBox* const b = const_cast(this); QApplication::sendPostedEvents(b, QEvent::ChildAdded ); return QFrame::minimumSizeHint(); } void RHBox::setSpacing(int spacing) { layout()->setSpacing(spacing); } void RHBox::setMargin(int margin) { layout()->setMargin(margin); } void RHBox::setStretchFactor(QWidget* const widget, int stretch) { static_cast(layout())->setStretchFactor(widget, stretch); } // ------------------------------------------------------------------------------------ RVBox::RVBox(QWidget* const parent) : RHBox(true, parent) { } RVBox::~RVBox() { } // ------------------------------------------------------------------------------------ class Q_DECL_HIDDEN RAdjustableLabel::Private { public: Private() { emode = Qt::ElideMiddle; } QString ajdText; Qt::TextElideMode emode; }; RAdjustableLabel::RAdjustableLabel(QWidget* const parent) : QLabel(parent), d(new Private) { setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); } RAdjustableLabel::~RAdjustableLabel() { delete d; } void RAdjustableLabel::resizeEvent(QResizeEvent*) { adjustTextToLabel(); } QSize RAdjustableLabel::minimumSizeHint() const { QSize sh = QLabel::minimumSizeHint(); sh.setWidth(-1); return sh; } QSize RAdjustableLabel::sizeHint() const { QFontMetrics fm(fontMetrics()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QScreen *s = qApp->screenAt(geometry().topLeft()); int maxW = s->availableGeometry().width() * 3 / 4; #else int maxW = QApplication::desktop()->screenGeometry(this).width() * 3 / 4; #endif #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) int currentW = fm.horizontalAdvance(d->ajdText); #else int currentW = fm.width(d->ajdText); #endif return (QSize(currentW > maxW ? maxW : currentW, QLabel::sizeHint().height())); } void RAdjustableLabel::setAdjustedText(const QString& text) { d->ajdText = text; if (d->ajdText.isNull()) QLabel::clear(); adjustTextToLabel(); } QString RAdjustableLabel::adjustedText() const { return d->ajdText; } void RAdjustableLabel::setAlignment(Qt::Alignment alignment) { QString tmp(d->ajdText); QLabel::setAlignment(alignment); d->ajdText = tmp; } void RAdjustableLabel::setElideMode(Qt::TextElideMode mode) { d->emode = mode; adjustTextToLabel(); } void RAdjustableLabel::adjustTextToLabel() { QFontMetrics fm(fontMetrics()); QStringList adjustedLines; int lblW = size().width(); bool adjusted = false; Q_FOREACH(const QString& line, d->ajdText.split(QLatin1Char('\n'))) { #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) int lineW = fm.horizontalAdvance(line); #else int lineW = fm.width(line); #endif if (lineW > lblW) { adjusted = true; adjustedLines << fm.elidedText(line, d->emode, lblW); } else { adjustedLines << line; } } if (adjusted) { QLabel::setText(adjustedLines.join(QStringLiteral("\n"))); setToolTip(d->ajdText); } else { QLabel::setText(d->ajdText); setToolTip(QString()); } } // ------------------------------------------------------------------------------------ class Q_DECL_HIDDEN RFileSelector::Private { public: Private() { edit = 0; btn = 0; fdMode = QFileDialog::ExistingFile; fdOptions = QFileDialog::DontUseNativeDialog; } QLineEdit* edit; QPushButton* btn; QFileDialog::FileMode fdMode; QString fdFilter; QString fdTitle; QFileDialog::Options fdOptions; }; RFileSelector::RFileSelector(QWidget* const parent) : RHBox(parent), d(new Private) { d->edit = new QLineEdit(this); d->btn = new QPushButton(i18n("Browse..."), this); setStretchFactor(d->edit, 10); connect(d->btn, SIGNAL(clicked()), this, SLOT(slotBtnClicked())); } RFileSelector::~RFileSelector() { delete d; } QLineEdit* RFileSelector::lineEdit() const { return d->edit; } void RFileSelector::setFileDlgMode(QFileDialog::FileMode mode) { d->fdMode = mode; } void RFileSelector::setFileDlgFilter(const QString& filter) { d->fdFilter = filter; } void RFileSelector::setFileDlgTitle(const QString& title) { d->fdTitle = title; } void RFileSelector::setFileDlgOptions(QFileDialog::Options opts) { d->fdOptions = opts; } void RFileSelector::slotBtnClicked() { if (d->fdMode == QFileDialog::ExistingFiles) { qCDebug(LIBKDCRAW_LOG) << "Multiple selection is not supported"; return; } QFileDialog* const fileDlg = new QFileDialog(this); fileDlg->setOptions(d->fdOptions); fileDlg->setDirectory(QFileInfo(d->edit->text()).dir()); fileDlg->setFileMode(d->fdMode); if (!d->fdFilter.isNull()) fileDlg->setNameFilter(d->fdFilter); if (!d->fdTitle.isNull()) fileDlg->setWindowTitle(d->fdTitle); connect(fileDlg, SIGNAL(urlSelected(QUrl)), this, SIGNAL(signalUrlSelected(QUrl))); emit signalOpenFileDialog(); if (fileDlg->exec() == QDialog::Accepted) { QStringList sel = fileDlg->selectedFiles(); if (!sel.isEmpty()) { d->edit->setText(sel.first()); } } delete fileDlg; } // --------------------------------------------------------------------------------------- WorkingPixmap::WorkingPixmap() { QPixmap pix(QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("libkdcraw/pics/process-working.png"))); QSize size(22, 22); if (pix.isNull()) { qCWarning(LIBKDCRAW_LOG) << "Invalid pixmap specified."; return; } if (!size.isValid()) { size = QSize(pix.width(), pix.width()); } if (pix.width() % size.width() || pix.height() % size.height()) { qCWarning(LIBKDCRAW_LOG) << "Invalid framesize."; return; } const int rowCount = pix.height() / size.height(); const int colCount = pix.width() / size.width(); m_frames.resize(rowCount * colCount); int pos = 0; for (int row = 0; row < rowCount; ++row) { for (int col = 0; col < colCount; ++col) { QPixmap frm = pix.copy(col * size.width(), row * size.height(), size.width(), size.height()); m_frames[pos++] = frm; } } } WorkingPixmap::~WorkingPixmap() { } bool WorkingPixmap::isEmpty() const { return m_frames.isEmpty(); } QSize WorkingPixmap::frameSize() const { if (isEmpty()) { qCWarning(LIBKDCRAW_LOG) << "No frame loaded."; return QSize(); } return m_frames[0].size(); } int WorkingPixmap::frameCount() const { return m_frames.size(); } QPixmap WorkingPixmap::frameAt(int index) const { if (isEmpty()) { qCWarning(LIBKDCRAW_LOG) << "No frame loaded."; return QPixmap(); } return m_frames.at(index); } // ------------------------------------------------------------------------------------ class Q_DECL_HIDDEN RColorSelector::Private { public: Private() { } QColor color; }; RColorSelector::RColorSelector(QWidget* const parent) : QPushButton(parent), d(new Private) { connect(this, SIGNAL(clicked()), this, SLOT(slotBtnClicked())); } RColorSelector::~RColorSelector() { delete d; } void RColorSelector::setColor(const QColor& color) { if (color.isValid()) { d->color = color; update(); } } QColor RColorSelector::color() const { return d->color; } void RColorSelector::slotBtnClicked() { QColor color = QColorDialog::getColor(d->color); if (color.isValid()) { setColor(color); emit signalColorSelected(color); } } void RColorSelector::paintEvent(QPaintEvent*) { QPainter painter(this); QStyle* const style = QWidget::style(); QStyleOptionButton opt; opt.initFrom(this); opt.state |= isDown() ? QStyle::State_Sunken : QStyle::State_Raised; opt.features = QStyleOptionButton::None; opt.icon = QIcon(); opt.text.clear(); style->drawControl(QStyle::CE_PushButtonBevel, &opt, &painter, this); QRect labelRect = style->subElementRect(QStyle::SE_PushButtonContents, &opt, this); int shift = style->pixelMetric(QStyle::PM_ButtonMargin, &opt, this) / 2; labelRect.adjust(shift, shift, -shift, -shift); int x, y, w, h; labelRect.getRect(&x, &y, &w, &h); if (isChecked() || isDown()) { x += style->pixelMetric(QStyle::PM_ButtonShiftHorizontal, &opt, this); y += style->pixelMetric(QStyle::PM_ButtonShiftVertical, &opt, this); } QColor fillCol = isEnabled() ? d->color : palette().color(backgroundRole()); qDrawShadePanel(&painter, x, y, w, h, palette(), true, 1, 0); if (fillCol.isValid()) { const QRect rect(x + 1, y + 1, w - 2, h - 2); if (fillCol.alpha() < 255) { QPixmap chessboardPattern(16, 16); QPainter patternPainter(&chessboardPattern); patternPainter.fillRect(0, 0, 8, 8, Qt::black); patternPainter.fillRect(8, 8, 8, 8, Qt::black); patternPainter.fillRect(0, 8, 8, 8, Qt::white); patternPainter.fillRect(8, 0, 8, 8, Qt::white); patternPainter.end(); painter.fillRect(rect, QBrush(chessboardPattern)); } painter.fillRect(rect, fillCol); } if (hasFocus()) { QRect focusRect = style->subElementRect(QStyle::SE_PushButtonFocusRect, &opt, this); QStyleOptionFocusRect focusOpt; focusOpt.init(this); focusOpt.rect = focusRect; focusOpt.backgroundColor = palette().window().color(); style->drawPrimitive(QStyle::PE_FrameFocusRect, &focusOpt, &painter, this); } } } // namespace KDcrawIface diff --git a/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp b/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp index a9111f868a..2b7915b513 100644 --- a/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp +++ b/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp @@ -1,360 +1,375 @@ /* * Copyright (C) 2011 Silvio Heinrich * * 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_colorsmudgeop.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include "kis_paintop_plugin_utils.h" + KisColorSmudgeOp::KisColorSmudgeOp(const KisPaintOpSettingsSP settings, KisPainter* painter, KisNodeSP node, KisImageSP image) : KisBrushBasedPaintOp(settings, painter) , m_firstRun(true) , m_image(image) , m_precisePainterWrapper(painter->device()) , m_tempDev(m_precisePainterWrapper.createPreciseCompositionSourceDevice()) , m_backgroundPainter(new KisPainter(m_tempDev)) , m_smudgePainter(new KisPainter(m_tempDev)) , m_colorRatePainter(new KisPainter(m_tempDev)) , m_finalPainter(new KisPainter(m_precisePainterWrapper.preciseDevice())) , m_smudgeRateOption() , m_colorRateOption("ColorRate", KisPaintOpOption::GENERAL, false) , m_smudgeRadiusOption() { Q_UNUSED(node); Q_ASSERT(settings); Q_ASSERT(painter); m_sizeOption.readOptionSetting(settings); m_opacityOption.readOptionSetting(settings); + m_ratioOption.readOptionSetting(settings); m_spacingOption.readOptionSetting(settings); + m_rateOption.readOptionSetting(settings); m_smudgeRateOption.readOptionSetting(settings); m_colorRateOption.readOptionSetting(settings); m_smudgeRadiusOption.readOptionSetting(settings); m_overlayModeOption.readOptionSetting(settings); m_rotationOption.readOptionSetting(settings); m_scatterOption.readOptionSetting(settings); m_gradientOption.readOptionSetting(settings); + m_airbrushOption.readOptionSetting(settings); m_sizeOption.resetAllSensors(); m_opacityOption.resetAllSensors(); + m_ratioOption.resetAllSensors(); m_spacingOption.resetAllSensors(); + m_rateOption.resetAllSensors(); m_smudgeRateOption.resetAllSensors(); m_colorRateOption.resetAllSensors(); m_smudgeRadiusOption.resetAllSensors(); m_rotationOption.resetAllSensors(); m_scatterOption.resetAllSensors(); m_gradientOption.resetAllSensors(); m_gradient = painter->gradient(); m_backgroundPainter->setCompositeOp(COMPOSITE_COPY); // Smudge Painter works in default COMPOSITE_OVER mode m_colorRatePainter->setCompositeOp(painter->compositeOp()->id()); m_finalPainter->setCompositeOp(COMPOSITE_COPY); m_finalPainter->setSelection(painter->selection()); m_finalPainter->setChannelFlags(painter->channelFlags()); m_finalPainter->copyMirrorInformationFrom(painter); m_paintColor = painter->paintColor().convertedTo(m_tempDev->colorSpace()); m_preciseColorRateCompositeOp = m_tempDev->colorSpace()->compositeOp(m_colorRatePainter->compositeOp()->id()); m_hsvOptions.append(KisPressureHSVOption::createHueOption()); m_hsvOptions.append(KisPressureHSVOption::createSaturationOption()); m_hsvOptions.append(KisPressureHSVOption::createValueOption()); Q_FOREACH (KisPressureHSVOption * option, m_hsvOptions) { option->readOptionSetting(settings); option->resetAllSensors(); if (option->isChecked() && !m_hsvTransform) { m_hsvTransform = m_paintColor.colorSpace()->createColorTransformation("hsv_adjustment", QHash()); } } m_rotationOption.applyFanCornersInfo(this); if (m_overlayModeOption.isChecked() && m_image && m_image->projection()){ m_preciseImageDeviceWrapper.reset(new KisPrecisePaintDeviceWrapper(m_image->projection())); } } KisColorSmudgeOp::~KisColorSmudgeOp() { qDeleteAll(m_hsvOptions); delete m_hsvTransform; } -void KisColorSmudgeOp::updateMask(const KisPaintInformation& info, double scale, double rotation, const QPointF &cursorPoint) +void KisColorSmudgeOp::updateMask(const KisPaintInformation& info, const KisDabShape &shape, const QPointF &cursorPoint) { static const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); static KoColor color(Qt::black, cs); m_maskDab = m_dabCache->fetchDab(cs, color, cursorPoint, - KisDabShape(scale, 1.0, rotation), + shape, info, 1.0, &m_dstDabRect); // sanity check KIS_ASSERT_RECOVER_NOOP(m_dstDabRect.size() == m_maskDab->bounds().size()); } inline void KisColorSmudgeOp::getTopLeftAligned(const QPointF &pos, const QPointF &hotSpot, qint32 *x, qint32 *y) { QPointF topLeft = pos - hotSpot; qreal xFraction, yFraction; // will not be used splitCoordinate(topLeft.x(), x, &xFraction); splitCoordinate(topLeft.y(), y, &yFraction); } KisSpacingInformation KisColorSmudgeOp::paintAt(const KisPaintInformation& info) { KisBrushSP brush = m_brush; const bool useDullingMode = m_smudgeRateOption.getMode() == KisSmudgeOption::DULLING_MODE; /* This is a fix for dulling + overlay + paint, * this should allow the image to composite paint addition effects correctly * while also respecting overlay mode. */ bool useAlternatePrecisionSource = (m_overlayModeOption.isChecked() && useDullingMode && m_preciseImageDeviceWrapper!= nullptr); KisPrecisePaintDeviceWrapper &activeWrapper = useAlternatePrecisionSource ? *m_preciseImageDeviceWrapper : m_precisePainterWrapper; // Simple error catching if (!painter()->device() || !brush || !brush->canPaintFor(info)) { return KisSpacingInformation(1.0); } if (m_smudgeRateOption.getMode() == KisSmudgeOption::SMEARING_MODE) { /** * Disable handling of the subpixel precision. In the smudge op we * should read from the aligned areas of the image, so having * additional internal offsets, created by the subpixel precision, * will worsen the quality (at least because * QRectF(m_dstDabRect).center() will not point to the real center * of the brush anymore). * Of course, this only really matters with smearing_mode (bug:327235), * and you only notice the lack of subpixel precision in the dulling methods. */ m_dabCache->disableSubpixelPrecision(); } #if 0 //if precision KoColor colorSpaceChanger = painter()->paintColor(); const KoColorSpace* preciseColorSpace = colorSpaceChanger.colorSpace(); if (colorSpaceChanger.colorSpace()->colorDepthId().id() == "U8") { preciseColorSpace = KoColorSpaceRegistry::instance()->colorSpace(colorSpaceChanger.colorSpace()->colorModelId().id(), "U16", colorSpaceChanger.profile() ); colorSpaceChanger.convertTo(preciseColorSpace); } painter()->setPaintColor(colorSpaceChanger); #endif // get the scaling factor calculated by the size option qreal scale = m_sizeOption.apply(info); scale *= KisLodTransform::lodToScale(painter()->device()); qreal rotation = m_rotationOption.apply(info); if (checkSizeTooSmall(scale)) return KisSpacingInformation(); - KisDabShape shape(scale, 1.0, rotation); + qreal ratio = m_ratioOption.apply(info); + + KisDabShape shape(scale, ratio, rotation); QPointF scatteredPos = m_scatterOption.apply(info, brush->maskWidth(shape, 0, 0, info), brush->maskHeight(shape, 0, 0, info)); QPointF hotSpot = brush->hotSpot(shape, info); /** * Update the brush mask. * * Upon leaving the function: * o m_maskDab stores the new mask * o m_maskBounds stores the extents of the mask paint device * o m_dstDabRect stores the destination rect where the mask is going * to be written to */ - updateMask(info, scale, rotation, scatteredPos); + updateMask(info, shape, scatteredPos); QPointF newCenterPos = QRectF(m_dstDabRect).center(); /** * Save the center of the current dab to know where to read the * data during the next pass. We do not save scatteredPos here, * because it may differ slightly from the real center of the * brush (due to rounding effects), which will result in a * really weird quality. */ QRect srcDabRect = m_dstDabRect.translated((m_lastPaintPos - newCenterPos).toPoint()); m_lastPaintPos = newCenterPos; KisSpacingInformation spacingInfo = effectiveSpacing(scale, rotation, - m_spacingOption, info); + &m_airbrushOption, &m_spacingOption, info); if (m_firstRun) { m_firstRun = false; return spacingInfo; } const qreal fpOpacity = (qreal(painter()->opacity()) / 255.0) * m_opacityOption.getOpacityf(info); if (m_image && m_overlayModeOption.isChecked()) { m_image->blockUpdates(); m_backgroundPainter->bitBlt(QPoint(), m_image->projection(), srcDabRect); m_image->unblockUpdates(); } else { // IMPORTANT: Clear the temporary painting device to transparent black. // It will only clear the extents of the brush. m_tempDev->clear(QRect(QPoint(), m_dstDabRect.size())); } // stored in the color space of the paintColor KoColor dullingFillColor = m_paintColor; QPoint canvasLocalSamplePoint = (srcDabRect.topLeft() + hotSpot).toPoint(); if (!useDullingMode) { activeWrapper.readRect(srcDabRect); m_smudgePainter->bitBlt(QPoint(), activeWrapper.preciseDevice(), srcDabRect); } else { if (m_smudgeRadiusOption.isChecked()) { const qreal effectiveSize = 0.5 * (m_dstDabRect.width() + m_dstDabRect.height()); const QRect sampleRect = m_smudgeRadiusOption.sampleRect(info, effectiveSize, canvasLocalSamplePoint); activeWrapper.readRect(sampleRect); m_smudgeRadiusOption.apply(&dullingFillColor, info, effectiveSize, canvasLocalSamplePoint.x(), canvasLocalSamplePoint.y(), activeWrapper.preciseDevice()); KIS_SAFE_ASSERT_RECOVER_NOOP(*dullingFillColor.colorSpace() == *m_tempDev->colorSpace()); } else { // get the pixel on the canvas that lies beneath the hot spot // of the dab and fill the temporary paint device with that color activeWrapper.readRect(QRect(canvasLocalSamplePoint, QSize(1,1))); KisCrossDeviceColorPickerInt colorPicker(activeWrapper.preciseDevice(), dullingFillColor); colorPicker.pickColor(canvasLocalSamplePoint.x(), canvasLocalSamplePoint.y(), dullingFillColor.data()); KIS_SAFE_ASSERT_RECOVER_NOOP(*dullingFillColor.colorSpace() == *m_tempDev->colorSpace()); } } // if the user selected the color smudge option, // we will mix some color into the temporary painting device (m_tempDev) if (m_colorRateOption.isChecked()) { // this will apply the opacity (selected by the user) to copyPainter // (but fit the rate inbetween the range 0.0 to (1.0-SmudgeRate)) qreal maxColorRate = qMax(1.0 - m_smudgeRateOption.getRate(), 0.2); m_colorRateOption.apply(*m_colorRatePainter, info, 0.0, maxColorRate, fpOpacity); // paint a rectangle with the current color (foreground color) // or a gradient color (if enabled) // into the temporary painting device and use the user selected // composite mode KoColor color = m_paintColor; m_gradientOption.apply(color, m_gradient, info); if (m_hsvTransform) { Q_FOREACH (KisPressureHSVOption * option, m_hsvOptions) { option->apply(m_hsvTransform, info); } m_hsvTransform->transform(color.data(), color.data(), 1); } if (!useDullingMode) { KIS_SAFE_ASSERT_RECOVER(*m_colorRatePainter->device()->colorSpace() == *color.colorSpace()) { color.convertTo(m_colorRatePainter->device()->colorSpace()); } m_colorRatePainter->fill(0, 0, m_dstDabRect.width(), m_dstDabRect.height(), color); } else { KIS_SAFE_ASSERT_RECOVER(*dullingFillColor.colorSpace() == *color.colorSpace()) { color.convertTo(dullingFillColor.colorSpace()); } KIS_SAFE_ASSERT_RECOVER_NOOP(*dullingFillColor.colorSpace() == *m_tempDev->colorSpace()); m_preciseColorRateCompositeOp->composite(dullingFillColor.data(), 0, color.data(), 0, 0, 0, 1, 1, m_colorRatePainter->opacity()); } } if (useDullingMode) { KIS_SAFE_ASSERT_RECOVER_NOOP(*dullingFillColor.colorSpace() == *m_tempDev->colorSpace()); m_tempDev->fill(QRect(0, 0, m_dstDabRect.width(), m_dstDabRect.height()), dullingFillColor); } m_precisePainterWrapper.readRects(m_finalPainter->calculateAllMirroredRects(m_dstDabRect)); // if color is disabled (only smudge) and "overlay mode" is enabled // then first blit the region under the brush from the image projection // to the painting device to prevent a rapid build up of alpha value // if the color to be smudged is semi transparent. if (m_image && m_overlayModeOption.isChecked() && !m_colorRateOption.isChecked()) { m_finalPainter->setOpacity(OPACITY_OPAQUE_U8); m_image->blockUpdates(); // TODO: check if this code is correct in mirrored mode! Technically, the // painter renders the mirrored dab only, so we should also prepare // the overlay for it in all the places. m_finalPainter->bitBlt(m_dstDabRect.topLeft(), m_image->projection(), m_dstDabRect); m_image->unblockUpdates(); } // set opacity calculated by the rate option m_smudgeRateOption.apply(*m_finalPainter, info, 0.0, 1.0, fpOpacity); // then blit the temporary painting device on the canvas at the current brush position // the alpha mask (maskDab) will be used here to only blit the pixels that are in the area (shape) of the brush m_finalPainter->bitBltWithFixedSelection(m_dstDabRect.x(), m_dstDabRect.y(), m_tempDev, m_maskDab, m_dstDabRect.width(), m_dstDabRect.height()); m_finalPainter->renderMirrorMaskSafe(m_dstDabRect, m_tempDev, 0, 0, m_maskDab, !m_dabCache->needSeparateOriginal()); const QVector dirtyRects = m_finalPainter->takeDirtyRegion(); m_precisePainterWrapper.writeRects(dirtyRects); painter()->addDirtyRects(dirtyRects); return spacingInfo; } KisSpacingInformation KisColorSmudgeOp::updateSpacingImpl(const KisPaintInformation &info) const { const qreal scale = m_sizeOption.apply(info) * KisLodTransform::lodToScale(painter()->device()); const qreal rotation = m_rotationOption.apply(info); - return effectiveSpacing(scale, rotation, m_spacingOption, info); + return effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); +} + + +KisTimingInformation KisColorSmudgeOp::updateTimingImpl(const KisPaintInformation &info) const +{ + return KisPaintOpPluginUtils::effectiveTiming(&m_airbrushOption, &m_rateOption, info); } diff --git a/plugins/paintops/colorsmudge/kis_colorsmudgeop.h b/plugins/paintops/colorsmudge/kis_colorsmudgeop.h index 239edb8c97..c10043391e 100644 --- a/plugins/paintops/colorsmudge/kis_colorsmudgeop.h +++ b/plugins/paintops/colorsmudge/kis_colorsmudgeop.h @@ -1,97 +1,104 @@ /* * Copyright (C) 2011 Silvio Heinrich * * 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_COLORSMUDGEOP_H_ #define _KIS_COLORSMUDGEOP_H_ #include #include "KoColorTransformation.h" #include #include #include #include #include +#include #include +#include #include #include #include #include +#include #include "kis_overlay_mode_option.h" #include "kis_rate_option.h" #include "kis_smudge_option.h" #include "kis_smudge_radius_option.h" #include "KisPrecisePaintDeviceWrapper.h" class QPointF; class KisBrushBasedPaintOpSettings; class KisPainter; class KoColorSpace; class KisColorSmudgeOp: public KisBrushBasedPaintOp { public: KisColorSmudgeOp(const KisPaintOpSettingsSP settings, KisPainter* painter, KisNodeSP node, KisImageSP image); ~KisColorSmudgeOp() override; protected: KisSpacingInformation paintAt(const KisPaintInformation& info) override; KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const override; + KisTimingInformation updateTimingImpl(const KisPaintInformation &info) const; private: // Sets the m_maskDab _and m_maskDabRect - void updateMask(const KisPaintInformation& info, double scale, double rotation, const QPointF &cursorPoint); + void updateMask(const KisPaintInformation& info, const KisDabShape &shape, const QPointF &cursorPoint); inline void getTopLeftAligned(const QPointF &pos, const QPointF &hotSpot, qint32 *x, qint32 *y); private: bool m_firstRun; KisImageWSP m_image; KisPrecisePaintDeviceWrapper m_precisePainterWrapper; KoColor m_paintColor; KisPaintDeviceSP m_tempDev; QScopedPointer m_preciseImageDeviceWrapper; QScopedPointer m_backgroundPainter; QScopedPointer m_smudgePainter; QScopedPointer m_colorRatePainter; QScopedPointer m_finalPainter; KoAbstractGradientSP m_gradient; KisPressureSizeOption m_sizeOption; KisPressureOpacityOption m_opacityOption; + KisPressureRatioOption m_ratioOption; KisPressureSpacingOption m_spacingOption; + KisPressureRateOption m_rateOption; KisSmudgeOption m_smudgeRateOption; KisRateOption m_colorRateOption; KisSmudgeRadiusOption m_smudgeRadiusOption; KisOverlayModeOption m_overlayModeOption; KisPressureRotationOption m_rotationOption; KisPressureScatterOption m_scatterOption; KisPressureGradientOption m_gradientOption; QList m_hsvOptions; + KisAirbrushOptionProperties m_airbrushOption; QRect m_dstDabRect; KisFixedPaintDeviceSP m_maskDab; QPointF m_lastPaintPos; KoColorTransformation *m_hsvTransform {0}; const KoCompositeOp *m_preciseColorRateCompositeOp {0}; }; #endif // _KIS_COLORSMUDGEOP_H_ diff --git a/plugins/paintops/colorsmudge/kis_colorsmudgeop_settings_widget.cpp b/plugins/paintops/colorsmudge/kis_colorsmudgeop_settings_widget.cpp index 4e2b71f3bd..daccc2fed2 100644 --- a/plugins/paintops/colorsmudge/kis_colorsmudgeop_settings_widget.cpp +++ b/plugins/paintops/colorsmudge/kis_colorsmudgeop_settings_widget.cpp @@ -1,92 +1,97 @@ /* * Copyright (C) 2011 Silvio Heinrich * * 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_colorsmudgeop_settings_widget.h" #include "kis_brush_based_paintop_settings.h" #include "kis_overlay_mode_option.h" #include "kis_rate_option.h" #include "kis_smudge_option_widget.h" #include "kis_smudge_radius_option.h" #include #include #include +#include #include #include #include #include #include #include #include #include +#include #include "kis_texture_option.h" #include #include "kis_pressure_texture_strength_option.h" #include "kis_pressure_hsv_option.h" #include "kis_colorsmudgeop_settings.h" #include KisColorSmudgeOpSettingsWidget::KisColorSmudgeOpSettingsWidget(QWidget* parent): KisBrushBasedPaintopOptionWidget(parent) { setObjectName("brush option widget"); setPrecisionEnabled(true); addPaintOpOption(new KisCompositeOpOption(true), i18n("Blending Mode")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureOpacityOption(), i18n("Transparent"), i18n("Opaque")), i18n("Opacity")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureSizeOption(), i18n("0%"), i18n("100%")), i18n("Size")); + addPaintOpOption(new KisCurveOptionWidget(new KisPressureRatioOption(), i18n("0%"), i18n("100%")), i18n("Ratio")); addPaintOpOption(new KisPressureSpacingOptionWidget(), i18n("Spacing")); addPaintOpOption(new KisPressureMirrorOptionWidget(), i18n("Mirror")); m_smudgeOptionWidget = new KisSmudgeOptionWidget(); addPaintOpOption(m_smudgeOptionWidget, i18n("Smudge Length")); addPaintOpOption(new KisCurveOptionWidget(new KisSmudgeRadiusOption(), i18n("0.0"), i18n("1.0")), i18n("Smudge Radius")); addPaintOpOption(new KisCurveOptionWidget(new KisRateOption("ColorRate", KisPaintOpOption::GENERAL, false), i18n("0.0"), i18n("1.0")), i18n("Color Rate")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureRotationOption(), i18n("-180°"), i18n("180°")), i18n("Rotation")); addPaintOpOption(new KisPressureScatterOptionWidget(), i18n("Scatter")); addPaintOpOption(new KisOverlayModeOptionWidget(), i18n("Overlay Mode")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureGradientOption(), i18n("0%"), i18n("100%")), i18n("Gradient")); addPaintOpOption(new KisCurveOptionWidget(KisPressureHSVOption::createHueOption(), KisPressureHSVOption::hueMinLabel(), KisPressureHSVOption::huemaxLabel()), i18n("Hue")); addPaintOpOption(new KisCurveOptionWidget(KisPressureHSVOption::createSaturationOption(), KisPressureHSVOption::saturationMinLabel(), KisPressureHSVOption::saturationmaxLabel()), i18n("Saturation")); addPaintOpOption(new KisCurveOptionWidget(KisPressureHSVOption::createValueOption(), KisPressureHSVOption::valueMinLabel(), KisPressureHSVOption::valuemaxLabel()), i18nc("HSV Value", "Value")); + addPaintOpOption(new KisAirbrushOptionWidget(false), i18n("Airbrush")); + addPaintOpOption(new KisCurveOptionWidget(new KisPressureRateOption(), i18n("0%"), i18n("100%")), i18n("Rate")); addPaintOpOption(new KisTextureOption(), i18n("Pattern")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureTextureStrengthOption(), i18n("Weak"), i18n("Strong")), i18n("Strength")); } KisColorSmudgeOpSettingsWidget::~KisColorSmudgeOpSettingsWidget() { } KisPropertiesConfigurationSP KisColorSmudgeOpSettingsWidget::configuration() const { KisColorSmudgeOpSettingsSP config = new KisColorSmudgeOpSettings(KisGlobalResourcesInterface::instance()); config->setOptionsWidget(const_cast(this)); config->setProperty("paintop", "colorsmudge"); writeConfiguration(config); return config; } void KisColorSmudgeOpSettingsWidget::notifyPageChanged() { KisBrushSP brush = this->brush(); bool pierced = brush ? brush->isPiercedApprox() : false; m_smudgeOptionWidget->updateBrushPierced(pierced); } diff --git a/plugins/paintops/libpaintop/kis_curve_option.cpp b/plugins/paintops/libpaintop/kis_curve_option.cpp index 74479cc67b..f205824fd5 100644 --- a/plugins/paintops/libpaintop/kis_curve_option.cpp +++ b/plugins/paintops/libpaintop/kis_curve_option.cpp @@ -1,468 +1,484 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_curve_option.h" #include KisCurveOption::KisCurveOption(const QString& name, KisPaintOpOption::PaintopCategory category, bool checked, qreal value, qreal min, qreal max) : m_name(name) , m_category(category) , m_checkable(true) , m_checked(checked) , m_useCurve(true) , m_useSameCurve(true) , m_separateCurveValue(false) , m_curveMode(0) { Q_FOREACH (const DynamicSensorType sensorType, KisDynamicSensor::sensorsTypes()) { KisDynamicSensorSP sensor = KisDynamicSensor::type2Sensor(sensorType, m_name); sensor->setActive(false); replaceSensor(sensor); } m_sensorMap[PRESSURE]->setActive(true); setValueRange(min, max); setValue(value); - QList points; - // needs to be set to something, weird curve is better for debugging - // it will be reset to the curve from the preset anyway though - points.push_back(QPointF(0,0)); - points.push_back(QPointF(0.25,0.9)); - points.push_back(QPointF(0.5,0)); - points.push_back(QPointF(0.75,0.6)); - points.push_back(QPointF(1,0)); - m_commonCurve = KisCubicCurve(points); + + m_commonCurve = defaultCurve(); } KisCurveOption::~KisCurveOption() { } const QString& KisCurveOption::name() const { return m_name; } KisPaintOpOption::PaintopCategory KisCurveOption::category() const { return m_category; } qreal KisCurveOption::minValue() const { return m_minValue; } qreal KisCurveOption::maxValue() const { return m_maxValue; } qreal KisCurveOption::value() const { return m_value; } void KisCurveOption::resetAllSensors() { Q_FOREACH (KisDynamicSensorSP sensor, m_sensorMap.values()) { if (sensor->isActive()) { sensor->reset(); } } } void KisCurveOption::writeOptionSetting(KisPropertiesConfigurationSP setting) const { if (m_checkable) { setting->setProperty("Pressure" + m_name, isChecked()); } if (activeSensors().size() == 1) { setting->setProperty(m_name + "Sensor", activeSensors().first()->toXML()); } else { QDomDocument doc = QDomDocument("params"); QDomElement root = doc.createElement("params"); doc.appendChild(root); root.setAttribute("id", "sensorslist"); Q_FOREACH (KisDynamicSensorSP sensor, activeSensors()) { QDomElement childelt = doc.createElement("ChildSensor"); sensor->toXML(doc, childelt); root.appendChild(childelt); } setting->setProperty(m_name + "Sensor", doc.toString()); } setting->setProperty(m_name + "UseCurve", m_useCurve); setting->setProperty(m_name + "UseSameCurve", m_useSameCurve); setting->setProperty(m_name + "Value", m_value); setting->setProperty(m_name + "curveMode", m_curveMode); setting->setProperty(m_name + "commonCurve", qVariantFromValue(m_commonCurve)); } void KisCurveOption::readOptionSetting(KisPropertiesConfigurationSP setting) { readNamedOptionSetting(m_name, setting); } void KisCurveOption::lodLimitations(KisPaintopLodLimitations *l) const { Q_UNUSED(l); } int KisCurveOption::intMinValue() const { return 0; } int KisCurveOption::intMaxValue() const { return 100; } QString KisCurveOption::valueSuffix() const { return i18n("%"); } void KisCurveOption::readNamedOptionSetting(const QString& prefix, const KisPropertiesConfigurationSP setting) { if (!setting) return; KisCubicCurve commonCurve = m_commonCurve; //dbgKrita << "readNamedOptionSetting" << prefix; // setting->dump(); if (m_checkable) { setChecked(setting->getBool("Pressure" + prefix, false)); } //dbgKrita << "\tPressure" + prefix << isChecked(); m_sensorMap.clear(); // Replace all sensors with the inactive defaults Q_FOREACH (const DynamicSensorType sensorType, KisDynamicSensor::sensorsTypes()) { replaceSensor(KisDynamicSensor::type2Sensor(sensorType, m_name)); } QString sensorDefinition = setting->getString(prefix + "Sensor"); if (!sensorDefinition.contains("sensorslist")) { KisDynamicSensorSP s = KisDynamicSensor::createFromXML(sensorDefinition, m_name); if (s) { replaceSensor(s); s->setActive(true); commonCurve = s->curve(); //dbgKrita << "\tsingle sensor" << s::id(s->sensorType()) << s->isActive() << "added"; } } else { QDomDocument doc; doc.setContent(sensorDefinition); QDomElement elt = doc.documentElement(); QDomNode node = elt.firstChild(); while (!node.isNull()) { if (node.isElement()) { QDomElement childelt = node.toElement(); if (childelt.tagName() == "ChildSensor") { KisDynamicSensorSP s = KisDynamicSensor::createFromXML(childelt, m_name); if (s) { replaceSensor(s); s->setActive(true); commonCurve = s->curve(); //dbgKrita << "\tchild sensor" << s::id(s->sensorType()) << s->isActive() << "added"; } } } node = node.nextSibling(); } } m_useSameCurve = setting->getBool(m_name + "UseSameCurve", true); // Only load the old curve format if the curve wasn't saved by the sensor // This will give every sensor the same curve. //dbgKrita << ">>>>>>>>>>>" << prefix + "Sensor" << setting->getString(prefix + "Sensor"); if (!setting->getString(prefix + "Sensor").contains("curve")) { //dbgKrita << "\told format"; if (setting->getBool("Custom" + prefix, false)) { Q_FOREACH (KisDynamicSensorSP s, m_sensorMap.values()) { s->setCurve(setting->getCubicCurve("Curve" + prefix)); commonCurve = s->curve(); } + } else { + commonCurve = emptyCurve(); } } if (m_useSameCurve) { m_commonCurve = setting->getCubicCurve(prefix + "commonCurve", commonCurve); } // At least one sensor needs to be active if (activeSensors().size() == 0) { m_sensorMap[PRESSURE]->setActive(true); } m_value = setting->getDouble(m_name + "Value", m_maxValue); //dbgKrita << "\t" + m_name + "Value" << m_value; m_useCurve = setting->getBool(m_name + "UseCurve", true); //dbgKrita << "\t" + m_name + "UseCurve" << m_useSameCurve; //dbgKrita << "\t" + m_name + "UseSameCurve" << m_useSameCurve; m_curveMode = setting->getInt(m_name + "curveMode"); //dbgKrita << "-----------------"; } void KisCurveOption::replaceSensor(KisDynamicSensorSP s) { Q_ASSERT(s); m_sensorMap[s->sensorType()] = s; } KisDynamicSensorSP KisCurveOption::sensor(DynamicSensorType sensorType, bool active) const { if (m_sensorMap.contains(sensorType)) { if (!active) { return m_sensorMap[sensorType]; } else { if (m_sensorMap[sensorType]->isActive()) { return m_sensorMap[sensorType]; } } } return 0; } bool KisCurveOption::isRandom() const { return bool(sensor(FUZZY_PER_DAB, true)) || bool(sensor(FUZZY_PER_STROKE, true)); } bool KisCurveOption::isCurveUsed() const { return m_useCurve; } bool KisCurveOption::isSameCurveUsed() const { return m_useSameCurve; } int KisCurveOption::getCurveMode() const { return m_curveMode; } KisCubicCurve KisCurveOption::getCommonCurve() const { return m_commonCurve; } void KisCurveOption::setSeparateCurveValue(bool separateCurveValue) { m_separateCurveValue = separateCurveValue; } bool KisCurveOption::isCheckable() { return m_checkable; } bool KisCurveOption::isChecked() const { return m_checked; } void KisCurveOption::setChecked(bool checked) { m_checked = checked; } void KisCurveOption::setCurveUsed(bool useCurve) { m_useCurve = useCurve; } void KisCurveOption::setCurveMode(int mode) { m_curveMode = mode; } void KisCurveOption::setUseSameCurve(bool useSameCurve) { m_useSameCurve = useSameCurve; } void KisCurveOption::setCommonCurve(KisCubicCurve curve) { m_commonCurve = curve; } void KisCurveOption::setCurve(DynamicSensorType sensorType, bool useSameCurve, const KisCubicCurve &curve) { if (useSameCurve == m_useSameCurve) { if (useSameCurve) { m_commonCurve = curve; } else { KisDynamicSensorSP s = sensor(sensorType, false); if (s) { s->setCurve(curve); } } } else { if (!m_useSameCurve && useSameCurve) { m_commonCurve = curve; } else { //if (m_useSameCurve && !useSameCurve) KisDynamicSensorSP s = 0; // And set the current sensor to the current curve if (!m_sensorMap.contains(sensorType)) { s = KisDynamicSensor::type2Sensor(sensorType, m_name); } else { KisDynamicSensorSP s = sensor(sensorType, false); } if (s) { s->setCurve(curve); } } m_useSameCurve = useSameCurve; } } void KisCurveOption::setValueRange(qreal min, qreal max) { m_minValue = qMin(min, max); m_maxValue = qMax(min, max); } void KisCurveOption::setValue(qreal value) { m_value = qBound(m_minValue, value, m_maxValue); } KisCurveOption::ValueComponents KisCurveOption::computeValueComponents(const KisPaintInformation& info) const { ValueComponents components; if (m_useCurve) { QMap::const_iterator i; QList sensorValues; for (i = m_sensorMap.constBegin(); i != m_sensorMap.constEnd(); ++i) { KisDynamicSensorSP s(i.value()); if (s->isActive()) { qreal valueFromCurve = m_useSameCurve ? s->parameter(info, m_commonCurve, true) : s->parameter(info); if (s->isAdditive()) { components.additive += valueFromCurve; components.hasAdditive = true; } else if (s->isAbsoluteRotation()) { components.absoluteOffset = valueFromCurve; components.hasAbsoluteOffset =true; } else { sensorValues << valueFromCurve; components.hasScaling = true; } } } if (sensorValues.count() == 1) { components.scaling = sensorValues.first(); } else { if (m_curveMode == 1){ // add components.scaling = 0; double i; foreach (i, sensorValues) { components.scaling += i; } } else if (m_curveMode == 2){ //max components.scaling = *std::max_element(sensorValues.begin(), sensorValues.end()); } else if (m_curveMode == 3){ //min components.scaling = *std::min_element(sensorValues.begin(), sensorValues.end()); } else if (m_curveMode == 4){ //difference double max = *std::max_element(sensorValues.begin(), sensorValues.end()); double min = *std::min_element(sensorValues.begin(), sensorValues.end()); components.scaling = max-min; } else { //multuply - default double i; foreach (i, sensorValues) { components.scaling *= i; } } } } if (!m_separateCurveValue) { components.constant = m_value; } components.minSizeLikeValue = m_minValue; components.maxSizeLikeValue = m_maxValue; return components; } qreal KisCurveOption::computeSizeLikeValue(const KisPaintInformation& info) const { const ValueComponents components = computeValueComponents(info); return components.sizeLikeValue(); } qreal KisCurveOption::computeRotationLikeValue(const KisPaintInformation& info, qreal baseValue, bool absoluteAxesFlipped) const { const ValueComponents components = computeValueComponents(info); return components.rotationLikeValue(baseValue, absoluteAxesFlipped); } +KisCubicCurve KisCurveOption::defaultCurve() +{ + QList points; + // needs to be set to something, weird curve is better for debugging + // it will be reset to the curve from the preset anyway though + points.push_back(QPointF(0,0)); + points.push_back(QPointF(0.25,0.9)); + points.push_back(QPointF(0.5,0)); + points.push_back(QPointF(0.75,0.6)); + points.push_back(QPointF(1,0)); + return KisCubicCurve(points); +} + +KisCubicCurve KisCurveOption::emptyCurve() +{ + QList points; + points.push_back(QPointF(0,0)); + points.push_back(QPointF(1,1)); + return KisCubicCurve(points); +} + QList KisCurveOption::sensors() { //dbgKrita << "ID" << name() << "has" << m_sensorMap.count() << "Sensors of which" << sensorList.count() << "are active."; return m_sensorMap.values(); } QList KisCurveOption::activeSensors() const { QList sensorList; Q_FOREACH (KisDynamicSensorSP sensor, m_sensorMap.values()) { if (sensor->isActive()) { sensorList << sensor; } } //dbgKrita << "ID" << name() << "has" << m_sensorMap.count() << "Sensors of which" << sensorList.count() << "are active."; return sensorList; } diff --git a/plugins/paintops/libpaintop/kis_curve_option.h b/plugins/paintops/libpaintop/kis_curve_option.h index 22df067944..36fafc098a 100644 --- a/plugins/paintops/libpaintop/kis_curve_option.h +++ b/plugins/paintops/libpaintop/kis_curve_option.h @@ -1,235 +1,248 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KIS_CURVE_OPTION_H #define KIS_CURVE_OPTION_H #include #include #include "kis_paintop_option.h" #include "kis_global.h" #include #include "kritapaintop_export.h" #include "kis_dynamic_sensor.h" class KisDynamicSensor; /** * KisCurveOption is the base class for paintop options that are * defined through one or more curves. * * Note: it is NOT a KisPaintOpOption, even though the API is pretty similar! * * KisCurveOption classes have a generic GUI widget, KisCurveOptionWidget. So, * in contrast to KisPaintOpOption classes, KisCurveOption instances can and * will be created in the constructor of KisPaintOp paintops. This class can * manage to read and write its settings directly. * */ class PAINTOP_EXPORT KisCurveOption { public: KisCurveOption(const QString& name, KisPaintOpOption::PaintopCategory category, bool checked, qreal value = 1.0, qreal min = 0.0, qreal max = 1.0); virtual ~KisCurveOption(); virtual void writeOptionSetting(KisPropertiesConfigurationSP setting) const; virtual void readOptionSetting(KisPropertiesConfigurationSP setting); virtual void lodLimitations(KisPaintopLodLimitations *l) const; //Please override for other values than 0-100 and % virtual int intMinValue()const; virtual int intMaxValue()const; virtual QString valueSuffix()const; const QString& name() const; KisPaintOpOption::PaintopCategory category() const; qreal minValue() const; qreal maxValue() const; qreal value() const; void resetAllSensors(); KisDynamicSensorSP sensor(DynamicSensorType sensorType, bool active) const; void replaceSensor(KisDynamicSensorSP sensor); QList sensors(); QList activeSensors() const; bool isCheckable(); bool isChecked() const; bool isCurveUsed() const; bool isSameCurveUsed() const; bool isRandom() const; int getCurveMode() const; /** * Returns the curve that is being used instead of sensor ones * in case "Use the same curve" is checked. */ KisCubicCurve getCommonCurve() const; void setSeparateCurveValue(bool separateCurveValue); void setChecked(bool checked); void setCurveUsed(bool useCurve); void setCurve(DynamicSensorType sensorType, bool useSameCurve, const KisCubicCurve &curve); void setValue(qreal value); void setCurveMode(int mode); /** * Sets the bool indicating whether "Share curve across all settings" is checked. */ void setUseSameCurve(bool useSameCurve); /** * Sets the curve that is being used instead of sensor ones * in case "Share curve across all settings" is checked. */ void setCommonCurve(KisCubicCurve curve); struct ValueComponents { ValueComponents() : constant(1.0), scaling(1.0), additive(0.0), absoluteOffset(0.0), hasAbsoluteOffset(false), hasScaling(false), hasAdditive(false) { } qreal constant; qreal scaling; qreal additive; qreal absoluteOffset; bool hasAbsoluteOffset; bool hasScaling; bool hasAdditive; qreal minSizeLikeValue; qreal maxSizeLikeValue; /** * @param normalizedBaseAngle canvas rotation angle normalized to range [0; 1] * @param absoluteAxesFlipped true if underlying image coordinate system is flipped (horiz. mirror != vert. mirror) */ qreal rotationLikeValue(qreal normalizedBaseAngle, bool absoluteAxesFlipped) const { const qreal offset = !hasAbsoluteOffset ? normalizedBaseAngle : absoluteAxesFlipped ? 1.0 - absoluteOffset : absoluteOffset; const qreal realScalingPart = hasScaling ? KisDynamicSensor::scalingToAdditive(scaling) : 0.0; const qreal realAdditivePart = hasAdditive ? additive : 0; qreal value = wrapInRange(2 * offset + constant * (realScalingPart + realAdditivePart), -1.0, 1.0); if (qIsNaN(value)) { qWarning() << "rotationLikeValue returns NaN!" << normalizedBaseAngle << absoluteAxesFlipped; value = 0; } return value; } qreal sizeLikeValue() const { const qreal offset = hasAbsoluteOffset ? absoluteOffset : 1.0; const qreal realScalingPart = hasScaling ? scaling : 1.0; const qreal realAdditivePart = hasAdditive ? KisDynamicSensor::additiveToScaling(additive) : 1.0; return qBound(minSizeLikeValue, constant * offset * realScalingPart * realAdditivePart, maxSizeLikeValue); } private: static inline qreal wrapInRange(qreal x, qreal min, qreal max) { const qreal range = max - min; x -= min; if (x < 0.0) { x = range + fmod(x, range); } if (x > range) { x = fmod(x, range); } return x + min; } }; /** * Uses the curves set on the sensors to compute a single * double value that can control the parameters of a brush. * * This value is derives from the values stored in * ValuesComponents object. */ ValueComponents computeValueComponents(const KisPaintInformation& info) const; qreal computeSizeLikeValue(const KisPaintInformation &info) const; qreal computeRotationLikeValue(const KisPaintInformation& info, qreal baseValue, bool absoluteAxesFlipped) const; + /** + * @brief defaultCurve returns a curve that is set when the KisCurveOption is not initialized yet + * the purpose of distinguishing between this one and emptyCurve() is to allow easier finding out that something is wrong + * in the code setting common curves + * @return a non-standard curve with two hills + */ + KisCubicCurve defaultCurve(); + /** + * @brief emptyCurve returns the simplest usable curve + * @return curve from (0, 0) to (1, 1) + */ + KisCubicCurve emptyCurve(); + protected: void setValueRange(qreal min, qreal max); /** * Read the option using the prefix in argument */ void readNamedOptionSetting(const QString& prefix, const KisPropertiesConfigurationSP setting); QString m_name; KisPaintOpOption::PaintopCategory m_category; bool m_checkable; bool m_checked; bool m_useCurve; bool m_useSameCurve; bool m_separateCurveValue; /** * Curve that is being used instead of sensors' internal ones * in case "Use the same curve" is checked. */ KisCubicCurve m_commonCurve; int m_curveMode; QMap m_sensorMap; private: qreal m_value; qreal m_minValue; qreal m_maxValue; }; #endif diff --git a/plugins/paintops/libpaintop/kis_dab_cache_base.cpp b/plugins/paintops/libpaintop/kis_dab_cache_base.cpp index 0c15329c5d..d3d3adceb9 100644 --- a/plugins/paintops/libpaintop/kis_dab_cache_base.cpp +++ b/plugins/paintops/libpaintop/kis_dab_cache_base.cpp @@ -1,284 +1,288 @@ /* * 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_dab_cache_base.h" #include #include "kis_color_source.h" #include "kis_paint_device.h" #include "kis_brush.h" #include #include #include #include #include #include #include struct PrecisionValues { qreal angle; qreal sizeFrac; qreal subPixel; qreal softnessFactor; + qreal ratio; }; const qreal eps = 1e-6; static const PrecisionValues precisionLevels[] = { - {M_PI / 180, 0.05, 1, 0.01}, - {M_PI / 180, 0.01, 1, 0.01}, - {M_PI / 180, 0, 1, 0.01}, - {M_PI / 180, 0, 0.5, 0.01}, - {eps, 0, eps, eps} + {M_PI / 180, 0.05, 1, 0.01, 0.05}, + {M_PI / 180, 0.01, 1, 0.01, 0.01}, + {M_PI / 180, 0, 1, 0.01, eps}, + {M_PI / 180, 0, 0.5, 0.01, eps}, + { eps, 0, eps, eps, eps} }; struct KisDabCacheBase::SavedDabParameters { KoColor color; qreal angle; int width; int height; qreal subPixelX; qreal subPixelY; qreal softnessFactor; + qreal ratio; int index; MirrorProperties mirrorProperties; bool compare(const SavedDabParameters &rhs, int precisionLevel) const { const PrecisionValues &prec = precisionLevels[precisionLevel]; return color == rhs.color && qAbs(angle - rhs.angle) <= prec.angle && qAbs(width - rhs.width) <= (int)(prec.sizeFrac * width) && qAbs(height - rhs.height) <= (int)(prec.sizeFrac * height) && qAbs(subPixelX - rhs.subPixelX) <= prec.subPixel && qAbs(subPixelY - rhs.subPixelY) <= prec.subPixel && qAbs(softnessFactor - rhs.softnessFactor) <= prec.softnessFactor && + qAbs(ratio - rhs.ratio) <= prec.ratio && index == rhs.index && mirrorProperties.horizontalMirror == rhs.mirrorProperties.horizontalMirror && mirrorProperties.verticalMirror == rhs.mirrorProperties.verticalMirror; } }; struct KisDabCacheBase::Private { Private() : mirrorOption(0), precisionOption(0), subPixelPrecisionDisabled(false) {} KisPressureMirrorOption *mirrorOption; KisPrecisionOption *precisionOption; bool subPixelPrecisionDisabled; SavedDabParameters lastSavedDabParameters; static qreal positiveFraction(qreal x); }; KisDabCacheBase::KisDabCacheBase() : m_d(new Private()) { } KisDabCacheBase::~KisDabCacheBase() { delete m_d; } void KisDabCacheBase::setMirrorPostprocessing(KisPressureMirrorOption *option) { m_d->mirrorOption = option; } void KisDabCacheBase::setPrecisionOption(KisPrecisionOption *option) { m_d->precisionOption = option; } void KisDabCacheBase::disableSubpixelPrecision() { m_d->subPixelPrecisionDisabled = true; } inline KisDabCacheBase::SavedDabParameters KisDabCacheBase::getDabParameters(KisBrushSP brush, const KoColor& color, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor, MirrorProperties mirrorProperties) { SavedDabParameters params; params.color = color; params.angle = shape.rotation(); params.width = brush->maskWidth(shape, subPixelX, subPixelY, info); params.height = brush->maskHeight(shape, subPixelX, subPixelY, info); params.subPixelX = subPixelX; params.subPixelY = subPixelY; params.softnessFactor = softnessFactor; params.index = brush->brushIndex(info); params.mirrorProperties = mirrorProperties; + params.ratio = shape.ratio(); return params; } bool KisDabCacheBase::needSeparateOriginal(KisTextureProperties *textureOption, KisPressureSharpnessOption *sharpnessOption) const { return (textureOption && textureOption->m_enabled) || (sharpnessOption && sharpnessOption->isChecked()); } struct KisDabCacheBase::DabPosition { DabPosition(const QRect &_rect, const QPointF &_subPixel, qreal _realAngle) : rect(_rect), subPixel(_subPixel), realAngle(_realAngle) { } QRect rect; QPointF subPixel; qreal realAngle; }; qreal KisDabCacheBase::Private::positiveFraction(qreal x) { qint32 unused = 0; qreal fraction = 0.0; KisPaintOp::splitCoordinate(x, &unused, &fraction); return fraction; } inline KisDabCacheBase::DabPosition KisDabCacheBase::calculateDabRect(KisBrushSP brush, const QPointF &cursorPoint, KisDabShape shape, const KisPaintInformation& info, const MirrorProperties &mirrorProperties, KisPressureSharpnessOption *sharpnessOption) { qint32 x = 0, y = 0; qreal subPixelX = 0.0, subPixelY = 0.0; if (mirrorProperties.coordinateSystemFlipped) { shape = KisDabShape(shape.scale(), shape.ratio(), 2 * M_PI - shape.rotation()); } QPointF hotSpot = brush->hotSpot(shape, info); QPointF pt = cursorPoint - hotSpot; if (sharpnessOption) { sharpnessOption->apply(info, pt, x, y, subPixelX, subPixelY); } else { KisPaintOp::splitCoordinate(pt.x(), &x, &subPixelX); KisPaintOp::splitCoordinate(pt.y(), &y, &subPixelY); } if (m_d->subPixelPrecisionDisabled) { subPixelX = 0; subPixelY = 0; } if (qIsNaN(subPixelX)) { subPixelX = 0; } if (qIsNaN(subPixelY)) { subPixelY = 0; } int width = brush->maskWidth(shape, subPixelX, subPixelY, info); int height = brush->maskHeight(shape, subPixelX, subPixelY, info); if (mirrorProperties.horizontalMirror) { subPixelX = Private::positiveFraction(-(cursorPoint.x() + hotSpot.x())); width = brush->maskWidth(shape, subPixelX, subPixelY, info); x = qRound(cursorPoint.x() + subPixelX + hotSpot.x()) - width; } if (mirrorProperties.verticalMirror) { subPixelY = Private::positiveFraction(-(cursorPoint.y() + hotSpot.y())); height = brush->maskHeight(shape, subPixelX, subPixelY, info); y = qRound(cursorPoint.y() + subPixelY + hotSpot.y()) - height; } return DabPosition(QRect(x, y, width, height), QPointF(subPixelX, subPixelY), shape.rotation()); } void KisDabCacheBase::fetchDabGenerationInfo(bool hasDabInCache, KisDabCacheUtils::DabRenderingResources *resources, const KisDabCacheUtils::DabRequestInfo &request, KisDabCacheUtils::DabGenerationInfo *di, bool *shouldUseCache) { di->info = request.info; di->softnessFactor = request.softnessFactor; if (m_d->mirrorOption) { di->mirrorProperties = m_d->mirrorOption->apply(request.info); } DabPosition position = calculateDabRect(resources->brush, request.cursorPoint, request.shape, request.info, di->mirrorProperties, resources->sharpnessOption.data()); di->shape = KisDabShape(request.shape.scale(), request.shape.ratio(), position.realAngle); di->dstDabRect = position.rect; di->subPixel = position.subPixel; di->solidColorFill = !resources->colorSource || resources->colorSource->isUniformColor(); di->paintColor = resources->colorSource && resources->colorSource->isUniformColor() ? resources->colorSource->uniformColor() : request.color; SavedDabParameters newParams = getDabParameters(resources->brush, di->paintColor, di->shape, di->info, di->subPixel.x(), di->subPixel.y(), di->softnessFactor, di->mirrorProperties); int precisionLevel = 4; if (m_d->precisionOption) { const int effectiveDabSize = qMin(newParams.width, newParams.height); precisionLevel = m_d->precisionOption->effectivePrecisionLevel(effectiveDabSize) - 1; } *shouldUseCache = hasDabInCache && di->solidColorFill && newParams.compare(m_d->lastSavedDabParameters, precisionLevel); if (!*shouldUseCache) { m_d->lastSavedDabParameters = newParams; } di->needsPostprocessing = needSeparateOriginal(resources->textureOption.data(), resources->sharpnessOption.data()); }