1419 lines
61 KiB
Diff
1419 lines
61 KiB
Diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index 1f436c90cc..e0da6c7a1d 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -12,6 +12,17 @@ message(STATUS "Using CMake version: ${CMAKE_VERSION}")
|
|
# Select the Qt version we want to use
|
|
# Qt5 is the default, set QT_MAJOR_VERSION to 6 to look for Qt6
|
|
option(BUILD_WITH_QT6 "Build against Qt 6" OFF)
|
|
+option(KRITA_EINK_SLIM "Build the light, monochrome E Ink painting configuration" OFF)
|
|
+option(KRITA_XIAOMI_EINK "Enable Xiaomi Mi Reader's optional EPD adapter" OFF)
|
|
+if(KRITA_EINK_SLIM)
|
|
+ set(BUILD_TESTING OFF CACHE BOOL "Tests are excluded from the E Ink build" FORCE)
|
|
+ set(ENABLE_UPDATERS OFF CACHE BOOL "Updaters are excluded from the E Ink build" FORCE)
|
|
+ set(FOUNDATION_BUILD OFF CACHE BOOL "Release extras are excluded from the E Ink build" FORCE)
|
|
+ add_compile_definitions(KRITA_EINK_SLIM=1)
|
|
+endif()
|
|
+if(KRITA_XIAOMI_EINK)
|
|
+ add_compile_definitions(KRITA_XIAOMI_EINK=1)
|
|
+endif()
|
|
|
|
if (BUILD_WITH_QT6)
|
|
set(MIN_QT_VERSION 6.0.0)
|
|
diff --git a/libs/ui/KisApplication.cpp b/libs/ui/KisApplication.cpp
|
|
index a3bcdb2846..9b7eb6d33f 100644
|
|
--- a/libs/ui/KisApplication.cpp
|
|
+++ b/libs/ui/KisApplication.cpp
|
|
@@ -549,7 +549,11 @@ bool KisApplication::start(const KisApplicationArguments &args)
|
|
KConfigGroup group(KSharedConfig::openConfig(), "theme");
|
|
#ifndef Q_OS_HAIKU
|
|
Digikam::ThemeManager themeManager;
|
|
+#ifdef KRITA_EINK_SLIM
|
|
+ themeManager.setCurrentTheme("Krita light");
|
|
+#else
|
|
themeManager.setCurrentTheme(group.readEntry("Theme", "Krita dark"));
|
|
+#endif
|
|
#endif
|
|
|
|
ResetStarting resetStarting(d->splashScreen, args.filenames().count()); // remove the splash when done
|
|
diff --git a/libs/ui/KisMainWindow.cpp b/libs/ui/KisMainWindow.cpp
|
|
index d3a1e0afe8..a926a254f9 100644
|
|
--- a/libs/ui/KisMainWindow.cpp
|
|
+++ b/libs/ui/KisMainWindow.cpp
|
|
@@ -377,7 +377,11 @@ KisMainWindow::KisMainWindow(QUuid uuid)
|
|
d->viewManager = new KisViewManager(this, actionCollection());
|
|
KConfigGroup group( KSharedConfig::openConfig(), "theme");
|
|
#ifndef Q_OS_HAIKU
|
|
+#ifdef KRITA_EINK_SLIM
|
|
+ d->themeManager = new Digikam::ThemeManager("Krita light", this);
|
|
+#else
|
|
d->themeManager = new Digikam::ThemeManager(group.readEntry("Theme", "Krita dark"), this);
|
|
+#endif
|
|
#endif
|
|
d->windowStateConfig = KSharedConfig::openConfig()->group("MainWindow");
|
|
|
|
@@ -2937,7 +2941,11 @@ void KisMainWindow::configChanged()
|
|
|
|
KConfigGroup group( KSharedConfig::openConfig(), "theme");
|
|
#ifndef Q_OS_HAIKU
|
|
+#ifdef KRITA_EINK_SLIM
|
|
+ d->themeManager->setCurrentTheme("Krita light");
|
|
+#else
|
|
d->themeManager->setCurrentTheme(group.readEntry("Theme", "Krita dark"));
|
|
+#endif
|
|
#endif
|
|
d->actionManager()->updateGUI();
|
|
|
|
diff --git a/libs/ui/thememanager.cpp b/libs/ui/thememanager.cpp
|
|
index 81ca21e9c5..99e1a2a94d 100644
|
|
--- a/libs/ui/thememanager.cpp
|
|
+++ b/libs/ui/thememanager.cpp
|
|
@@ -109,7 +109,11 @@ QString ThemeManager::currentThemeName() const
|
|
}
|
|
if (themeName.isEmpty()) {
|
|
//qDebug() << "\tfallback";
|
|
+#ifdef KRITA_EINK_SLIM
|
|
+ themeName = "Krita light";
|
|
+#else
|
|
themeName = "Krita dark";
|
|
+#endif
|
|
}
|
|
//qDebug() << "\tresult" << themeName;
|
|
return themeName;
|
|
diff --git a/packaging/android/apk/AndroidManifest.xml b/packaging/android/apk/AndroidManifest.xml
|
|
index 553aff4c3d..e092710933 100644
|
|
--- a/packaging/android/apk/AndroidManifest.xml
|
|
+++ b/packaging/android/apk/AndroidManifest.xml
|
|
@@ -1,5 +1,5 @@
|
|
<?xml version='1.0' encoding='utf-8'?>
|
|
-<manifest package="org.krita" xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
|
|
+<manifest package="rip.crit.ms.org.krita" xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
|
|
|
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
|
|
diff --git a/packaging/android/apk/build.gradle b/packaging/android/apk/build.gradle
|
|
index be3b33441a..bd9ad0b8d7 100644
|
|
--- a/packaging/android/apk/build.gradle
|
|
+++ b/packaging/android/apk/build.gradle
|
|
@@ -35,6 +35,7 @@ ext {
|
|
// tldr; at one time, either qt5AndroidDir or qtResPath is valid and points
|
|
// to the same files
|
|
qtResPath = System.getenv('KRITA_BUILD_APPBUNDLE') ? 'extra-bundle-deps/android/java' : ""
|
|
+ einkProfile = (System.getenv('KRITA_XIAOMI_EINK') ?: '0') in ['1', 'ON', 'true', 'TRUE']
|
|
}
|
|
|
|
tasks.register('configure') {
|
|
@@ -141,7 +142,7 @@ android {
|
|
* `package` attribute in the manifest, because androiddeployqt from Qt 5.15.x
|
|
* does not know how to fetch the package name from the Manifest
|
|
*/
|
|
- namespace = "org.krita"
|
|
+ namespace = "rip.crit.ms.org.krita"
|
|
|
|
ndkPath ndkpath
|
|
|
|
@@ -153,8 +154,10 @@ android {
|
|
|
|
sourceSets {
|
|
main {
|
|
+ def uNoteSource = System.getenv('UNOTE_SOURCE_DIR')
|
|
+ def uNotePlusSource = System.getenv('UNOTE_PLUS_SOURCE_DIR')
|
|
manifest.srcFile 'AndroidManifest.xml'
|
|
- java.srcDirs = [qt5AndroidDir + '/src', qtResPath + '/src', 'src', 'java']
|
|
+ java.srcDirs = [qt5AndroidDir + '/src', qtResPath + '/src', 'src', 'java'] + (uNoteSource ? [uNoteSource] : []) + (uNotePlusSource ? [uNotePlusSource] : [])
|
|
aidl.srcDirs = [qt5AndroidDir + '/src', qtResPath + '/src', 'src', 'aidl']
|
|
res.srcDirs = [qt5AndroidDir + '/res', qtResPath + '/res', 'res']
|
|
resources.srcDirs = ['src']
|
|
@@ -170,8 +173,24 @@ android {
|
|
}
|
|
}
|
|
|
|
+ signingConfigs {
|
|
+ release {
|
|
+ def releaseStore = System.getenv('MISSTRING_RELEASE_STORE_FILE')
|
|
+ if (releaseStore != null) {
|
|
+ storeFile file(releaseStore)
|
|
+ storePassword System.getenv('MISSTRING_RELEASE_STORE_PASSWORD')
|
|
+ keyAlias System.getenv('MISSTRING_RELEASE_KEY_ALIAS')
|
|
+ keyPassword System.getenv('MISSTRING_RELEASE_KEY_PASSWORD')
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
buildTypes {
|
|
release {
|
|
+ if (System.getenv('MISSTRING_RELEASE_STORE_FILE') != null) {
|
|
+ signingConfig signingConfigs.release
|
|
+ }
|
|
minifyEnabled true
|
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
|
'proguard-rules.pro'
|
|
@@ -201,6 +220,10 @@ android {
|
|
def versionRelease = 0
|
|
|
|
defaultConfig {
|
|
+ applicationId "rip.crit.ms.org.krita"
|
|
+ if (einkProfile) {
|
|
+ ndk { abiFilters 'arm64-v8a' }
|
|
+ }
|
|
compileSdk androidCompileSdkVersion.toInteger()
|
|
|
|
// even though buildToolsVersion is deprecated,
|
|
@@ -208,7 +231,8 @@ android {
|
|
// AGP will use the default one
|
|
buildToolsVersion "36.0.0"
|
|
|
|
- ndkVersion "27.3.13750724"
|
|
+ // Keep AGP's declared NDK aligned with the NDK selected by ENV.sh or CI.
|
|
+ ndkVersion(System.getenv('KRITA_ANDROID_NDK_VERSION') ?: "28.2.13676358")
|
|
|
|
targetSdkVersion 36
|
|
minSdkVersion 24
|
|
diff --git a/packaging/android/apk/proguard-rules.pro b/packaging/android/apk/proguard-rules.pro
|
|
index 91e3b04949..c671c1f36c 100644
|
|
--- a/packaging/android/apk/proguard-rules.pro
|
|
+++ b/packaging/android/apk/proguard-rules.pro
|
|
@@ -8,3 +8,5 @@
|
|
-keep class org.qtproject.qt5.android.** { *; }
|
|
-keep class org.krita.android.** { *; }
|
|
-keep class org.libsdl.app.** { *; }
|
|
+-keep class com.moan.fakerkepdlib.EpdUtil { *; }
|
|
+-keepclasseswithmembernames class * { native <methods>; }
|
|
diff --git a/packaging/android/apk/src/org/krita/android/MainActivity.java b/packaging/android/apk/src/org/krita/android/MainActivity.java
|
|
index 093be86d51..611d322258 100644
|
|
--- a/packaging/android/apk/src/org/krita/android/MainActivity.java
|
|
+++ b/packaging/android/apk/src/org/krita/android/MainActivity.java
|
|
@@ -30,8 +30,10 @@ import android.widget.Toast;
|
|
import androidx.annotation.RequiresApi;
|
|
|
|
import java.util.List;
|
|
+import java.util.Optional;
|
|
|
|
-import org.krita.R;
|
|
+import rip.crit.ms.org.krita.R;
|
|
+import rip.crit.ms.unote.UNoteEinkController;
|
|
import org.libsdl.app.SDLAudioManager;
|
|
import org.qtproject.qt5.android.QtNative;
|
|
import org.qtproject.qt5.android.bindings.QtActivity;
|
|
@@ -48,6 +50,8 @@ public class MainActivity extends QtActivity {
|
|
private boolean serviceStarted = false;
|
|
private boolean inFullScreen = false;
|
|
private DonationDialog mDonationDialog = null;
|
|
+ private XiaomiEinkController einkController = null;
|
|
+ private UNoteEinkController uNoteEinkController = null;
|
|
|
|
@Override
|
|
@SuppressLint("MissingSuperCall")
|
|
@@ -71,6 +75,22 @@ public class MainActivity extends QtActivity {
|
|
Log.i(TAG, "LibsLoaded");
|
|
haveLibsLoaded = true;
|
|
|
|
+ // The Xiaomi EBC overlay is a private, panel-global resource. Do not acquire it
|
|
+ // automatically. A stale or competing overlay can make the whole display appear
|
|
+ // blank until the system compositor recovers. The adapter remains an explicit
|
|
+ // experimental opt-in while geometry and waveform calibration are device-verified.
|
|
+ final boolean xiaomiOverlayEnabled = getSharedPreferences("krita_eink", MODE_PRIVATE)
|
|
+ .getBoolean("xiaomi_overlay_experimental", false);
|
|
+ if (xiaomiOverlayEnabled) {
|
|
+ try {
|
|
+ einkController = new XiaomiEinkController(getWindow());
|
|
+ Log.i(TAG, "Xiaomi E Ink overlay enabled by user preference");
|
|
+ } catch (LinkageError | RuntimeException unavailable) {
|
|
+ Log.i(TAG, "Xiaomi E Ink overlay unavailable", unavailable);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (einkController == null && xiaomiOverlayEnabled) {
|
|
+ Optional<UNoteEinkController> controller = UNoteEinkController.create(
|
|
+ this, getWindow().getDecorView(), null);
|
|
+ if (controller.isPresent()) {
|
|
+ uNoteEinkController = controller.get();
|
|
+ Log.i(TAG, "uNote E Ink provider enabled");
|
|
+ }
|
|
+ }
|
|
+
|
|
DonationHelper.getInstance();
|
|
DonationProduct.initAllProducts(this);
|
|
}
|
|
@@ -165,9 +185,30 @@ public class MainActivity extends QtActivity {
|
|
Log.i(TAG, "[onDestroy]");
|
|
startServiceGeneric(DocumentSaverService.KILL_PROCESS);
|
|
|
|
+ if (einkController != null) {
|
|
+ einkController.close();
|
|
+ einkController = null;
|
|
+ }
|
|
+ if (uNoteEinkController != null) {
|
|
+ uNoteEinkController.close();
|
|
+ uNoteEinkController = null;
|
|
+ }
|
|
super.onDestroy();
|
|
}
|
|
|
|
+ @Override
|
|
+ public boolean dispatchTouchEvent(MotionEvent event) {
|
|
+ if (einkController != null && (event.isFromSource(InputDevice.SOURCE_STYLUS)
|
|
+ || event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS)) {
|
|
+ einkController.onMotionEvent(event);
|
|
+ }
|
|
+ if (uNoteEinkController != null) {
|
|
+ uNoteEinkController.onMotionEvent(event);
|
|
+ }
|
|
+ // Qt receives the original event, including its untouched pressure and tilt axes.
|
|
+ return super.dispatchTouchEvent(event);
|
|
+ }
|
|
+
|
|
@Override
|
|
public boolean onKeyUp(final int keyCode, final KeyEvent event) {
|
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
|
|
index 44d0867fb0..17726b2e35 100644
|
|
--- a/plugins/CMakeLists.txt
|
|
+++ b/plugins/CMakeLists.txt
|
|
@@ -1,17 +1,24 @@
|
|
-add_subdirectory( assistants )
|
|
add_subdirectory( color )
|
|
add_subdirectory( dockers )
|
|
-add_subdirectory( extensions )
|
|
-add_subdirectory( filters )
|
|
add_subdirectory( flake )
|
|
-add_subdirectory( generators )
|
|
+add_subdirectory( filters )
|
|
add_subdirectory( impex )
|
|
-add_subdirectory( metadata )
|
|
add_subdirectory( paintops )
|
|
add_subdirectory( tools )
|
|
-add_subdirectory( qt )
|
|
|
|
-if ((HAVE_PYQT5 OR HAVE_PYQT6) AND HAVE_SIP AND HAVE_PYTHONLIBS)
|
|
+if (NOT KRITA_EINK_SLIM)
|
|
+ add_subdirectory( assistants )
|
|
+ add_subdirectory( extensions )
|
|
+ add_subdirectory( generators )
|
|
+ add_subdirectory( metadata )
|
|
+ add_subdirectory( qt )
|
|
+endif()
|
|
+
|
|
+if(KRITA_EINK_SLIM)
|
|
+ add_subdirectory(extensions/einksync)
|
|
+endif()
|
|
+
|
|
+if (NOT KRITA_EINK_SLIM AND (HAVE_PYQT5 OR HAVE_PYQT6) AND HAVE_SIP AND HAVE_PYTHONLIBS)
|
|
add_subdirectory( python )
|
|
endif()
|
|
|
|
diff --git a/plugins/dockers/CMakeLists.txt b/plugins/dockers/CMakeLists.txt
|
|
index f71b5b1042..de9ce3930d 100644
|
|
--- a/plugins/dockers/CMakeLists.txt
|
|
+++ b/plugins/dockers/CMakeLists.txt
|
|
@@ -1,34 +1,40 @@
|
|
add_subdirectory(layerdocker)
|
|
-if(HAVE_OPENEXR AND NOT ANDROID)
|
|
- add_subdirectory(smallcolorselector)
|
|
-endif()
|
|
add_subdirectory(specificcolorselector)
|
|
-add_subdirectory(digitalmixer)
|
|
-add_subdirectory(advancedcolorselector)
|
|
add_subdirectory(presetdocker)
|
|
-add_subdirectory(historydocker)
|
|
-add_subdirectory(channeldocker)
|
|
-add_subdirectory(artisticcolorselector)
|
|
-add_subdirectory(tasksetdocker)
|
|
-add_subdirectory(compositiondocker)
|
|
-add_subdirectory(patterndocker)
|
|
-add_subdirectory(griddocker)
|
|
-add_subdirectory(arrangedocker)
|
|
-if(HAVE_OCIO AND NOT ANDROID AND NOT HAIKU)
|
|
- add_subdirectory(lut)
|
|
-endif()
|
|
-add_subdirectory(overview)
|
|
-add_subdirectory(palettedocker)
|
|
-add_subdirectory(animation)
|
|
-add_subdirectory(presethistory)
|
|
-add_subdirectory(svgcollectiondocker)
|
|
-add_subdirectory(histogram)
|
|
-add_subdirectory(gamutmask)
|
|
-add_subdirectory(recorder)
|
|
-add_subdirectory(touchdocker)
|
|
-add_subdirectory(logdocker)
|
|
-add_subdirectory(snapshotdocker)
|
|
-add_subdirectory(storyboarddocker)
|
|
-add_subdirectory(widegamutcolorselector)
|
|
add_subdirectory(brushhud)
|
|
-add_subdirectory(textproperties)
|
|
+add_subdirectory(touchdocker)
|
|
+if(KRITA_EINK_SLIM)
|
|
+ add_subdirectory(einkmonochromeselector)
|
|
+endif()
|
|
+
|
|
+if(NOT KRITA_EINK_SLIM)
|
|
+ if(HAVE_OPENEXR AND NOT ANDROID)
|
|
+ add_subdirectory(smallcolorselector)
|
|
+ endif()
|
|
+ add_subdirectory(digitalmixer)
|
|
+ add_subdirectory(advancedcolorselector)
|
|
+ add_subdirectory(historydocker)
|
|
+ add_subdirectory(channeldocker)
|
|
+ add_subdirectory(artisticcolorselector)
|
|
+ add_subdirectory(tasksetdocker)
|
|
+ add_subdirectory(compositiondocker)
|
|
+ add_subdirectory(patterndocker)
|
|
+ add_subdirectory(griddocker)
|
|
+ add_subdirectory(arrangedocker)
|
|
+ if(HAVE_OCIO AND NOT ANDROID AND NOT HAIKU)
|
|
+ add_subdirectory(lut)
|
|
+ endif()
|
|
+ add_subdirectory(overview)
|
|
+ add_subdirectory(palettedocker)
|
|
+ add_subdirectory(animation)
|
|
+ add_subdirectory(presethistory)
|
|
+ add_subdirectory(svgcollectiondocker)
|
|
+ add_subdirectory(histogram)
|
|
+ add_subdirectory(gamutmask)
|
|
+ add_subdirectory(recorder)
|
|
+ add_subdirectory(logdocker)
|
|
+ add_subdirectory(snapshotdocker)
|
|
+ add_subdirectory(storyboarddocker)
|
|
+ add_subdirectory(widegamutcolorselector)
|
|
+ add_subdirectory(textproperties)
|
|
+endif()
|
|
diff --git a/plugins/filters/CMakeLists.txt b/plugins/filters/CMakeLists.txt
|
|
index 91625abf9a..2d65b45fc4 100644
|
|
--- a/plugins/filters/CMakeLists.txt
|
|
+++ b/plugins/filters/CMakeLists.txt
|
|
@@ -1,34 +1,37 @@
|
|
-add_subdirectory( tests )
|
|
add_subdirectory( blur )
|
|
add_subdirectory( colors )
|
|
add_subdirectory( colorsfilters )
|
|
add_subdirectory( convolutionfilters )
|
|
-add_subdirectory( gaussianhighpass)
|
|
-add_subdirectory( embossfilter )
|
|
-add_subdirectory( example )
|
|
-add_subdirectory( fastcolortransfer )
|
|
add_subdirectory( imageenhancement )
|
|
-add_subdirectory( noisefilter )
|
|
-add_subdirectory( oilpaintfilter )
|
|
-add_subdirectory( pixelizefilter )
|
|
-add_subdirectory( raindropsfilter )
|
|
-add_subdirectory( randompickfilter )
|
|
-add_subdirectory( roundcorners )
|
|
-add_subdirectory( smalltilesfilter )
|
|
add_subdirectory( unsharp )
|
|
-add_subdirectory( wavefilter )
|
|
add_subdirectory( levelfilter )
|
|
-add_subdirectory( dodgeburn )
|
|
-add_subdirectory( phongbumpmap )
|
|
add_subdirectory( posterize )
|
|
-add_subdirectory( indexcolors )
|
|
add_subdirectory( normalize )
|
|
-add_subdirectory( gradientmap )
|
|
add_subdirectory( threshold )
|
|
-add_subdirectory( halftone )
|
|
add_subdirectory( edgedetection )
|
|
-add_subdirectory( convertheightnormalmap )
|
|
-add_subdirectory( asccdl )
|
|
-add_subdirectory( palettize )
|
|
-add_subdirectory( resettransparent )
|
|
-add_subdirectory( propagatecolors )
|
|
+
|
|
+if(NOT KRITA_EINK_SLIM)
|
|
+ add_subdirectory( tests )
|
|
+ add_subdirectory( gaussianhighpass)
|
|
+ add_subdirectory( embossfilter )
|
|
+ add_subdirectory( example )
|
|
+ add_subdirectory( fastcolortransfer )
|
|
+ add_subdirectory( noisefilter )
|
|
+ add_subdirectory( oilpaintfilter )
|
|
+ add_subdirectory( pixelizefilter )
|
|
+ add_subdirectory( raindropsfilter )
|
|
+ add_subdirectory( randompickfilter )
|
|
+ add_subdirectory( roundcorners )
|
|
+ add_subdirectory( smalltilesfilter )
|
|
+ add_subdirectory( wavefilter )
|
|
+ add_subdirectory( dodgeburn )
|
|
+ add_subdirectory( phongbumpmap )
|
|
+ add_subdirectory( indexcolors )
|
|
+ add_subdirectory( gradientmap )
|
|
+ add_subdirectory( halftone )
|
|
+ add_subdirectory( convertheightnormalmap )
|
|
+ add_subdirectory( asccdl )
|
|
+ add_subdirectory( palettize )
|
|
+ add_subdirectory( resettransparent )
|
|
+ add_subdirectory( propagatecolors )
|
|
+endif()
|
|
diff --git a/plugins/impex/CMakeLists.txt b/plugins/impex/CMakeLists.txt
|
|
index e2758ac756..1f15fb0c2e 100644
|
|
--- a/plugins/impex/CMakeLists.txt
|
|
+++ b/plugins/impex/CMakeLists.txt
|
|
@@ -1,63 +1,54 @@
|
|
project(kritafilters)
|
|
|
|
add_subdirectory(libkra)
|
|
-
|
|
if(JPEG_FOUND AND HAVE_LCMS2)
|
|
- add_subdirectory(jpeg)
|
|
-endif()
|
|
-
|
|
-if(TIFF_FOUND AND JPEG_FOUND)
|
|
- add_subdirectory(tiff)
|
|
+ add_subdirectory(jpeg)
|
|
endif()
|
|
-
|
|
if(PNG_FOUND)
|
|
add_subdirectory(png)
|
|
- add_subdirectory(csv)
|
|
-endif()
|
|
-
|
|
-if(OpenEXR_FOUND)
|
|
- add_subdirectory(exr)
|
|
endif()
|
|
-
|
|
-if(Poppler_Qt${QT_MAJOR_VERSION}_FOUND)
|
|
- add_subdirectory(pdf)
|
|
-endif()
|
|
-
|
|
-if(OpenJPEG_FOUND AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
|
- add_subdirectory(jp2)
|
|
-endif()
|
|
-
|
|
-if(KF5KDcraw_FOUND OR KDcrawQt6_FOUND)
|
|
- add_subdirectory(raw)
|
|
-endif()
|
|
-
|
|
-add_subdirectory(svg)
|
|
add_subdirectory(qimageio)
|
|
add_subdirectory(ora)
|
|
-add_subdirectory(psd)
|
|
-add_subdirectory(qml)
|
|
-add_subdirectory(tga)
|
|
-add_subdirectory(heightmap)
|
|
add_subdirectory(brush)
|
|
-add_subdirectory(spriter)
|
|
add_subdirectory(kra)
|
|
-
|
|
-if (GIF_FOUND)
|
|
- add_subdirectory(gif)
|
|
-endif()
|
|
-
|
|
-if (HEIF_FOUND)
|
|
- add_subdirectory(heif)
|
|
-endif()
|
|
-
|
|
add_subdirectory(krz)
|
|
|
|
-if (WebP_FOUND)
|
|
- add_subdirectory(webp)
|
|
+if(NOT KRITA_EINK_SLIM)
|
|
+ if(TIFF_FOUND AND JPEG_FOUND)
|
|
+ add_subdirectory(tiff)
|
|
+ endif()
|
|
+ if(PNG_FOUND)
|
|
+ add_subdirectory(csv)
|
|
+ endif()
|
|
+ if(OpenEXR_FOUND)
|
|
+ add_subdirectory(exr)
|
|
+ endif()
|
|
+ if(Poppler_Qt${QT_MAJOR_VERSION}_FOUND)
|
|
+ add_subdirectory(pdf)
|
|
+ endif()
|
|
+ if(OpenJPEG_FOUND AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
|
+ add_subdirectory(jp2)
|
|
+ endif()
|
|
+ if(KF5KDcraw_FOUND OR KDcrawQt6_FOUND)
|
|
+ add_subdirectory(raw)
|
|
+ endif()
|
|
+ add_subdirectory(svg)
|
|
+ add_subdirectory(psd)
|
|
+ add_subdirectory(qml)
|
|
+ add_subdirectory(tga)
|
|
+ add_subdirectory(heightmap)
|
|
+ add_subdirectory(spriter)
|
|
+ if(GIF_FOUND)
|
|
+ add_subdirectory(gif)
|
|
+ endif()
|
|
+ if(HEIF_FOUND)
|
|
+ add_subdirectory(heif)
|
|
+ endif()
|
|
+ if(WebP_FOUND)
|
|
+ add_subdirectory(webp)
|
|
+ endif()
|
|
+ if(JPEGXL_FOUND)
|
|
+ add_subdirectory(jxl)
|
|
+ endif()
|
|
+ add_subdirectory(rgbe)
|
|
endif()
|
|
-
|
|
-if (JPEGXL_FOUND)
|
|
- add_subdirectory(jxl)
|
|
-endif()
|
|
-
|
|
-add_subdirectory(rgbe)
|
|
diff --git a/plugins/paintops/CMakeLists.txt b/plugins/paintops/CMakeLists.txt
|
|
index ae0b64d5d9..1073b86054 100644
|
|
--- a/plugins/paintops/CMakeLists.txt
|
|
+++ b/plugins/paintops/CMakeLists.txt
|
|
@@ -2,20 +2,23 @@ include_directories(libpaintop)
|
|
|
|
add_subdirectory( libpaintop )
|
|
add_subdirectory( defaultpaintops )
|
|
-add_subdirectory( hairy )
|
|
-add_subdirectory( deform )
|
|
-add_subdirectory( curvebrush )
|
|
-add_subdirectory( spray )
|
|
-add_subdirectory( filterop )
|
|
-add_subdirectory( experiment )
|
|
-add_subdirectory( particle )
|
|
-add_subdirectory( gridbrush )
|
|
-add_subdirectory( hatching)
|
|
-add_subdirectory( sketch )
|
|
-add_subdirectory( colorsmudge )
|
|
add_subdirectory( roundmarker )
|
|
-add_subdirectory( tangentnormal )
|
|
|
|
-if(LibMyPaint_FOUND)
|
|
+if (NOT KRITA_EINK_SLIM)
|
|
+ add_subdirectory( hairy )
|
|
+ add_subdirectory( deform )
|
|
+ add_subdirectory( curvebrush )
|
|
+ add_subdirectory( spray )
|
|
+ add_subdirectory( filterop )
|
|
+ add_subdirectory( experiment )
|
|
+ add_subdirectory( particle )
|
|
+ add_subdirectory( gridbrush )
|
|
+ add_subdirectory( hatching)
|
|
+ add_subdirectory( sketch )
|
|
+ add_subdirectory( colorsmudge )
|
|
+ add_subdirectory( tangentnormal )
|
|
+endif()
|
|
+
|
|
+if(NOT KRITA_EINK_SLIM AND LibMyPaint_FOUND)
|
|
add_subdirectory( mypaint )
|
|
endif()
|
|
diff --git a/plugins/tools/CMakeLists.txt b/plugins/tools/CMakeLists.txt
|
|
index abf3ba2558..9549961fb0 100644
|
|
--- a/plugins/tools/CMakeLists.txt
|
|
+++ b/plugins/tools/CMakeLists.txt
|
|
@@ -2,16 +2,19 @@ add_subdirectory( basictools )
|
|
add_subdirectory( defaulttool )
|
|
add_subdirectory( selectiontools )
|
|
add_subdirectory( tool_crop )
|
|
-add_subdirectory( tool_polygon )
|
|
-add_subdirectory( tool_polyline )
|
|
add_subdirectory( tool_transform2 )
|
|
-add_subdirectory( tool_dyna )
|
|
-add_subdirectory( karbonplugins )
|
|
-add_subdirectory( tool_lazybrush )
|
|
-add_subdirectory( tool_smart_patch )
|
|
-add_subdirectory( svgtexttool )
|
|
-add_subdirectory( tool_enclose_and_fill )
|
|
-add_subdirectory( tool_knife )
|
|
+
|
|
+if (NOT KRITA_EINK_SLIM)
|
|
+ add_subdirectory( tool_polygon )
|
|
+ add_subdirectory( tool_polyline )
|
|
+ add_subdirectory( tool_dyna )
|
|
+ add_subdirectory( karbonplugins )
|
|
+ add_subdirectory( tool_lazybrush )
|
|
+ add_subdirectory( tool_smart_patch )
|
|
+ add_subdirectory( svgtexttool )
|
|
+ add_subdirectory( tool_enclose_and_fill )
|
|
+ add_subdirectory( tool_knife )
|
|
+endif()
|
|
|
|
|
|
install( FILES
|
|
diff --git a/EINK_SLIM.md b/EINK_SLIM.md
|
|
new file mode 100644
|
|
index 0000000000..d42a7c9760
|
|
--- /dev/null
|
|
+++ b/EINK_SLIM.md
|
|
@@ -0,0 +1,17 @@
|
|
+# E Ink slim build and Xiaomi adapter
|
|
+
|
|
+Configure the general E Ink build with `-DKRITA_EINK_SLIM=ON`. It keeps the raster painting core, layers, the default and round-marker brush engines, brush presets, the freehand brush stabilizer, pressure and tilt events, selection, crop and transform tools, common blur and color filters, KRA, KRZ, ORA, PNG and JPEG support. It excludes tests, updates, assistants, Python, generators, animation, advanced brush engines, advanced dockers and uncommon importers.
|
|
+
|
|
+The slim build forces Krita Light and replaces the advanced color selector with a 16-step monochrome selector. It includes continuous Syncthing export and Google Drive OAuth with PKCE. uNote preserves Android pressure and tilt data and aggregates partial-refresh damage.
|
|
+
|
|
+`-DKRITA_XIAOMI_EINK=ON` packages optional Xiaomi hardware support but never acquires the panel-global EBC overlay automatically. It needs the explicit `krita_eink/xiaomi_overlay_experimental` preference after per-device geometry and waveform calibration. Stylus events continue to Qt unchanged, including pressure and tilt. The default path remains the normal Android compositor so a failed or conflicting private overlay cannot leave the panel in a broken state.
|
|
+
|
|
+The Xiaomi adapter supports only `arm64-v8a` because the recovered vendor library is ARM64. The slim build itself remains usable without that adapter and falls back to normal Android rendering.
|
|
+
|
|
+Krita's normal Android dependency toolchain is still required. Add this to the existing Android CMake arguments.
|
|
+
|
|
+```text
|
|
+-DKRITA_EINK_SLIM=ON -DBUILD_TESTING=OFF -DENABLE_UPDATERS=OFF
|
|
+```
|
|
+
|
|
+Add `-DKRITA_XIAOMI_EINK=ON` and export `KRITA_XIAOMI_EINK=1` only for the Xiaomi Android package. Gradle then restricts the package to ARM64 and bundles the recovered native E Ink library from `packaging/android/apk/lib/arm64-v8a`.
|
|
diff --git a/packaging/android/apk/.gitignore b/packaging/android/apk/.gitignore
|
|
new file mode 100644
|
|
index 0000000000..abc78ba75a
|
|
--- /dev/null
|
|
+++ b/packaging/android/apk/.gitignore
|
|
@@ -0,0 +1,2 @@
|
|
+/.gradle/
|
|
+/build/
|
|
diff --git a/packaging/android/apk/settings.gradle b/packaging/android/apk/settings.gradle
|
|
new file mode 100644
|
|
index 0000000000..8e0bdcbb55
|
|
--- /dev/null
|
|
+++ b/packaging/android/apk/settings.gradle
|
|
@@ -0,0 +1,9 @@
|
|
+pluginManagement {
|
|
+ repositories {
|
|
+ google()
|
|
+ mavenCentral()
|
|
+ gradlePluginPortal()
|
|
+ }
|
|
+}
|
|
+
|
|
+rootProject.name = "krita-build-apk"
|
|
diff --git a/packaging/android/apk/lib/arm64-v8a/libfakeRKEpdUtils.so b/packaging/android/apk/lib/arm64-v8a/libfakeRKEpdUtils.so
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..e730040b53e0cb4eb6bdb443d069c4d550012a99
|
|
GIT binary patch
|
|
literal 22592
|
|
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
|
|
|
|
literal 0
|
|
HcmV?d00001
|
|
|
|
diff --git a/packaging/android/apk/src/com/moan/fakerkepdlib/EpdUtil.java b/packaging/android/apk/src/com/moan/fakerkepdlib/EpdUtil.java
|
|
new file mode 100644
|
|
index 0000000000..694e122052
|
|
--- /dev/null
|
|
+++ b/packaging/android/apk/src/com/moan/fakerkepdlib/EpdUtil.java
|
|
@@ -0,0 +1,57 @@
|
|
+package com.moan.fakerkepdlib;
|
|
+
|
|
+import android.graphics.Bitmap;
|
|
+import android.graphics.PointF;
|
|
+import android.graphics.RectF;
|
|
+
|
|
+/** Binary-compatible binding reconstructed from MoanNote Smali. */
|
|
+public final class EpdUtil {
|
|
+ static { System.loadLibrary("fakeRKEpdUtils"); }
|
|
+ private EpdUtil() {}
|
|
+ private static native int JniCall_ClearOverlayBuffer(int id, int color, int sendOsd);
|
|
+ private static native int JniCall_CommitOverlay(Bitmap bitmap, int id, float left, float top, float right, float bottom, float x, float y);
|
|
+ private static native int JniCall_ObtainEBC(float[] bounds);
|
|
+ private static native void JniCall_ReleaseEBC(int id);
|
|
+ private static native int JniCall_SetOverLayBgControl(int id, int mode);
|
|
+ private static native int JniCall_SetOverLayCommit(int id, int enabled);
|
|
+ private static native int JniCall_test(int[] values);
|
|
+
|
|
+ public static EBCData sInitEBC() {
|
|
+ float[] bounds = new float[4];
|
|
+ int id = JniCall_ObtainEBC(bounds);
|
|
+ if (id < 0) return null;
|
|
+ EBCData data = new EBCData();
|
|
+ data.mDataID = id;
|
|
+ data.mCommitBufRect.set(bounds[0], bounds[1], bounds[2], bounds[3]);
|
|
+ return data;
|
|
+ }
|
|
+ public static void sRecycleEBC(EBCData data) { if (data != null) JniCall_ReleaseEBC(data.mDataID); }
|
|
+ public static int sSetFastBgRefresh(EBCData data, boolean enabled) { return JniCall_SetOverLayBgControl(data.mDataID, enabled ? 0 : 1); }
|
|
+ public static int sSetFastHandWrite(EBCData data, boolean enabled) { return JniCall_SetOverLayCommit(data.mDataID, enabled ? 1 : 0); }
|
|
+ public static int sCommitColor(EBCData data, EBCCommitRequest request, EBCCommitResult result) {
|
|
+ return JniCall_ClearOverlayBuffer(data.mDataID, request.mCommitColor, request.mSendOsd);
|
|
+ }
|
|
+ public static int sCommitImage(EBCData data, EBCCommitRequest request, EBCCommitResult result) {
|
|
+ int code = request.mCommitType == EBCCommitRequest.COMMIT_CLEAR_COLOR
|
|
+ ? JniCall_ClearOverlayBuffer(data.mDataID, request.mCommitColor, request.mSendOsd)
|
|
+ : JniCall_CommitOverlay(request.mCommitImg, data.mDataID, request.mCommitSrcRect.left,
|
|
+ request.mCommitSrcRect.top, request.mCommitSrcRect.right, request.mCommitSrcRect.bottom,
|
|
+ request.mCommitPt.x, request.mCommitPt.y);
|
|
+ if (code > 0) result.mResultCode = 0;
|
|
+ return code;
|
|
+ }
|
|
+ public static final class EBCData {
|
|
+ public int mDataID;
|
|
+ public final RectF mCommitBufRect = new RectF();
|
|
+ public int getEBCRect(RectF destination) { destination.set(mCommitBufRect); return 0; }
|
|
+ }
|
|
+ public static final class EBCCommitResult { public int mResultCode; }
|
|
+ public static final class EBCCommitRequest {
|
|
+ public static final int COMMIT_NORMAL = 0, COMMIT_WITH_SCALE = 1, COMMIT_WITH_ROTATE = 2, COMMIT_CLEAR_COLOR = 4;
|
|
+ public int mCommitColor, mCommitType, mSendOsd;
|
|
+ public final RectF mCommitDstRect = new RectF();
|
|
+ public Bitmap mCommitImg;
|
|
+ public final PointF mCommitPt = new PointF();
|
|
+ public final RectF mCommitSrcRect = new RectF();
|
|
+ }
|
|
+}
|
|
diff --git a/packaging/android/apk/src/org/krita/android/XiaomiEinkController.java b/packaging/android/apk/src/org/krita/android/XiaomiEinkController.java
|
|
new file mode 100644
|
|
index 0000000000..7ef635c4ff
|
|
--- /dev/null
|
|
+++ b/packaging/android/apk/src/org/krita/android/XiaomiEinkController.java
|
|
@@ -0,0 +1,91 @@
|
|
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
+package org.krita.android;
|
|
+
|
|
+import android.graphics.Bitmap;
|
|
+import android.graphics.Rect;
|
|
+import android.os.Handler;
|
|
+import android.os.HandlerThread;
|
|
+import android.util.Log;
|
|
+import android.view.MotionEvent;
|
|
+import android.view.PixelCopy;
|
|
+import android.view.Window;
|
|
+
|
|
+import com.moan.fakerkepdlib.EpdUtil;
|
|
+import rip.crit.ms.unote.DamageTracker;
|
|
+import rip.crit.ms.unote.PenInput;
|
|
+import rip.crit.ms.unote.PenSample;
|
|
+
|
|
+/** Sends pressure-aware dirty regions from Krita's Qt surface to Xiaomi's E Ink overlay. */
|
|
+final class XiaomiEinkController implements AutoCloseable {
|
|
+ private static final String TAG = "krita.XiaomiEink";
|
|
+ private final Window window;
|
|
+ private final HandlerThread worker = new HandlerThread("krita-eink-commit");
|
|
+ private final Handler handler;
|
|
+ private final EpdUtil.EBCData buffer;
|
|
+ private final DamageTracker pending = new DamageTracker();
|
|
+ private boolean captureScheduled;
|
|
+ private boolean closed;
|
|
+
|
|
+ XiaomiEinkController(Window window) {
|
|
+ this.window = window;
|
|
+ worker.start();
|
|
+ handler = new Handler(worker.getLooper());
|
|
+ buffer = EpdUtil.sInitEBC();
|
|
+ if (buffer == null) throw new IllegalStateException("Xiaomi E Ink buffer unavailable");
|
|
+ EpdUtil.sSetFastBgRefresh(buffer, true);
|
|
+ EpdUtil.sSetFastHandWrite(buffer, true);
|
|
+ }
|
|
+
|
|
+ void onMotionEvent(MotionEvent event) {
|
|
+ if (closed || event.getPointerCount() == 0) return;
|
|
+ final int index = event.getActionIndex();
|
|
+ synchronized (pending) {
|
|
+ for (PenSample sample : PenInput.samples(event)) {
|
|
+ pending.add(sample, 18f, 42f, 8f);
|
|
+ }
|
|
+ if (!captureScheduled) {
|
|
+ captureScheduled = true;
|
|
+ handler.postDelayed(this::capture, event.getActionMasked() == MotionEvent.ACTION_UP ? 0 : 16);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void capture() {
|
|
+ final int width = window.getDecorView().getWidth();
|
|
+ final int height = window.getDecorView().getHeight();
|
|
+ if (closed || width <= 0 || height <= 0) return;
|
|
+ final Rect dirty;
|
|
+ synchronized (pending) {
|
|
+ dirty = pending.take(width, height);
|
|
+ captureScheduled = false;
|
|
+ }
|
|
+ if (dirty.isEmpty()) return;
|
|
+ final Bitmap frame = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
+ PixelCopy.request(window, frame, result -> {
|
|
+ try {
|
|
+ if (result == PixelCopy.SUCCESS && !closed) commit(frame, dirty);
|
|
+ else Log.w(TAG, "PixelCopy failed with code " + result);
|
|
+ } finally {
|
|
+ frame.recycle();
|
|
+ }
|
|
+ }, handler);
|
|
+ }
|
|
+
|
|
+ private void commit(Bitmap frame, Rect dirty) {
|
|
+ EpdUtil.EBCCommitRequest request = new EpdUtil.EBCCommitRequest();
|
|
+ request.mCommitImg = frame;
|
|
+ request.mCommitSrcRect.set(dirty);
|
|
+ request.mCommitPt.set(dirty.left, dirty.top);
|
|
+ int result = EpdUtil.sCommitImage(buffer, request, new EpdUtil.EBCCommitResult());
|
|
+ if (result < 0) Log.w(TAG, "E Ink commit failed with code " + result);
|
|
+ }
|
|
+
|
|
+ @Override public void close() {
|
|
+ if (closed) return;
|
|
+ closed = true;
|
|
+ EpdUtil.sSetFastHandWrite(buffer, false);
|
|
+ EpdUtil.sSetFastBgRefresh(buffer, false);
|
|
+ EpdUtil.sRecycleEBC(buffer);
|
|
+ worker.quitSafely();
|
|
+ }
|
|
+}
|
|
diff --git a/plugins/dockers/einkmonochromeselector/CMakeLists.txt b/plugins/dockers/einkmonochromeselector/CMakeLists.txt
|
|
new file mode 100644
|
|
index 0000000000..7a5d4f02b5
|
|
--- /dev/null
|
|
+++ b/plugins/dockers/einkmonochromeselector/CMakeLists.txt
|
|
@@ -0,0 +1,3 @@
|
|
+kis_add_library(kritaeinkmonochromeselector MODULE einkmonochromeselector.cpp)
|
|
+target_link_libraries(kritaeinkmonochromeselector kritaui)
|
|
+install(TARGETS kritaeinkmonochromeselector DESTINATION ${KRITA_PLUGIN_INSTALL_DIR})
|
|
diff --git a/plugins/dockers/einkmonochromeselector/einkmonochromeselector.cpp b/plugins/dockers/einkmonochromeselector/einkmonochromeselector.cpp
|
|
new file mode 100644
|
|
index 0000000000..dd7f50cb0d
|
|
--- /dev/null
|
|
+++ b/plugins/dockers/einkmonochromeselector/einkmonochromeselector.cpp
|
|
@@ -0,0 +1,73 @@
|
|
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
+#include <QDockWidget>
|
|
+#include <QColor>
|
|
+#include <QGridLayout>
|
|
+#include <QPushButton>
|
|
+#include <QLatin1Char>
|
|
+#include <QWidget>
|
|
+#include <KisViewManager.h>
|
|
+#include <kis_canvas_resource_provider.h>
|
|
+#include <kis_mainwindow_observer.h>
|
|
+#include <KoColor.h>
|
|
+#include <KoDockFactoryBase.h>
|
|
+#include <KoDockRegistry.h>
|
|
+#include <kpluginfactory.h>
|
|
+#include <klocalizedstring.h>
|
|
+
|
|
+class EinkMonochromeDock final : public QDockWidget, public KisMainwindowObserver
|
|
+{
|
|
+ Q_OBJECT
|
|
+public:
|
|
+ EinkMonochromeDock() : QDockWidget(i18n("E Ink Grays")) {
|
|
+ auto *panel = new QWidget(this);
|
|
+ auto *layout = new QGridLayout(panel);
|
|
+ layout->setSpacing(3);
|
|
+ for (int step = 0; step < 16; ++step) {
|
|
+ const int gray = 255 - step * 17;
|
|
+ auto *button = new QPushButton(QString::number(step), panel);
|
|
+ button->setMinimumSize(44, 44);
|
|
+ button->setStyleSheet(QString("background:#%1%1%1;color:%2")
|
|
+ .arg(gray, 2, 16, QLatin1Char('0')).arg(gray < 128 ? "white" : "black"));
|
|
+ button->setAccessibleName(i18n("Gray level %1 of 15", step));
|
|
+ connect(button, &QPushButton::clicked, this, [this, gray] {
|
|
+ if (!m_view) return;
|
|
+ KoColor color = m_view->canvasResourceProvider()->fgColor();
|
|
+ color.fromQColor(QColor(gray, gray, gray));
|
|
+ m_view->canvasResourceProvider()->slotSetFGColor(color);
|
|
+ });
|
|
+ layout->addWidget(button, step / 4, step % 4);
|
|
+ }
|
|
+ setWidget(panel);
|
|
+ }
|
|
+ QString observerName() override { return QStringLiteral("EinkMonochromeDock"); }
|
|
+ void setCanvas(KoCanvasBase *canvas) override { setEnabled(canvas != nullptr); }
|
|
+ void unsetCanvas() override { setEnabled(false); }
|
|
+ void setViewManager(KisViewManager *view) override { m_view = view; }
|
|
+private:
|
|
+ KisViewManager *m_view = nullptr;
|
|
+};
|
|
+
|
|
+class EinkMonochromeFactory final : public KoDockFactoryBase
|
|
+{
|
|
+public:
|
|
+ QString id() const override { return QStringLiteral("EinkMonochromeSelector"); }
|
|
+ Qt::DockWidgetArea defaultDockWidgetArea() const { return Qt::RightDockWidgetArea; }
|
|
+ QDockWidget *createDockWidget() override {
|
|
+ auto *dock = new EinkMonochromeDock();
|
|
+ dock->setObjectName(id());
|
|
+ return dock;
|
|
+ }
|
|
+ DockPosition defaultDockPosition() const override { return DockMinimized; }
|
|
+};
|
|
+
|
|
+class EinkMonochromePlugin final : public QObject
|
|
+{
|
|
+ Q_OBJECT
|
|
+public:
|
|
+ EinkMonochromePlugin(QObject *parent, const QVariantList &) : QObject(parent) {
|
|
+ KoDockRegistry::instance()->add(new EinkMonochromeFactory());
|
|
+ }
|
|
+};
|
|
+
|
|
+K_PLUGIN_FACTORY_WITH_JSON(EinkMonochromePluginFactory, "krita_einkmonochromeselector.json", registerPlugin<EinkMonochromePlugin>();)
|
|
+#include "einkmonochromeselector.moc"
|
|
diff --git a/plugins/dockers/einkmonochromeselector/krita_einkmonochromeselector.json b/plugins/dockers/einkmonochromeselector/krita_einkmonochromeselector.json
|
|
new file mode 100644
|
|
index 0000000000..494e2cfeed
|
|
--- /dev/null
|
|
+++ b/plugins/dockers/einkmonochromeselector/krita_einkmonochromeselector.json
|
|
@@ -0,0 +1,7 @@
|
|
+{
|
|
+ "Id": "EinkMonochromeSelector",
|
|
+ "Type": "Service",
|
|
+ "X-KDE-Library": "kritaeinkmonochromeselector",
|
|
+ "X-KDE-ServiceTypes": ["Krita/Dock"],
|
|
+ "X-Krita-Version": "28"
|
|
+}
|
|
diff --git a/plugins/extensions/einksync/CMakeLists.txt b/plugins/extensions/einksync/CMakeLists.txt
|
|
new file mode 100644
|
|
index 0000000000..eaabbecb80
|
|
--- /dev/null
|
|
+++ b/plugins/extensions/einksync/CMakeLists.txt
|
|
@@ -0,0 +1,5 @@
|
|
+set(kritaeinksync_SOURCES EinkSyncPlugin.cpp)
|
|
+kis_add_library(kritaeinksync MODULE ${kritaeinksync_SOURCES})
|
|
+target_link_libraries(kritaeinksync kritaui Qt${QT_MAJOR_VERSION}::Network)
|
|
+install(TARGETS kritaeinksync DESTINATION ${KRITA_PLUGIN_INSTALL_DIR})
|
|
+install(FILES EinkSyncPlugin.action DESTINATION ${KDE_INSTALL_DATADIR}/krita/actions)
|
|
diff --git a/plugins/extensions/einksync/EinkSyncPlugin.action b/plugins/extensions/einksync/EinkSyncPlugin.action
|
|
new file mode 100644
|
|
index 0000000000..b714a5941c
|
|
--- /dev/null
|
|
+++ b/plugins/extensions/einksync/EinkSyncPlugin.action
|
|
@@ -0,0 +1,13 @@
|
|
+<?xml version="1.0" encoding="UTF-8"?>
|
|
+<ActionCollection version="2" name="Krita">
|
|
+ <Actions category="Tools">
|
|
+ <text>E Ink Sync</text>
|
|
+ <Action name="eink_sync_settings">
|
|
+ <text>E Ink Sync Settings...</text>
|
|
+ <toolTip>Configure Syncthing folder and Google Drive export</toolTip>
|
|
+ </Action>
|
|
+ <Action name="eink_sync_now">
|
|
+ <text>Sync Current Document Now</text>
|
|
+ </Action>
|
|
+ </Actions>
|
|
+</ActionCollection>
|
|
diff --git a/plugins/extensions/einksync/EinkSyncPlugin.cpp b/plugins/extensions/einksync/EinkSyncPlugin.cpp
|
|
new file mode 100644
|
|
index 0000000000..3aaafe0b15
|
|
--- /dev/null
|
|
+++ b/plugins/extensions/einksync/EinkSyncPlugin.cpp
|
|
@@ -0,0 +1,264 @@
|
|
+/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
+#include <KisActionPlugin.h>
|
|
+#include <KisDocument.h>
|
|
+#include <KisMainWindow.h>
|
|
+#include <KisPart.h>
|
|
+#include <KisViewManager.h>
|
|
+#include <kis_action.h>
|
|
+#include <kis_action_manager.h>
|
|
+#include <kconfiggroup.h>
|
|
+#include <kpluginfactory.h>
|
|
+#include <ksharedconfig.h>
|
|
+#include <klocalizedstring.h>
|
|
+#include <QCheckBox>
|
|
+#include <QCryptographicHash>
|
|
+#include <QDesktopServices>
|
|
+#include <QDialog>
|
|
+#include <QDialogButtonBox>
|
|
+#include <QDir>
|
|
+#include <QFile>
|
|
+#include <QFileInfo>
|
|
+#include <QFormLayout>
|
|
+#include <QHttpMultiPart>
|
|
+#include <QHostAddress>
|
|
+#include <QJsonDocument>
|
|
+#include <QJsonObject>
|
|
+#include <QLabel>
|
|
+#include <QLineEdit>
|
|
+#include <QMessageBox>
|
|
+#include <QNetworkAccessManager>
|
|
+#include <QNetworkReply>
|
|
+#include <QNetworkRequest>
|
|
+#include <QPushButton>
|
|
+#include <QSaveFile>
|
|
+#include <QTimer>
|
|
+#include <QTcpServer>
|
|
+#include <QTcpSocket>
|
|
+#include <QUuid>
|
|
+#include <QUrlQuery>
|
|
+#include <functional>
|
|
+
|
|
+class EinkSyncPlugin final : public KisActionPlugin
|
|
+{
|
|
+ Q_OBJECT
|
|
+public:
|
|
+ EinkSyncPlugin(QObject *parent, const QVariantList &) : KisActionPlugin(parent) {
|
|
+ connect(viewManager()->actionManager()->createAction("eink_sync_settings"), &QAction::triggered,
|
|
+ this, &EinkSyncPlugin::showSettings);
|
|
+ connect(viewManager()->actionManager()->createAction("eink_sync_now"), &QAction::triggered,
|
|
+ this, [this] { syncFile(viewManager()->document() ? viewManager()->document()->path() : QString()); });
|
|
+ connect(KisPart::instance(), &KisPart::sigDocumentSaved, this, &EinkSyncPlugin::queueSync);
|
|
+ m_debounce.setSingleShot(true);
|
|
+ m_debounce.setInterval(1200);
|
|
+ connect(&m_debounce, &QTimer::timeout, this, [this] { syncFile(m_pendingPath); });
|
|
+ }
|
|
+
|
|
+private Q_SLOTS:
|
|
+ void queueSync(const QString &path) {
|
|
+ m_pendingPath = path;
|
|
+ m_debounce.start();
|
|
+ }
|
|
+
|
|
+ void showSettings() {
|
|
+ KConfigGroup cfg(KSharedConfig::openConfig(), "EinkSync");
|
|
+ QDialog dialog(viewManager()->mainWindow());
|
|
+ dialog.setWindowTitle(i18n("E Ink Continuous Sync"));
|
|
+ auto *layout = new QFormLayout(&dialog);
|
|
+ auto *enabled = new QCheckBox(i18n("Sync after every successful save"), &dialog);
|
|
+ enabled->setChecked(cfg.readEntry("Enabled", true));
|
|
+ auto *folder = new QLineEdit(cfg.readEntry("SyncthingFolder", QString()), &dialog);
|
|
+ folder->setPlaceholderText(i18n("App-accessible Syncthing folder"));
|
|
+ auto *stUrl = new QLineEdit(cfg.readEntry("SyncthingUrl", "http://127.0.0.1:8384"), &dialog);
|
|
+ auto *stKey = new QLineEdit(cfg.readEntry("SyncthingApiKey", QString()), &dialog);
|
|
+ stKey->setEchoMode(QLineEdit::Password);
|
|
+ auto *client = new QLineEdit(cfg.readEntry("GoogleClientId", QString()), &dialog);
|
|
+ client->setPlaceholderText(i18n("Google OAuth desktop client ID"));
|
|
+ auto *google = new QPushButton(i18n("Authorize Google Drive"), &dialog);
|
|
+ layout->addRow(enabled);
|
|
+ layout->addRow(i18n("Syncthing folder"), folder);
|
|
+ layout->addRow(i18n("Syncthing address"), stUrl);
|
|
+ layout->addRow(i18n("Syncthing API key"), stKey);
|
|
+ layout->addRow(i18n("Google client ID"), client);
|
|
+ layout->addRow(google);
|
|
+ auto *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, &dialog);
|
|
+ layout->addRow(buttons);
|
|
+ connect(google, &QPushButton::clicked, &dialog, [this, client] { beginGoogleLocalAuthorization(client->text()); });
|
|
+ connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
|
+ connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
|
+ if (dialog.exec() == QDialog::Accepted) {
|
|
+ cfg.writeEntry("Enabled", enabled->isChecked());
|
|
+ cfg.writeEntry("SyncthingFolder", folder->text().trimmed());
|
|
+ cfg.writeEntry("SyncthingUrl", stUrl->text().trimmed());
|
|
+ cfg.writeEntry("SyncthingApiKey", stKey->text().trimmed());
|
|
+ cfg.writeEntry("GoogleClientId", client->text().trimmed());
|
|
+ cfg.sync();
|
|
+ }
|
|
+ }
|
|
+
|
|
+private:
|
|
+ void syncFile(const QString &path) {
|
|
+ KConfigGroup cfg(KSharedConfig::openConfig(), "EinkSync");
|
|
+ if (!cfg.readEntry("Enabled", true) || path.isEmpty() || !QFileInfo::exists(path)) return;
|
|
+ const QString folder = cfg.readEntry("SyncthingFolder", QString());
|
|
+ if (!folder.isEmpty()) exportToSyncthing(path, folder);
|
|
+ const QString refreshToken = cfg.readEntry("GoogleRefreshToken", QString());
|
|
+ if (!refreshToken.isEmpty()) refreshGoogleTokenAndUpload(path, refreshToken, cfg.readEntry("GoogleClientId", QString()));
|
|
+ }
|
|
+
|
|
+ // Shared Syncthing contract (mirrors SyncthingHelper used by Notable
|
|
+ // and Share File): stage the file into the Syncthing folder, then ask
|
|
+ // the local Syncthing REST endpoint to rescan so peers converge.
|
|
+ void exportToSyncthing(const QString &source, const QString &folder) {
|
|
+ if (!QDir().mkpath(folder)) return;
|
|
+ QFile input(source);
|
|
+ QSaveFile output(QDir(folder).filePath(QFileInfo(source).fileName()));
|
|
+ if (!input.open(QIODevice::ReadOnly) || !output.open(QIODevice::WriteOnly)) return;
|
|
+ while (!input.atEnd()) output.write(input.read(1024 * 1024));
|
|
+ if (!output.commit()) return;
|
|
+ KConfigGroup cfg(KSharedConfig::openConfig(), "EinkSync");
|
|
+ const QString base = cfg.readEntry("SyncthingUrl", QStringLiteral("http://127.0.0.1:8384"));
|
|
+ QNetworkRequest scan{QUrl(base + QStringLiteral("/rest/db/scan?folder=")
|
|
+ + QString::fromUtf8(QUrl::toPercentEncoding(QFileInfo(source).fileName())))};
|
|
+ const QString key = cfg.readEntry("SyncthingApiKey", QString());
|
|
+ if (!key.isEmpty()) scan.setRawHeader("X-API-Key", key.toUtf8());
|
|
+ auto *reply = m_network.post(scan, QByteArray());
|
|
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
|
+ }
|
|
+
|
|
+ void beginGoogleLocalAuthorization(const QString &clientId) {
|
|
+ if (clientId.isEmpty()) return;
|
|
+ m_clientId = clientId;
|
|
+ delete m_oauthServer;
|
|
+ m_oauthServer = new QTcpServer(this);
|
|
+ if (!m_oauthServer->listen(QHostAddress::LocalHost, 0)) {
|
|
+ QMessageBox::warning(viewManager()->mainWindow(), i18n("Google Drive"), i18n("Could not open the local OAuth callback port"));
|
|
+ return;
|
|
+ }
|
|
+ m_oauthState = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
+ m_pkceVerifier = QUuid::createUuid().toString(QUuid::WithoutBraces)
|
|
+ + QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
+ const QByteArray challenge = QCryptographicHash::hash(m_pkceVerifier.toUtf8(), QCryptographicHash::Sha256)
|
|
+ .toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
|
+ m_redirectUri = QString("http://127.0.0.1:%1/oauth2/callback").arg(m_oauthServer->serverPort());
|
|
+
|
|
+ connect(m_oauthServer, &QTcpServer::newConnection, this, [this] {
|
|
+ QTcpSocket *socket = m_oauthServer->nextPendingConnection();
|
|
+ connect(socket, &QTcpSocket::readyRead, this, [this, socket] {
|
|
+ const QByteArray firstLine = socket->readLine();
|
|
+ const QList<QByteArray> parts = firstLine.split(' ');
|
|
+ QUrl callback(parts.size() > 1 ? QString("http://127.0.0.1") + QString::fromUtf8(parts[1]) : QString());
|
|
+ QUrlQuery query(callback);
|
|
+ const bool valid = query.queryItemValue("state") == m_oauthState && !query.queryItemValue("code").isEmpty();
|
|
+ const QByteArray message = valid
|
|
+ ? "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nAuthorization complete. Return to Krita."
|
|
+ : "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nAuthorization failed. Return to Krita.";
|
|
+ socket->write(message);
|
|
+ socket->disconnectFromHost();
|
|
+ m_oauthServer->close();
|
|
+ if (!valid) return;
|
|
+ QUrlQuery form;
|
|
+ form.addQueryItem("client_id", m_clientId);
|
|
+ form.addQueryItem("code", query.queryItemValue("code"));
|
|
+ form.addQueryItem("code_verifier", m_pkceVerifier);
|
|
+ form.addQueryItem("redirect_uri", m_redirectUri);
|
|
+ form.addQueryItem("grant_type", "authorization_code");
|
|
+ tokenRequest(form, [this](const QJsonObject &data) {
|
|
+ const QString refresh = data.value("refresh_token").toString();
|
|
+ if (refresh.isEmpty()) {
|
|
+ QMessageBox::warning(viewManager()->mainWindow(), i18n("Google Drive"), i18n("Google returned no refresh token"));
|
|
+ return;
|
|
+ }
|
|
+ KConfigGroup cfg(KSharedConfig::openConfig(), "EinkSync");
|
|
+ cfg.writeEntry("GoogleRefreshToken", refresh);
|
|
+ cfg.sync();
|
|
+ QMessageBox::information(viewManager()->mainWindow(), i18n("Google Drive"), i18n("Authorization complete"));
|
|
+ });
|
|
+ });
|
|
+ });
|
|
+
|
|
+ QUrl authorization("https://accounts.google.com/o/oauth2/v2/auth");
|
|
+ QUrlQuery query;
|
|
+ query.addQueryItem("client_id", clientId);
|
|
+ query.addQueryItem("redirect_uri", m_redirectUri);
|
|
+ query.addQueryItem("response_type", "code");
|
|
+ query.addQueryItem("scope", "https://www.googleapis.com/auth/drive.file");
|
|
+ query.addQueryItem("code_challenge", QString::fromLatin1(challenge));
|
|
+ query.addQueryItem("code_challenge_method", "S256");
|
|
+ query.addQueryItem("state", m_oauthState);
|
|
+ query.addQueryItem("access_type", "offline");
|
|
+ query.addQueryItem("prompt", "consent");
|
|
+ authorization.setQuery(query);
|
|
+ QDesktopServices::openUrl(authorization);
|
|
+ }
|
|
+
|
|
+ void refreshGoogleTokenAndUpload(const QString &path, const QString &refreshToken, const QString &clientId) {
|
|
+ QUrlQuery form;
|
|
+ form.addQueryItem("client_id", clientId);
|
|
+ form.addQueryItem("refresh_token", refreshToken);
|
|
+ form.addQueryItem("grant_type", "refresh_token");
|
|
+ tokenRequest(form, [this, path](const QJsonObject &data) {
|
|
+ const QString token = data.value("access_token").toString();
|
|
+ if (!token.isEmpty()) uploadGoogleDrive(path, token);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ void tokenRequest(const QUrlQuery &form, std::function<void(const QJsonObject &)> callback) {
|
|
+ QNetworkRequest request{QUrl("https://oauth2.googleapis.com/token")};
|
|
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
|
+ auto *reply = m_network.post(request, form.toString(QUrl::FullyEncoded).toUtf8());
|
|
+ connect(reply, &QNetworkReply::finished, this, [reply, callback] {
|
|
+ const QJsonObject data = QJsonDocument::fromJson(reply->readAll()).object();
|
|
+ reply->deleteLater();
|
|
+ callback(data);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ void uploadGoogleDrive(const QString &path, const QString &accessToken) {
|
|
+ auto *file = new QFile(path);
|
|
+ if (!file->open(QIODevice::ReadOnly)) { delete file; return; }
|
|
+ auto *body = new QHttpMultiPart(QHttpMultiPart::RelatedType);
|
|
+ QHttpPart metadata;
|
|
+ metadata.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
|
|
+ metadata.setBody(QJsonDocument(QJsonObject{{"name", QFileInfo(path).fileName()}}).toJson(QJsonDocument::Compact));
|
|
+ QHttpPart media;
|
|
+ media.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
|
|
+ media.setBodyDevice(file);
|
|
+ file->setParent(body);
|
|
+ body->append(metadata);
|
|
+ body->append(media);
|
|
+ KConfigGroup cfg(KSharedConfig::openConfig(), "EinkSync");
|
|
+ const QString idKey = "GoogleFileId_" + QString::fromLatin1(QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha256).toHex());
|
|
+ const QString fileId = cfg.readEntry(idKey, QString());
|
|
+ const QString endpoint = fileId.isEmpty()
|
|
+ ? QStringLiteral("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart")
|
|
+ : QStringLiteral("https://www.googleapis.com/upload/drive/v3/files/%1?uploadType=multipart").arg(fileId);
|
|
+ QNetworkRequest request{QUrl(endpoint)};
|
|
+ request.setRawHeader("Authorization", QByteArray("Bearer ") + accessToken.toUtf8());
|
|
+ QNetworkReply *reply = fileId.isEmpty()
|
|
+ ? m_network.post(request, body)
|
|
+ : m_network.sendCustomRequest(request, "PATCH", body);
|
|
+ body->setParent(reply);
|
|
+ connect(reply, &QNetworkReply::finished, this, [reply, idKey] {
|
|
+ const QJsonObject response = QJsonDocument::fromJson(reply->readAll()).object();
|
|
+ const QString createdId = response.value("id").toString();
|
|
+ if (!createdId.isEmpty()) {
|
|
+ KConfigGroup cfg(KSharedConfig::openConfig(), "EinkSync");
|
|
+ cfg.writeEntry(idKey, createdId);
|
|
+ cfg.sync();
|
|
+ }
|
|
+ reply->deleteLater();
|
|
+ });
|
|
+ }
|
|
+
|
|
+ QNetworkAccessManager m_network;
|
|
+ QTimer m_debounce;
|
|
+ QString m_pendingPath;
|
|
+ QString m_clientId;
|
|
+ QString m_oauthState;
|
|
+ QString m_pkceVerifier;
|
|
+ QString m_redirectUri;
|
|
+ QTcpServer *m_oauthServer = nullptr;
|
|
+};
|
|
+
|
|
+K_PLUGIN_FACTORY_WITH_JSON(EinkSyncFactory, "kritaeinksync.json", registerPlugin<EinkSyncPlugin>();)
|
|
+#include "EinkSyncPlugin.moc"
|
|
diff --git a/plugins/extensions/einksync/kritaeinksync.json b/plugins/extensions/einksync/kritaeinksync.json
|
|
new file mode 100644
|
|
index 0000000000..1ef752cd45
|
|
--- /dev/null
|
|
+++ b/plugins/extensions/einksync/kritaeinksync.json
|
|
@@ -0,0 +1,7 @@
|
|
+{
|
|
+ "Id": "EinkSync",
|
|
+ "Type": "Service",
|
|
+ "X-KDE-Library": "kritaeinksync",
|
|
+ "X-KDE-ServiceTypes": ["Krita/ViewPlugin"],
|
|
+ "X-Krita-Version": "28"
|
|
+}
|